Shimmer
A pure-CSS shimmer sweep for text. Purely presentational — no Morphos equivalent.
Installation
npx kosmesis add shimmerpnpm dlx kosmesis add shimmeryarn dlx kosmesis add shimmerbunx kosmesis add shimmerCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { cn } from "@/lib/utils";let shimmerIdCounter = 0;export interface ShimmerProps { color?: string; duration?: number; spread?: number | string; angle?: number; reverse?: boolean; once?: boolean; disabled?: boolean; class?: string; children?: Children;}// Transparency is `-webkit-text-fill-color`, not `color` — `currentColor` in the gradient// resolves against this element's own `color`, so setting `color` would zero out the gradient.// `animation-*` stays in the stylesheet (read via `var()`) rather than inline, so `:dir(rtl)` and// reduced-motion below can override it without fighting inline-style specificity.@Component()export class Shimmer extends StatelessComponent<ShimmerProps> { private readonly _id = shimmerIdCounter++; private readonly _animName = `kosmesis-shimmer-${String(this._id)}`; private readonly _scopeClass = `kosmesis-shimmer-scope-${String(this._id)}`; render() { const { color, duration = 2000, spread = "calc(3ch + 40px)", angle = 20, reverse = false, once = false, disabled = false, class: cls, children, } = this.props; if (disabled) { return ( <span data-slot="shimmer" class={cls}> {children} </span> ); } const spreadValue = typeof spread === "number" ? `${String(spread)}px` : spread; return ( <span data-slot="shimmer" data-shimmer-reverse={reverse ? "" : undefined} class={cn(this._scopeClass, cls)} style={{ "--shimmer-anim-name": this._animName, "--shimmer-color": color ?? "oklch(from currentColor calc(l + 0.35) c h)", "--shimmer-spread": spreadValue, "--shimmer-angle": `${String(angle)}deg`, "--shimmer-duration": `${String(duration)}ms`, "--shimmer-iteration": once ? "1" : "infinite", "--shimmer-fill": once ? "forwards" : "none", }} > <style>{`@keyframes ${this._animName} { from { background-position: 200% 0, 0 0; } to { background-position: -200% 0, 0 0; } }.${this._scopeClass} { background-image: linear-gradient(var(--shimmer-angle), transparent calc(50% - var(--shimmer-spread) / 2), var(--shimmer-color), transparent calc(50% + var(--shimmer-spread) / 2)), linear-gradient(currentColor, currentColor); background-size: 250% 100%, 100% 100%; background-repeat: no-repeat; background-position: 200% 0, 0 0; -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; animation-name: var(--shimmer-anim-name); animation-duration: var(--shimmer-duration); animation-timing-function: linear; animation-iteration-count: var(--shimmer-iteration, infinite); animation-fill-mode: var(--shimmer-fill, none); animation-direction: normal;}.${this._scopeClass}[data-shimmer-reverse] { animation-direction: reverse; }.${this._scopeClass}:dir(rtl) { animation-direction: reverse; }.${this._scopeClass}[data-shimmer-reverse]:dir(rtl) { animation-direction: normal; }@media (prefers-reduced-motion: reduce) { .${this._scopeClass} { animation: none; background-image: none; -webkit-background-clip: initial; background-clip: initial; -webkit-text-fill-color: inherit; }}`}</style> {children} </span> ); }}Install the following dependencies:
npm install @praxisjs/csspnpm add @praxisjs/cssyarn add @praxisjs/cssbun add @praxisjs/cssCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";let shimmerIdCounter = 0;// Transparency is `-webkit-text-fill-color`, not `color` — `currentColor` in `backgroundImage`// resolves against this element's own `color`, so setting `color` would zero out the gradient.// `animation-*` stays here (read via `var()`) rather than inline, so `:dir(rtl)` and reduced-motion// below can override it without fighting inline-style specificity.class ShimmerStyles extends Stylesheet { $root = this.css({ backgroundImage: "linear-gradient(var(--shimmer-angle), transparent calc(50% - var(--shimmer-spread) / 2), var(--shimmer-color), transparent calc(50% + var(--shimmer-spread) / 2)), linear-gradient(currentColor, currentColor)", backgroundSize: "250% 100%, 100% 100%", backgroundRepeat: "no-repeat", backgroundPosition: "200% 0, 0 0", WebkitBackgroundClip: "text", backgroundClip: "text", WebkitTextFillColor: "transparent", animationName: "var(--shimmer-anim-name)", animationDuration: "var(--shimmer-duration)", animationTimingFunction: "linear", animationIterationCount: "var(--shimmer-iteration, infinite)", animationFillMode: "var(--shimmer-fill, none)", animationDirection: "normal", }) .on("&[data-shimmer-reverse]", { animationDirection: "reverse" }) .on("&:dir(rtl)", { animationDirection: "reverse" }) .on("&[data-shimmer-reverse]:dir(rtl)", { animationDirection: "normal" }) .media("(prefers-reduced-motion: reduce)", { animation: "none", backgroundImage: "none", WebkitBackgroundClip: "initial", backgroundClip: "initial", WebkitTextFillColor: "inherit", });}export interface ShimmerProps { color?: string; duration?: number; spread?: number | string; angle?: number; reverse?: boolean; once?: boolean; disabled?: boolean; class?: string; children?: Children;}@Component()export class Shimmer extends StatelessComponent<ShimmerProps> { @Styled(ShimmerStyles) $s!: ShimmerStyles; private readonly _animName = `kosmesis-shimmer-${String(shimmerIdCounter++)}`; render() { const { color, duration = 2000, spread = "calc(3ch + 40px)", angle = 20, reverse = false, once = false, disabled = false, class: cls, children, } = this.props; if (disabled) { return ( <span data-slot="shimmer" class={cls}> {children} </span> ); } const spreadValue = typeof spread === "number" ? `${String(spread)}px` : spread; return ( <span data-slot="shimmer" data-shimmer-reverse={reverse ? "" : undefined} class={cx(this.$s.$root, cls)} style={{ "--shimmer-anim-name": this._animName, "--shimmer-color": color ?? "oklch(from currentColor calc(l + 0.35) c h)", "--shimmer-spread": spreadValue, "--shimmer-angle": `${String(angle)}deg`, "--shimmer-duration": `${String(duration)}ms`, "--shimmer-iteration": once ? "1" : "infinite", "--shimmer-fill": once ? "forwards" : "none", }} > <style>{`@keyframes ${this._animName} { from { background-position: 200% 0, 0 0; } to { background-position: -200% 0, 0 0; } }`}</style> {children} </span> ); }}Examples
About
Purely presentational — no Morphos equivalent. Unlike shadcn's shimmer (a set of global Tailwind
utility classes shipped via the shadcn npm package), Kosmesis ships this as a copy-paste Shimmer
component: wrap any text in it instead of adding a shimmer class. The effect is pure CSS —
background-clip: text paints the text with a moving highlight gradient layered over a static
currentColor gradient, so it adapts to whatever color the wrapping element sets, in light or dark
mode, with no configuration needed. Every prop (color, duration, spread, angle) is threaded
through as a CSS custom property and read back via var() in a scoped rule injected once per
instance — the actual animation-*/background-* properties are never set inline, so the RTL and
reduced-motion overrides in that same rule always win over the per-instance custom properties.
<p className="text-muted-foreground">
<Shimmer>Generating response…</Shimmer>
</p>With Marker
A common pairing is a status dot and spinner next to shimmering status text:
<div className="flex items-center gap-2">
<Marker variant="default" aria-label="Working" />
<Spinner />
<Shimmer>Thinking…</Shimmer>
</div>Color
Use the color prop to set the highlight color explicitly — any valid CSS color, including
color-mix()/alpha syntax for opacity. Left unset, the highlight is derived automatically from
currentColor via relative color syntax (oklch(from currentColor calc(l + 0.35) c h)), so it
adapts to any text color without configuration.
<Shimmer color="oklch(0.7 0.19 260 / 70%)">Generating response…</Shimmer>
<Shimmer color="#378ADD">Generating response…</Shimmer>Duration
Use duration to set the length of one sweep in milliseconds. The default is 2000.
<Shimmer duration={1000}>Generating response…</Shimmer>Spread
Use spread to set the width of the highlight band, as a number of pixels or any CSS length
string. The default is "calc(3ch + 40px)": a fixed base plus a 3ch term that scales with the
font size.
<Shimmer spread={96}>Generating response…</Shimmer>
<Shimmer spread="5rem">Generating response…</Shimmer>Angle
Use angle to set the tilt of the highlight band, in degrees. The default is 20.
<Shimmer angle={45}>Generating response…</Shimmer>Reverse
Use reverse to sweep the highlight in the opposite direction. In RTL layouts the sweep already
follows the reading direction — see RTL.
<Shimmer reverse>Generating response…</Shimmer>Play Once
Use once to play a single sweep instead of looping — useful as a reveal when streaming completes.
Pair it with duration to control how long the sweep takes.
<Shimmer duration={1100} once>
Response generated.
</Shimmer>Disabled
Use disabled to turn the effect off and render the text normally — the component skips every
shimmer style entirely, so it's cheap to gate behind a prop.
<Shimmer disabled={!isStreaming}>Generating response…</Shimmer>Fallback
The shimmer is built on modern color features — relative color syntax and multi-layer
background-clip: text — available in all current browsers. In older browsers without support,
the highlight gradient is dropped and the text can render transparent; pass an explicit color
(a plain color, not relative-color syntax) if you need to support them.
Reduced Motion
When the user prefers reduced motion, the animation and gradient are disabled automatically and the
text renders as normal currentColor text. There is nothing to configure.
RTL
The sweep follows the reading direction — left to right in LTR, right to left in RTL — with no
extra props, driven by :dir(rtl) in the component's own scoped rule. Use reverse to flip the
direction manually in either mode.
Props
| Prop | Type | Default |
|---|---|---|
color | string | auto (brightened currentColor) |
duration | number (ms) | 2000 |
spread | number (px) | string | "calc(3ch + 40px)" |
angle | number (deg) | 20 |
reverse | boolean | false |
once | boolean | false |
disabled | boolean | false |
class | string | — |