# Underline draw

> A hand-drawn underline sketches in.

- Type: Component (draw)
- Page: https://textlab.dev/components/underline-draw
- Markdown: https://textlab.dev/components/underline-draw.md
- Registry item: https://textlab.dev/r/underline-draw.json
- Source file: `registry/textlab/underline-draw/underline-draw.tsx`
- Dependencies: motion
- Variants: line, wave, scribble
- Keywords: underline, hand drawn, annotation, squiggle, rough notation, emphasis

## When to use

- Emphasising one word or phrase inside a headline.
- Friendly, human brands.

## When not to use

- Links; people expect a real underline there.
- Several underlines in one paragraph.

## Installation

### CLI

```bash
npx shadcn@latest add @textlab/underline-draw
pnpm dlx shadcn@latest add @textlab/underline-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/underline-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 { UnderlineDraw } from "@/components/textlab/underline-draw/underline-draw";

export function Example() {
  return <h2>Ship <UnderlineDraw>today</UnderlineDraw>, not someday</h2>;
}
```

## API reference

### UnderlineDraw

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` (required) | `ReactNode` | – | The words to underline. |
| `variant` | `"line" \| "wave" \| "scribble"` | `"line"` | A confident curve, a squiggle, or a quick double pass. |
| `color` | `string` | `var(--tl-accent)` | Pen color. |
| `thickness` | `number` | `3` | Stroke width in px, constant at any size. |
| `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` | `0.6 (0.9 scribble)` | 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. |

## Accessibility

- The underline is decorative SVG; the words are real text.
- Emphasis is visual only; add <strong> or <em> if it matters semantically.

## Motion

- Draws with the standard ease so the pen speeds up and settles like a real stroke.

## Reduced motion

The underline is drawn from the start.

## Performance

- One SVG path animated with pathLength.

## 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: line, wave, scribble.

## Related

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

## Also in draw

- [Outline draw](https://textlab.dev/components/outline-draw.md): The outline draws itself, then fills.
- [Signature](https://textlab.dev/components/signature.md): A handwritten signature writes itself.

## Source

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

```tsx
"use client";

import type { CSSProperties, ElementType, ReactNode } from "react";
import { motion } from "motion/react";
import { drawProps } from "../lib/draw";
import { hashString } from "../lib/math";
import type { EaseInput } from "../lib/motion-tokens";
import { roughScribble, roughUnderline, roughWave } from "../lib/rough";
import { useReduced, type ReducedMotion } from "../lib/use-reduced";
import { useTrigger, type Trigger } from "../lib/use-trigger";

export type UnderlineDrawVariant = "line" | "wave" | "scribble";

export interface UnderlineDrawProps {
  /** The words to underline. Can include other inline elements. */
  children: ReactNode;
  /** line is a confident curve, wave is a squiggle, scribble is a quick double pass. */
  variant?: UnderlineDrawVariant;
  /** Pen color. Defaults to the accent token. */
  color?: string;
  /** Stroke width in px. */
  thickness?: number;
  as?: ElementType;
  trigger?: Exclude<Trigger, "always">;
  active?: boolean;
  once?: boolean;
  delay?: number;
  duration?: number;
  ease?: EaseInput;
  reducedMotion?: ReducedMotion;
  className?: string;
  style?: CSSProperties;
}

const PATHS = { line: roughUnderline, wave: roughWave, scribble: roughScribble };

/**
 * A hand-drawn underline sketches in under the words. The stroke keeps its width at any size, and
 * the jitter is seeded from the text so every underline is unique but stable.
 */
export function UnderlineDraw({ children, variant = "line", color = "var(--tl-accent, #3a9a5b)", thickness = 3, as: Tag = "span", trigger = "inView", active, once = true, delay = 0, duration, ease, reducedMotion = "user", className, style }: UnderlineDrawProps) {
  const { ref, on, bind } = useTrigger<HTMLElement>({ trigger, active, once });
  const reduced = useReduced(reducedMotion);
  const seed = hashString(typeof children === "string" ? children : variant);
  const d = PATHS[variant](seed);
  const draw = drawProps(on || reduced, { duration: duration ?? (variant === "scribble" ? 0.9 : 0.6), delay, ease });

  return (
    <Tag ref={ref} className={className} style={{ position: "relative", display: "inline-block", ...style }} {...bind}>
      {children}
      <svg aria-hidden="true" viewBox="0 0 100 20" preserveAspectRatio="none" style={{ position: "absolute", left: "-2%", width: "104%", top: "68%", height: "0.5em", overflow: "visible", pointerEvents: "none" }}>
        <motion.path
          d={d}
          fill="none"
          stroke={color}
          strokeWidth={thickness}
          strokeLinecap="round"
          strokeLinejoin="round"
          vectorEffect="non-scaling-stroke"
          {...(reduced ? { initial: false, animate: { pathLength: 1, opacity: 1 } } : draw)}
        />
      </svg>
    </Tag>
  );
}

export default UnderlineDraw;
```

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