auroraText
A Text block/component from Magic UI — clean-room reimplemented for Domphy (see methodology). Call auroraText() with no arguments for a working demo, or edit the code below live.
Implementation notes
Diagonal multi-color background-clip: text gradient that pans back and forth via CSS animation-direction: alternate (8s / speed multiplier, ease-in-out, infinite) -- matches the spec's own description of an alternating, easing ping-pong loop almost exactly, and is simpler/more correct than a hand-authored 3-stop keyframe. First color is repeated at the gradient's end so the pan never shows a seam, per the spec. Default 4-color pink/purple/blue/cyan palette is mapped to theme color-family roles (secondary, highlight, primary, info) instead of literal hex, since Domphy's doctor rules forbid raw hex/rgb colors on style props -- same documented tradeoff this package's existing animatedGradientText block already makes for its own default colors. Accessibility uses the exact sr-only-text + aria-hidden-decorative-gradient-copy structure the spec's own DOM sketch describes, reusing the same pattern already established elsewhere in this package (sidebarInDialog).
Status: ported · Reference: Magic UI original
// magicui "Aurora Text" — clean-room reimplementation from the public
// behavior/visual spec only (no upstream source viewed or copied). Display
// text filled with a diagonal multi-color gradient that pans back and forth
// across the glyphs on an endless ease-in-out loop — the same
// `background-clip: text` panning technique this package's
// `animatedGradientText`/`animatedShinyText` use, but alternating direction
// each cycle (CSS `animation-direction: alternate`) instead of resetting
// abruptly, and repeating the first color at the gradient's end so the pan
// never shows a visible seam.
//
// The upstream spec's default four-color palette is literal hex
// (pink/magenta, purple, blue, cyan) — Domphy's doctor rules forbid raw
// hex/rgb colors on style props, so the palette is exposed as `ThemeColor`
// roles instead, defaulting to `["secondary", "highlight", "primary", "info"]`
// (this theme's closest four distinct built-in families to that hue spread).
// Same tradeoff `animatedGradientText` documents for its own default colors.
//
// Accessibility: the visible gradient-filled copy is `aria-hidden`, paired
// with a visually-hidden duplicate carrying the plain text — the same
// sr-only-text + aria-hidden-decoration pattern used by this package's
// `sidebarInDialog` block, generalized here to a public component per the
// spec's own DOM sketch.
import type { DomphyElement, Listener, StyleObject } from "@domphy/core";
import { hashString } from "@domphy/core";
import { type ThemeColor, themeColor } from "@domphy/theme";
export type AuroraTextTag = "span" | "div" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
export interface AuroraTextProps {
/** Text content. Defaults to `"Aurora Text"`. */
children?: string;
/** Gradient color families the sweep cycles through. Defaults to four theme
* roles standing in for magic UI's pink/purple/blue/cyan palette. */
colors?: ThemeColor[];
/** Speed multiplier — `2` sweeps twice as fast, `0.5` half as fast. Defaults to `1`. */
speed?: number;
/** Wrapping element tag. Defaults to `"span"`. */
as?: AuroraTextTag;
style?: StyleObject;
}
const SR_ONLY_STYLE = {
position: "absolute",
width: "1px",
height: "1px",
padding: "0",
margin: "-1px",
overflow: "hidden",
clip: "rect(0, 0, 0, 0)",
whiteSpace: "nowrap",
border: "0",
} as const;
let auroraTextInstanceCounter = 0;
/**
* Display text filled with a continuously panning multicolor gradient
* (an "aurora" sweep), alternating direction on an endless ease-in-out loop.
* Call with no arguments for a working demo.
*/
function auroraText(props: AuroraTextProps = {}): DomphyElement {
const text = props.children ?? "Aurora Text";
const colors = props.colors ?? (["secondary", "highlight", "primary", "info"] as ThemeColor[]);
const speed = Math.max(props.speed ?? 1, 0.01);
const wrapperTag = props.as ?? "span";
const instanceId = ++auroraTextInstanceCounter;
const durationSeconds = Math.max(0.5, 8 / speed);
const gradientStops = (listener: Listener): string => {
const roles = colors.length > 0 ? colors : (["primary"] as ThemeColor[]);
// Repeat the first stop at the end so the alternating pan never shows a seam.
const stops = [...roles, roles[0]];
return stops.map((role) => themeColor(listener, "shift-8", role)).join(", ");
};
const animationName = `aurora-text-sweep-${hashString(JSON.stringify({ instanceId, colors }))}`;
const keyframes = {
"0%": { backgroundPosition: "0% 50%" },
"100%": { backgroundPosition: "100% 50%" },
};
const auroraSpan: DomphyElement<"span"> = {
span: text,
ariaHidden: "true",
_key: "aurora-fill",
style: {
backgroundImage: (listener: Listener) => `linear-gradient(135deg, ${gradientStops(listener)})`,
backgroundSize: "200% 200%",
backgroundClip: "text",
WebkitBackgroundClip: "text",
color: "transparent",
WebkitTextFillColor: "transparent",
animation: `${animationName} ${durationSeconds}s ease-in-out infinite alternate`,
[`@keyframes ${animationName}`]: keyframes,
} as StyleObject,
};
const outer = {
[wrapperTag]: [{ span: text, _key: "sr-only-text", style: SR_ONLY_STYLE }, auroraSpan],
style: {
position: "relative",
display: "inline-block",
...(props.style ?? {}),
} as StyleObject,
} as unknown as DomphyElement;
return outer;
}
export { auroraText };