# Signature

> A handwritten signature writes itself.

- Type: Component (draw)
- Page: https://textlab.dev/components/signature
- Markdown: https://textlab.dev/components/signature.md
- Registry item: https://textlab.dev/r/signature.json
- Source file: `registry/textlab/signature/signature.tsx`
- Dependencies: motion
- Variants: pen, brush
- Keywords: signature, handwriting, script, autograph, write on, pen

## When to use

- Founder letters, about pages and certificates.
- Sign-offs at the end of a story.

## When not to use

- Without a script font loaded; system cursive fonts vary a lot.
- Anything longer than a name.

## Installation

### CLI

```bash
npx shadcn@latest add @textlab/signature
pnpm dlx shadcn@latest add @textlab/signature
```

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/signature.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 { Signature } from "@/components/textlab/signature/signature";

export function Example() {
  return <Signature text="Ada Lovelace" />;
}
```

## API reference

### Signature

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` (required) | `string` | – | The name to sign. |
| `variant` | `"pen" \| "brush"` | `"pen"` | A fine nib, or a fuller brush where ink pools sooner. |
| `font` | `string` | `var(--tl-font-script)` | Script font family. Load one with next/font, for example Mrs Saint Delafield. |
| `color` | `string` | `currentColor` | Ink color. |
| `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` | `2.2` | Seconds each part takes. |
| `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 name is read once from a hidden copy.
- Script fonts are hard to read; keep the plain name nearby if it matters.

## Motion

- Writes left to right on a gentle in-out curve, like a hand gaining then losing speed.
- Ink fills in behind the nib, never ahead of it.

## Reduced motion

Shows the signature in full.

## Performance

- One SVG text and one animated mask rectangle.

## 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: pen, brush.

## Related

- [Outline draw](https://textlab.dev/components/outline-draw.md): The outline draws itself, then fills.
- [Ink bleed](https://textlab.dev/components/ink-bleed.md): Text soaks in like ink on paper.
- [Typewriter](https://textlab.dev/components/typewriter.md): Types, deletes and retypes with a caret.

## 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/signature/signature.tsx

```tsx
"use client";

import type { CSSProperties, ElementType } from "react";
import { motion } from "motion/react";
import { srOnly } from "../lib/a11y";
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 SignatureVariant = "pen" | "brush";

export interface SignatureProps {
  /** The name to sign. */
  text: string;
  /** pen is a fine nib that fills in behind itself, brush is a fuller stroke with ink that pools. */
  variant?: SignatureVariant;
  /** Script font. Defaults to the --tl-font-script token. */
  font?: string;
  /** Pen color. */
  color?: string;
  as?: ElementType;
  trigger?: Exclude<Trigger, "always">;
  active?: boolean;
  once?: boolean;
  delay?: number;
  /** Seconds to sign the whole name. */
  duration?: number;
  reducedMotion?: ReducedMotion;
  className?: string;
  style?: CSSProperties;
  onComplete?: () => void;
}

/**
 * A handwritten signature writes itself, left to right. The nib traces the letters while a soft edge
 * moves along with it, so ink only appears where the pen has already been.
 */
export function Signature({ text, variant = "pen", font = "var(--tl-font-script, 'Snell Roundhand', 'Brush Script MT', cursive)", color = "currentColor", as: Tag = "span", trigger = "inView", active, once = true, delay = 0, duration = 2.2, reducedMotion = "user", className, style, onComplete }: SignatureProps) {
  const id = useSvgId("tl-sign");
  const { ref, on, bind } = useTrigger<HTMLElement>({ trigger, active, once });
  const reduced = useReduced(reducedMotion);
  const fontStyle: CSSProperties = { fontFamily: font, ...style };

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

  const brush = variant === "brush";
  const writing = { duration, delay, ease: [0.45, 0.05, 0.35, 1] as [number, number, number, number] };
  return (
    <Tag ref={ref} className={className} style={{ position: "relative", display: "inline-block", ...fontStyle }} {...bind}>
      <span style={srOnly}>{text}</span>
      <span aria-hidden="true" style={{ visibility: "hidden", whiteSpace: "pre", padding: "0 0.2em" }}>
        {text}
      </span>
      <svg aria-hidden="true" width="100%" height="100%" style={{ position: "absolute", inset: 0, overflow: "visible", color }}>
        <defs>
          <linearGradient id={`${id}-edge`}>
            <stop offset="0.85" stopColor="white" />
            <stop offset="1" stopColor="black" />
          </linearGradient>
          <mask id={`${id}-mask`} maskUnits="userSpaceOnUse" x="-10%" y="-50%" width="120%" height="200%">
            {/* The soft-edged rect slides in from the left; translating it costs no geometry work. */}
            <motion.rect
              x="-10%"
              y="-50%"
              width="125%"
              height="200%"
              fill={`url(#${id}-edge)`}
              style={{ transformBox: "fill-box" }}
              initial={{ transform: "translateX(-100%)" }}
              animate={{ transform: on ? "translateX(0%)" : "translateX(-100%)" }}
              transition={on ? writing : { duration: 0.2 }}
              onAnimationComplete={on ? onComplete : undefined}
            />
          </mask>
        </defs>
        <g mask={`url(#${id}-mask)`}>
          <motion.text
            x="50%"
            y="55%"
            textAnchor="middle"
            dominantBaseline="middle"
            fill="currentColor"
            stroke="currentColor"
            strokeLinecap="round"
            strokeLinejoin="round"
            style={{ fontSize: "1em", strokeWidth: brush ? "0.03em" : "0.012em", strokeDasharray: "5em 5em" }}
            initial={{ strokeDashoffset: "5em", fillOpacity: 0 }}
            animate={on ? { strokeDashoffset: "0em", fillOpacity: 1 } : { strokeDashoffset: "5em", fillOpacity: 0 }}
            transition={on ? { strokeDashoffset: writing, fillOpacity: { duration: duration * 0.6, delay: delay + duration * (brush ? 0.15 : 0.35) } } : { duration: 0.2 }}
          >
            {text}
          </motion.text>
        </g>
      </svg>
    </Tag>
  );
}

export default Signature;
```

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