# Ember text

> Letters land hot and cool down to the text color.

- Type: Component (light)
- Page: https://textlab.dev/components/ember-text
- Markdown: https://textlab.dev/components/ember-text.md
- Registry item: https://textlab.dev/r/ember-text.json
- Source file: `registry/textlab/ember-text/ember-text.tsx`
- Dependencies: motion
- Variants: ember, steel, ice
- Keywords: ember, fire, hot metal, forge, glow, cooling

## When to use

- Titles for craft, hardware and energy brands.
- Dark sections where warmth should arrive with the words.

## When not to use

- Light backgrounds with the ice variant; the frost disappears.
- Paragraphs.

## Installation

### CLI

```bash
npx shadcn@latest add @textlab/ember-text
pnpm dlx shadcn@latest add @textlab/ember-text
```

The `@textlab` name needs `"registries": { "@textlab": "https://textlab.dev/r/{name}.json" }` in `components.json`. Without it, use the full URL:

```bash
npx shadcn@latest add https://textlab.dev/r/ember-text.json
```

Import the tokens once in your root layout: `import "@/components/textlab/tokens.css";`

### Manual

1. Install the dependencies: `npm install motion@12`
2. Copy `text-fx` (https://textlab.dev/r/text-fx.json) and this component into `components/textlab/`.

## Usage

```tsx
import { EmberText } from "@/components/textlab/ember-text/ember-text";

export function Example() {
  return <EmberText as="h2" text="Forged in fire" />;
}
```

## API reference

### EmberText

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` (required) | `string` | – | The copy. |
| `variant` | `"ember" \| "steel" \| "ice"` | `"ember"` | White-hot to red, blue-white steel, or cyan frost. |
| `as` | `ElementType` | `"span"` | Rendered element. |
| `trigger` | `"mount" \| "inView" \| "hover" \| "manual"` | `"inView"` | What starts the animation. manual follows `active`. |
| `active` | `boolean` | `false` | Drives trigger="manual". |
| `once` | `boolean` | `true` | In view: play once instead of on every entry. |
| `delay` | `number` | `0` | Seconds before the first part moves. |
| `stagger` | `number` | `0.045` | Seconds between parts. |
| `duration` | `number` | `1.6` | Seconds a letter takes to cool. |
| `reducedMotion` | `"user" \| "always" \| "never"` | `"user"` | user follows the OS setting; always shows the still version. |
| `className` | `string` | – | Merged onto the root. |

## Accessibility

- The glow is a decorative copy; the title is read once.
- The settled color is the inherited text color, so contrast is unchanged.

## Motion

- Each letter settles a short drop with a long-tailed ease-out.
- Heat fades through four stops over 1.6 s, brightest right as it lands.

## Reduced motion

The title appears in its normal color.

## Performance

- Two motion spans per letter; nothing runs after the last one cools.

## Notes for AI

- Use only the props documented above. Do not invent props.
- Keep the screen-reader text and the reduced-motion branch when editing the source.
- Pick a variant from: ember, steel, ice.

## Related

- [Neon sign](https://textlab.dev/components/neon-text.md): A neon sign that hums and flickers on.
- [Matrix decode](https://textlab.dev/components/matrix-decode.md): Glyph rain resolves into the word.
- [Character reveal](https://textlab.dev/components/char-reveal.md): Letters arrive one at a time.

## Also in light

- [Gradient text](https://textlab.dev/components/gradient-text.md): Color flows slowly through the letters.
- [Shimmer text](https://textlab.dev/components/shimmer-text.md): A calm light passes across the words.
- [Aurora text](https://textlab.dev/components/aurora-text.md): Northern lights drift inside the letters.
- [Holographic text](https://textlab.dev/components/holo-text.md): Foil that shifts color as you move across it.

## Source

### components/textlab/ember-text/ember-text.tsx

```tsx
"use client";

import type { CSSProperties, ElementType } from "react";
import { motion } from "motion/react";
import { SrText } from "../lib/a11y";
import { splitWords } from "../lib/split";
import { useHydrated } from "../lib/use-hydrated";
import { useReduced, type ReducedMotion } from "../lib/use-reduced";
import { useTrigger, type Trigger } from "../lib/use-trigger";

export type EmberTextVariant = "ember" | "steel" | "ice";

export interface EmberTextProps {
  text: string;
  /** ember glows white-hot to red, steel cools from blue-white, ice frosts from cyan. */
  variant?: EmberTextVariant;
  as?: ElementType;
  trigger?: Exclude<Trigger, "always">;
  active?: boolean;
  once?: boolean;
  delay?: number;
  /** Seconds between letters. */
  stagger?: number;
  /** Seconds a letter takes to cool. */
  duration?: number;
  reducedMotion?: ReducedMotion;
  className?: string;
  style?: CSSProperties;
}

const HEAT: Record<EmberTextVariant, string[]> = {
  ember: ["oklch(0.99 0.04 95)", "oklch(0.88 0.17 85)", "oklch(0.72 0.2 50)", "oklch(0.55 0.2 30)"],
  steel: ["oklch(0.99 0.01 250)", "oklch(0.88 0.06 240)", "oklch(0.7 0.1 250)", "oklch(0.5 0.08 260)"],
  ice: ["oklch(0.99 0.02 200)", "oklch(0.9 0.08 200)", "oklch(0.8 0.12 210)", "oklch(0.65 0.12 220)"],
};

/**
 * Letters land white-hot and cool down to the text color, one after another, like metal leaving
 * the forge. A glowing copy cools through the palette on top of each settled letter.
 */
export function EmberText({ text, variant = "ember", as: Tag = "span", trigger = "inView", active, once = true, delay = 0, stagger = 0.045, duration = 1.6, reducedMotion = "user", className, style }: EmberTextProps) {
  const { ref, on, bind } = useTrigger<HTMLElement>({ trigger, active, once });
  const hydrated = useHydrated();
  const reduced = useReduced(reducedMotion);
  const words = splitWords(text);
  const heat = HEAT[variant];

  if (reduced) {
    return (
      <Tag ref={ref} className={className} style={style} {...bind}>
        {text}
      </Tag>
    );
  }

  return (
    <Tag ref={ref} className={className} style={style} data-tl-motion={reducedMotion === "user" ? "user" : undefined} data-tl-pending={hydrated ? undefined : ""} {...bind}>
      <SrText>{text}</SrText>
      <span aria-hidden="true">
        {words.map((word, w) => (
          <span key={w}>
            <span style={{ display: "inline-block", whiteSpace: "nowrap" }}>
              {word.chars.map((char, c) => {
                const at = delay + (word.charOffset + c) * stagger;
                return (
                  <span key={c} style={{ position: "relative", display: "inline-block" }}>
                    <motion.span
                      data-tl-part=""
                      style={{ display: "inline-block" }}
                      initial={{ opacity: 0, transform: "translateY(0.15em)" }}
                      animate={on ? { opacity: 1, transform: "translateY(0em)" } : { opacity: 0, transform: "translateY(0.15em)" }}
                      transition={on ? { opacity: { duration: 0.2, delay: at }, transform: { duration: 0.6, delay: at, ease: [0.16, 1, 0.3, 1] } } : { duration: 0.15 }}
                    >
                      {char}
                    </motion.span>
                    <motion.span
                      style={{ position: "absolute", inset: 0, pointerEvents: "none" }}
                      initial={{ opacity: 0, color: heat[0] }}
                      animate={
                        on
                          ? {
                              opacity: [0, 1, 1, 0],
                              color: heat,
                              textShadow: [`0 0 0.5em ${heat[1]}`, `0 0 0.35em ${heat[2]}`, `0 0 0.2em ${heat[3]}`, `0 0 0em ${heat[3]}`],
                            }
                          : { opacity: 0 }
                      }
                      transition={on ? { duration, delay: at, ease: "easeOut", times: [0, 0.08, 0.6, 1] } : { duration: 0 }}
                    >
                      {char}
                    </motion.span>
                  </span>
                );
              })}
            </span>
            {w < words.length - 1 ? " " : null}
          </span>
        ))}
      </span>
    </Tag>
  );
}

export default EmberText;
```

Full library index: https://textlab.dev/llms.txt
