# Outline draw

> The outline draws itself, then fills.

- Type: Component (draw)
- Page: https://textlab.dev/components/outline-draw
- Markdown: https://textlab.dev/components/outline-draw.md
- Registry item: https://textlab.dev/r/outline-draw.json
- Source file: `registry/textlab/outline-draw/outline-draw.tsx`
- Dependencies: motion
- Variants: draw-fill, draw-only, fill-sweep
- Keywords: outline, stroke, draw, svg text, line drawing, dashoffset

## When to use

- Big, single-line display words: names, product marks, numbers.
- Architectural and blueprint aesthetics.

## When not to use

- Text that wraps; the SVG draws a single line.
- Small sizes where the outline is thinner than a pixel.

## Installation

### CLI

```bash
npx shadcn@latest add @textlab/outline-draw
pnpm dlx shadcn@latest add @textlab/outline-draw
```

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/outline-draw.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 { OutlineDraw } from "@/components/textlab/outline-draw/outline-draw";

export function Example() {
  return <OutlineDraw as="h1" text="Draft" />;
}
```

## API reference

### OutlineDraw

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` (required) | `string` | – | The copy. |
| `variant` | `"draw-fill" \| "draw-only" \| "fill-sweep"` | `"draw-fill"` | Trace then flood, keep the outline, or trace then fill left to right. |
| `stroke` | `number` | `0.018` | Outline width in em. |
| `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. |
| `duration` | `number` | `1.6` | Seconds each part takes. |
| `ease` | `EaseName \| [number, number, number, number]` | `"standard"` | Easing token or cubic bezier. |
| `reducedMotion` | `"user" \| "always" \| "never"` | `"user"` | user follows the OS setting; always shows the still version. |
| `className` | `string` | – | Merged onto the root. |
| `onComplete` | `() => void` | – | Called when a one-shot animation finishes. |

## Accessibility

- The SVG is decorative; the word is read from a hidden copy.
- An invisible copy holds the exact size so nothing shifts.

## Motion

- All contours trace at once and finish together.
- The fill starts at 75% of the trace so the two overlap.

## Reduced motion

Shows the final state: filled, or outlined for draw-only.

## Responsive behavior

- Keep it on one line; size it with a fluid font size.

## Performance

- One SVG text element; stroke-dashoffset and fill-opacity only.

## 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: draw-fill, draw-only, fill-sweep.

## Related

- [Signature](https://textlab.dev/components/signature.md): A handwritten signature writes itself.
- [Wipe reveal](https://textlab.dev/components/wipe-reveal.md): A soft edge uncovers the line.
- [Ink bleed](https://textlab.dev/components/ink-bleed.md): Text soaks in like ink on paper.

## Also in draw

- [Marker highlight](https://textlab.dev/components/marker-highlight.md): A highlighter swipes behind the words.
- [Underline draw](https://textlab.dev/components/underline-draw.md): A hand-drawn underline sketches in.
- [Strike-through](https://textlab.dev/components/strike-through.md): Cross out a word and write the new one.
- [Circle annotate](https://textlab.dev/components/circle-annotate.md): A pen circles the word that matters.

## Source

### components/textlab/outline-draw/outline-draw.tsx

```tsx
"use client";

import type { CSSProperties, ElementType } from "react";
import { motion } from "motion/react";
import { srOnly } from "../lib/a11y";
import { resolveEase, type EaseInput } from "../lib/motion-tokens";
import { useReduced, type ReducedMotion } from "../lib/use-reduced";
import { useSvgId } from "../lib/use-svg-id";
import { useTrigger, type Trigger } from "../lib/use-trigger";

export type OutlineDrawVariant = "draw-fill" | "draw-only" | "fill-sweep";

export interface OutlineDrawProps {
  text: string;
  /** draw-fill traces then floods with ink, draw-only leaves the outline, fill-sweep traces then fills left to right. */
  variant?: OutlineDrawVariant;
  /** Outline width in em. */
  stroke?: number;
  as?: ElementType;
  trigger?: Exclude<Trigger, "always">;
  active?: boolean;
  once?: boolean;
  delay?: number;
  /** Seconds the trace takes. The fill follows. */
  duration?: number;
  ease?: EaseInput;
  reducedMotion?: ReducedMotion;
  className?: string;
  style?: CSSProperties;
  onComplete?: () => void;
}

/**
 * The outline draws itself, then fills. Every glyph contour is traced at once with a dash that
 * runs out, so even long words finish together.
 */
export function OutlineDraw({ text, variant = "draw-fill", stroke = 0.018, as: Tag = "span", trigger = "inView", active, once = true, delay = 0, duration = 1.6, ease = "standard", reducedMotion = "user", className, style, onComplete }: OutlineDrawProps) {
  const id = useSvgId("tl-outline");
  const { ref, on, bind } = useTrigger<HTMLElement>({ trigger, active, once });
  const reduced = useReduced(reducedMotion);
  const curve = resolveEase(ease);

  if (reduced) {
    return (
      <Tag ref={ref} className={className} style={style} {...bind}>
        {variant === "draw-only" ? <span style={{ WebkitTextFillColor: "transparent", WebkitTextStroke: `${stroke}em currentColor` }}>{text}</span> : text}
      </Tag>
    );
  }

  const fillAt = delay + duration * 0.75;
  return (
    <Tag ref={ref} className={className} style={{ position: "relative", display: "inline-block", ...style }} {...bind}>
      <span style={srOnly}>{text}</span>
      <span aria-hidden="true" style={{ visibility: "hidden", whiteSpace: "pre" }}>
        {text}
      </span>
      <svg aria-hidden="true" width="100%" height="100%" style={{ position: "absolute", inset: 0, overflow: "visible" }}>
        {variant === "fill-sweep" && (
          <defs>
            <linearGradient id={`${id}-sweep`} x1="0" x2="1" y1="0" y2="0">
              <motion.stop offset={0} stopColor="white" initial={{ offset: 0 }} animate={{ offset: on ? 1 : 0 }} transition={{ duration: duration * 0.7, delay: fillAt, ease: curve }} />
              <motion.stop offset={0} stopColor="black" initial={{ offset: 0 }} animate={{ offset: on ? 1 : 0 }} transition={{ duration: duration * 0.7, delay: fillAt, ease: curve }} />
            </linearGradient>
            <mask id={`${id}-mask`} maskContentUnits="objectBoundingBox">
              <rect width="1" height="1" fill={`url(#${id}-sweep)`} />
            </mask>
          </defs>
        )}
        <motion.text
          x="50%"
          y="54%"
          textAnchor="middle"
          dominantBaseline="middle"
          fill="currentColor"
          stroke="currentColor"
          strokeLinejoin="round"
          style={{ fontSize: "1em", strokeWidth: `${stroke}em`, strokeDasharray: "6em 6em" }}
          initial={{ strokeDashoffset: "6em", fillOpacity: 0 }}
          animate={on ? { strokeDashoffset: "0em", fillOpacity: variant === "draw-fill" ? 1 : 0 } : { strokeDashoffset: "6em", fillOpacity: 0 }}
          transition={on ? { strokeDashoffset: { duration, delay, ease: curve }, fillOpacity: { duration: duration * 0.5, delay: fillAt } } : { duration: 0.2 }}
          onAnimationComplete={on ? onComplete : undefined}
        >
          {text}
        </motion.text>
        {variant === "fill-sweep" && (
          <text x="50%" y="54%" textAnchor="middle" dominantBaseline="middle" fill="currentColor" mask={`url(#${id}-mask)`} style={{ fontSize: "1em" }}>
            {text}
          </text>
        )}
      </svg>
    </Tag>
  );
}

export default OutlineDraw;
```

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