---
title: Trace
description: Border or icon path that draws itself.
group: Path
order: 31
new: true
---

Stroke paths draw on in a quiet sweep. Use it on an SVG mark, or wrap a surface so the outline traces itself.

## Icon

```tsx
import { useState } from "react"
import { Trace } from "@/components/motion/trace"
import { Button } from "@/components/ui/button"

export function Example() {
  const [play, setPlay] = useState(0)

  return (
    <>
      <Trace play={play} duration={1200}>
        <svg viewBox="0 0 64 64" className="size-20" fill="none">
          <path d="M12 44V20c0-2.2 1.8-4 4-4h16c6.6 0 12 5.4 12 12s-5.4 12-12 12H20" />
          <path d="M20 28h12" />
          <circle cx="44" cy="44" r="6" />
        </svg>
      </Trace>
      <Button onClick={() => setPlay((n) => n + 1)}>Trace</Button>
    </>
  )
}
```

## Border

```tsx
import { useState } from "react"
import { Trace } from "@/components/motion/trace"
import { Button } from "@/components/ui/button"

export function Example() {
  const [play, setPlay] = useState(0)

  return (
    <>
      <Trace play={play} duration={1200}>
        <svg viewBox="0 0 64 64" className="size-20" fill="none">
          <path d="M12 44V20c0-2.2 1.8-4 4-4h16c6.6 0 12 5.4 12 12s-5.4 12-12 12H20" />
          <path d="M20 28h12" />
          <circle cx="44" cy="44" r="6" />
        </svg>
      </Trace>
      <Button onClick={() => setPlay((n) => n + 1)}>Trace</Button>
    </>
  )
}
```

## Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add trace
```

## Usage

```tsx
import { useState } from "react"
import { Trace, TraceBorder } from "@/components/motion/trace"
import { Button } from "@/components/ui/button"
```

```tsx
const [play, setPlay] = useState(0)

<>
  <Trace play={play} duration={1200}>
    <svg viewBox="0 0 64 64" className="size-20" fill="none">
      <path d="M12 44V20c0-2.2 1.8-4 4-4h16c6.6 0 12 5.4 12 12s-5.4 12-12 12H20" />
      <path d="M20 28h12" />
      <circle cx="44" cy="44" r="6" />
    </svg>
  </Trace>
  <Button onClick={() => setPlay((n) => n + 1)}>Trace</Button>
</>
```

```tsx
<TraceBorder play={play} radius={12} className="w-full max-w-sm">
  <div className="px-5 py-5">Token ready</div>
</TraceBorder>
```

## API Reference

### Trace

| Prop | Type | Default |
| --- | --- | --- |
| duration | number | 1200 |
| delay | number | 0 |
| play | number | 0 |
| strokeWidth | number | 2 |
| color | string | currentColor |
| className | string | — |
| children | ReactNode | — |

Wrap an SVG. Trace animates `path`, `circle`, `ellipse`, `line`, `polyline`, `polygon`, and `rect` nodes. Duration and delay are in milliseconds.

### TraceBorder

| Prop | Type | Default |
| --- | --- | --- |
| duration | number | 1200 |
| delay | number | 0 |
| play | number | 0 |
| strokeWidth | number | 1.5 |
| color | string | currentColor |
| radius | number | 12 |
| className | string | — |
| children | ReactNode | — |

Draws a rounded outline around the wrapped surface. Bump `play` to replay.

## Source

```tsx
"use client";

import "@/lib/motion-styles";

import {
  useEffect,
  useRef,
  useState,
  type ComponentProps,
  type CSSProperties,
  type ReactNode,
} from "react";
import { cn } from "@/lib/utils";

export type TraceProps = Omit<ComponentProps<"div">, "children"> & {
  /** Full draw length in ms. */
  duration?: number;
  /** Delay before the draw starts in ms. */
  delay?: number;
  /** Bump to replay the draw. */
  play?: number;
  /** Stroke width in px. */
  strokeWidth?: number;
  /** Stroke color. */
  color?: string;
  children?: ReactNode;
};

export type TraceBorderProps = Omit<ComponentProps<"div">, "children"> & {
  duration?: number;
  delay?: number;
  play?: number;
  strokeWidth?: number;
  color?: string;
  /** Corner radius in px. */
  radius?: number;
  children?: ReactNode;
};

const TRACEABLE = "path, circle, ellipse, line, polyline, polygon, rect";

function prepareNode(
  node: Element,
  color: string,
  strokeWidth: number,
  reduced: boolean,
) {
  const el = node as SVGGeometryElement & HTMLElement;
  let total = 0;
  try {
    total = el.getTotalLength();
  } catch {
    total = 0;
  }
  if (!total || !Number.isFinite(total)) return null;

  el.style.setProperty("--trace-length", String(total));
  el.style.stroke = color;
  el.style.strokeWidth = String(strokeWidth);
  el.style.fill = "none";
  el.style.strokeLinecap = "round";
  el.style.strokeLinejoin = "round";
  el.style.strokeDasharray = String(total);
  el.style.strokeDashoffset = reduced ? "0" : String(total);
  el.style.animation = "none";
  return el;
}

function Trace({
  className,
  duration = 1200,
  delay = 0,
  play = 0,
  strokeWidth = 2,
  color = "currentColor",
  children,
  style,
  ...props
}: TraceProps) {
  const rootRef = useRef<HTMLDivElement>(null);
  const length = Math.max(duration, 120);
  const wait = Math.max(delay, 0);

  useEffect(() => {
    const root = rootRef.current;
    if (!root) return;
    const svg = root.querySelector("svg");
    if (!svg) return;

    const nodes = Array.from(svg.querySelectorAll(TRACEABLE));
    const reduced =
      typeof window !== "undefined" &&
      window.matchMedia("(prefers-reduced-motion: reduce)").matches;

    const prepared = nodes
      .map((node) => prepareNode(node, color, strokeWidth, reduced))
      .filter(Boolean) as HTMLElement[];

    if (reduced) return;

    const id = window.requestAnimationFrame(() => {
      for (const el of prepared) {
        el.style.animation = `arctis-trace-draw ${length}ms cubic-bezier(0.4, 0, 0.2, 1) ${wait}ms both`;
      }
    });

    return () => window.cancelAnimationFrame(id);
  }, [play, color, strokeWidth, length, wait]);

  return (
    <div
      ref={rootRef}
      data-slot="trace"
      className={cn("arctis-trace inline-flex text-foreground", className)}
      style={style as CSSProperties}
      {...props}
    >
      {children}
    </div>
  );
}

function roundedRectPath(width: number, height: number, radius: number) {
  const w = Math.max(width, 0);
  const h = Math.max(height, 0);
  const r = Math.min(Math.max(radius, 0), w / 2, h / 2);
  if (r <= 0) {
    return `M 0 0 H ${w} V ${h} H 0 Z`;
  }
  return [
    `M ${r} 0`,
    `H ${w - r}`,
    `A ${r} ${r} 0 0 1 ${w} ${r}`,
    `V ${h - r}`,
    `A ${r} ${r} 0 0 1 ${w - r} ${h}`,
    `H ${r}`,
    `A ${r} ${r} 0 0 1 0 ${h - r}`,
    `V ${r}`,
    `A ${r} ${r} 0 0 1 ${r} 0`,
    "Z",
  ].join(" ");
}

function TraceBorder({
  className,
  duration = 1200,
  delay = 0,
  play = 0,
  strokeWidth = 1.5,
  color = "currentColor",
  radius = 12,
  children,
  style,
  ...props
}: TraceBorderProps) {
  const rootRef = useRef<HTMLDivElement>(null);
  const pathRef = useRef<SVGPathElement>(null);
  const [size, setSize] = useState({ w: 0, h: 0 });
  const length = Math.max(duration, 120);
  const wait = Math.max(delay, 0);
  const pad = strokeWidth;
  const drawW = Math.max(size.w - strokeWidth, 0);
  const drawH = Math.max(size.h - strokeWidth, 0);
  const pathD = roundedRectPath(drawW, drawH, Math.max(radius - strokeWidth / 2, 0));

  useEffect(() => {
    const root = rootRef.current;
    if (!root) return;
    const measure = () => {
      setSize({
        w: Math.max(root.clientWidth, 1),
        h: Math.max(root.clientHeight, 1),
      });
    };
    measure();
    const observer = new ResizeObserver(measure);
    observer.observe(root);
    return () => observer.disconnect();
  }, []);

  useEffect(() => {
    const node = pathRef.current;
    if (!node || size.w === 0 || size.h === 0) return;

    const reduced =
      typeof window !== "undefined" &&
      window.matchMedia("(prefers-reduced-motion: reduce)").matches;

    const prepared = prepareNode(node, color, strokeWidth, reduced);
    if (!prepared || reduced) return;

    // Force a reflow so replay always restarts from a hidden stroke.
    void prepared.getBoundingClientRect();
    const id = window.requestAnimationFrame(() => {
      prepared.style.animation = `arctis-trace-draw ${length}ms cubic-bezier(0.4, 0, 0.2, 1) ${wait}ms both`;
    });
    return () => window.cancelAnimationFrame(id);
  }, [play, color, strokeWidth, size.w, size.h, length, wait, radius, pathD]);

  return (
    <div
      ref={rootRef}
      data-slot="trace-border"
      className={cn(
        "arctis-trace-border relative overflow-visible text-foreground",
        className,
      )}
      style={style as CSSProperties}
      {...props}
    >
      {children}
      <svg
        className="arctis-trace-border-svg pointer-events-none absolute overflow-visible"
        width={size.w + pad}
        height={size.h + pad}
        viewBox={`0 0 ${size.w + pad} ${size.h + pad}`}
        style={{
          left: -pad / 2,
          top: -pad / 2,
        }}
        aria-hidden="true"
      >
        <path
          ref={pathRef}
          d={pathD}
          transform={`translate(${pad / 2} ${pad / 2})`}
          fill="none"
        />
      </svg>
    </div>
  );
}

export { Trace, TraceBorder };
```
