# Terms

> A terms-of-service step with an owl who reads along: Skim sums up each section in one plain line, gets dizzy if you scroll too fast, and gives a thumbs up at the end.

Nobody reads the terms, and every product still has to ask people to accept them. This block reads them with you. Skim, an illustrated owl in round reading glasses, sits on the lit stage with his book open, and his bubble gives the section you’re on in one plain line: “Your files stay yours. We only touch them to run Relay for you.” Scroll into the next section and the line lifts away and the next one rises in. Scroll too fast and his eyes spin behind his glasses (“Whoa. Slow down, I’m reading.”); stay on one section long enough and he yawns. Reach the end and he gives a thumbs up (“That’s everything. Nothing sneaky.”) and Accept turns on. The full text is all there in a scrollable pane with a reading line along its foot; Skip to the end takes you straight down, and onAccept gets the version you agreed to.

## Install

```bash
npx shadcn@latest add https://hairlineui.com/r/terms.json
```

Needs React 19, Tailwind CSS v4 and shadcn/ui theme variables (any style, light and dark). Icons from lucide-react.

- npm: none
- Hairline UI components it needs (the CLI installs them too): `status-button`, `text-morph`
- Docs and live preview: https://hairlineui.com/ui/terms
- Registry item (source): https://hairlineui.com/r/terms.json

## Usage

```tsx
import { Terms } from "@/components/terms";

<Terms
  version="2026-10-21"
  sections={[{ id: "data", title: "Your data", tldr: "Your files stay yours.", body: <p>…</p> }]}
  onAccept={(version) => fetch("/api/terms/accept", { method: "POST", body: JSON.stringify({ version }) })}
/>
```

## Asking again when the terms change

Keep the version each person accepted, and show the block when it is older than the current one.

```tsx
"use client";
import { Terms } from "@/components/terms";
import { SECTIONS, VERSION } from "@/content/terms";

export function TermsGate({ accepted, onDone }: { accepted: string | null; onDone: () => void }) {
  if (accepted === VERSION) return null;
  return (
    <Terms
      sections={SECTIONS}
      version={VERSION}
      updated="Updated 21 October 2026"
      onAccept={async (version) => {
        const res = await fetch("/api/terms/accept", { method: "POST", body: JSON.stringify({ version }) });
        if (!res.ok) throw new Error("Couldn’t save that");
        setTimeout(onDone, 900);
      }}
    />
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `sections` | `TermsSection[]` | – | { id, title, body, tldr }: body is any React content, tldr is Skim’s one line. |
| `version` | `string` | – | Which terms these are; onAccept gets it back. |
| `onAccept` | `(version: string) => Promise<unknown>` | – | Record the acceptance. Throw to say it didn’t save. |
| `onDecline` | `() => void` | – | Shows Not now beside Accept. |
| `requireRead` | `boolean` | `true` | Accept stays off until the terms have been scrolled to the end. |
| `updated / title / name` | `string` | `— / "Terms of Service" / "Skim"` | The line under the title, the title, and the owl’s name in his lines. |

## States

- **reading**: Skim sits bottom left of the lit stage with his book; his bubble reads “Skim’s summary · 1 of 5” over the section’s tldr. The section is the one at the reading line, a third of the way down the pane.
- **next section**: The old line lifts out through a 4px blur (200ms) as the next rises in (360ms); the count moves on.
- **dizzy**: Scrolling faster than 2.4px per ms, or Skip to the end: his eyes become spirals for about a second and he says “Whoa. Slow down, I’m reading.”
- **yawn**: Seven seconds on the same section: he yawns, “Still on your data…”, until you scroll again.
- **read**: The pane reaches its end: thumbs up, “That’s everything. Nothing sneaky.”, Skip to the end goes and Accept lights up.
- **accepted**: Accept → Accepting → Accepted with a drawn check; “Thanks for reading. Well, skimming.” If onAccept throws, the button shakes into Try again.

## What it handles

A terms step is usually a wall of text and a checkbox people tick without reading. This one puts a one-line summary on every section and only turns Accept on once the terms have been scrolled through.

- **Every section, in a line**: Each section carries a tldr that Skim says as you reach it.
- **Read before accepting**: Accept stays off until the pane has been scrolled to the end (requireRead), with Skip to the end for people who know.
- **A reading line**: A hairline along the foot of the pane fills as you scroll, written straight to the element.
- **The version agreed**: onAccept gets the version string, so you can record exactly which terms were accepted.
- **Your own text**: Section bodies are any React content: paragraphs, lists, links.
- **Not now**: onDecline shows a second button beside Accept.

## Replaces

a terms modal, a scroll-to-accept rule, section summaries, a reading progress bar, an accept button with states

## Where it fits

- **Sign-up**: The terms step before the account is created.
- **Updated terms**: The “we’ve changed our terms” screen people see on their next visit.
- **Data processing**: A DPA or AI-use policy a workspace admin has to agree to.
- **Marketplaces**: Seller or creator agreements with sections people actually need to understand.

## Notes

- The terms are a focusable, labelled region you can scroll with the keyboard; the bubble is a polite live region, so each section’s summary is read out as you reach it.
- Skim is decoration (aria-hidden). His four poses are illustrations shipped inside skim-art.ts as WebP data URIs (53KB of images), so the install is complete; the pose changes, the pop and the summary lines are code.
- Scroll handling writes the reading line straight to the element and only re-renders when the section, the pose or the read state changes.
- With reduced motion the poses and lines swap in place and Skip to the end jumps instead of scrolling.
- Built from Status button, which the CLI installs with it. No animation library.

License: MIT (https://hairlineui.com/license#components)
