animatedGradientText
A Text block/component from Magic UI — clean-room reimplemented for Domphy (see methodology). Call animatedGradientText() with no arguments for a working demo, or edit the code below live.
Implementation notes
Multi-stop background-clip:text gradient panned via an infinite linear background-position keyframe animation over a 300%-wide, repeating background, with speed mapped to animation duration (higher speed = shorter loop). Also implements the spec's optional 'stretch feature' — a pill wrapper whose own border carries the identical flowing gradient, via the classic dual-background-layer trick (opaque padding-box layer + gradient border-box layer, both background-position-animated in sync) rather than mask-composite, which is simpler and more broadly supported. One documented tradeoff: the upstream spec's literal default hex colors (#ffaa40 orange → #9c40ff purple) can't be used directly since Domphy forbids raw hex/rgb in style props and this theme ships no dedicated purple role — colorFrom/colorVia/colorTo are exposed as ThemeColor roles instead, defaulting to warning (orange, matches upstream) → secondary (this theme's rose/magenta family, the closest built-in role to purple) → primary (blue). Same tradeoff this package's glareHover component already documents for its own literal-color prop.
Status: ported · Reference: Magic UI original
// magicui "Animated Gradient Text" — clean-room reimplementation from the
// public behavior/visual spec only (no upstream source viewed or copied).
// Text filled with a multi-stop gradient that pans continuously and loops
// via animated `background-position` over an oversized `background-size`
// (300%) — the same `background-clip: text` panning technique as
// `animatedShinyText`, but here the gradient is fully opaque everywhere (no
// muted/bright alternation), so the whole word reads as a slow color flow
// rather than a single glint. Optionally wraps the text in a pill whose OWN
// border is filled with the identical panning gradient via the classic
// dual-background-layer "gradient border" trick (an opaque surface layer
// clipped to the padding-box, the gradient layer clipped to the
// border-box) — no `mask-composite`/pseudo-element needed.
//
// The upstream spec's default colors are literal hex (`#ffaa40` orange to
// `#9c40ff` purple) — Domphy's doctor rules forbid raw hex/rgb colors on
// style props, and this theme ships no dedicated purple family, so
// `colorFrom`/`colorVia`/`colorTo` are exposed as `ThemeColor` roles instead,
// defaulting to `"warning"` (matches upstream's orange) → `"secondary"`
// (this theme's rose/magenta family — the closest built-in role to
// "vivid purple") → `"primary"` (blue), which keeps the flow fully
// theme-aware (it follows light/dark theme swaps) at the cost of not
// accepting an arbitrary caller-supplied hex pair. Same tradeoff
// `glareHover` documents for its own literal-color prop.
import type { DomphyElement, Listener, StyleObject } from "@domphy/core";
import { hashString } from "@domphy/core";
import { type ThemeColor, themeColor, themeSpacing } from "@domphy/theme";
export interface AnimatedGradientTextProps {
/** Text content. Defaults to `"Animated Gradient Text"`. */
children?: string;
/** Flow speed multiplier — higher plays faster. Defaults to `1`. */
speed?: number;
/** First gradient stop's theme color family. Defaults to `"warning"` (orange). */
colorFrom?: ThemeColor;
/** Middle gradient stop's theme color family. Defaults to `"secondary"` (this theme's closest role to purple). */
colorVia?: ThemeColor;
/** Last gradient stop's theme color family. Defaults to `"primary"` (blue). */
colorTo?: ThemeColor;
/** Wraps the text in a pill whose border is filled with the same flowing
* gradient (a subtle ring/glow around the label). Defaults to `true`. */
showPill?: boolean;
style?: StyleObject;
}
let animatedGradientTextInstanceCounter = 0;
/**
* Text filled with a continuously flowing multi-color gradient that slides
* horizontally in an endless loop, optionally wrapped in a pill whose own
* border carries the identical flowing gradient. Call with no arguments for
* a working demo.
*/
function animatedGradientText(
props: AnimatedGradientTextProps = {},
): DomphyElement {
const text = props.children ?? "Animated Gradient Text";
const speed = props.speed ?? 1;
const colorFrom = props.colorFrom ?? "warning";
const colorVia = props.colorVia ?? "secondary";
const colorTo = props.colorTo ?? "primary";
const showPill = props.showPill ?? true;
const instanceId = ++animatedGradientTextInstanceCounter;
// Base loop is 8s at speed=1; higher speed plays faster (shorter duration).
const durationSeconds = Math.max(0.5, 8 / Math.max(speed, 0.01));
const gradientStops = (listener: Listener): string =>
`${themeColor(listener, "shift-8", colorFrom)}, ${themeColor(listener, "shift-8", colorVia)}, ${themeColor(listener, "shift-8", colorTo)}, ${themeColor(listener, "shift-8", colorFrom)}`;
const textAnimationName = `animated-gradient-text-flow-${hashString(
JSON.stringify({ instanceId, colorFrom, colorVia, colorTo }),
)}`;
const textKeyframes = {
from: { backgroundPosition: "0% 50%" },
to: { backgroundPosition: "300% 50%" },
};
const gradientSpan: DomphyElement<"span"> = {
span: text,
style: {
backgroundImage: (listener: Listener) =>
`linear-gradient(90deg, ${gradientStops(listener)})`,
backgroundSize: "300% 100%",
backgroundRepeat: "repeat",
backgroundClip: "text",
WebkitBackgroundClip: "text",
color: "transparent",
animation: `${textAnimationName} ${durationSeconds}s linear infinite`,
[`@keyframes ${textAnimationName}`]: textKeyframes,
} as StyleObject,
};
if (!showPill) return gradientSpan;
const pillAnimationName = `animated-gradient-pill-flow-${hashString(
JSON.stringify({ instanceId, colorFrom, colorVia, colorTo, ring: true }),
)}`;
const pillKeyframes = {
from: { backgroundPosition: "0 0, 0% 50%" },
to: { backgroundPosition: "0 0, 300% 50%" },
};
return {
div: [gradientSpan],
style: {
display: "inline-flex",
alignItems: "center",
paddingInline: themeSpacing(4),
paddingBlock: themeSpacing(2),
borderRadius: themeSpacing(20),
border: "1px solid transparent",
backgroundImage: (listener: Listener) =>
`linear-gradient(${themeColor(listener, "inherit")}, ${themeColor(listener, "inherit")}), linear-gradient(90deg, ${gradientStops(listener)})`,
backgroundOrigin: "border-box",
backgroundClip: "padding-box, border-box",
backgroundSize: "auto, 300% 100%",
backgroundRepeat: "no-repeat, repeat",
color: (listener: Listener) => themeColor(listener, "shift-9"),
animation: `${pillAnimationName} ${durationSeconds}s linear infinite`,
[`@keyframes ${pillAnimationName}`]: pillKeyframes,
...(props.style ?? {}),
} as StyleObject,
};
}
export { animatedGradientText };