` attribute, and forwards its ref. The entry is a client module: render it from a Server Component without writing "use client".
```tsx
import { Terrain } from "@lucasmarkes/hairline/react";
export default function Page() {
return
;
}
```
Vanilla: `import { riffle, terrain, exploded, phosphor, slow, turntable, keyboard, elevator, phone, laptop, terminal, cabinet, branches, vault, lockers, padlock, patch, dish, router } from "@lucasmarkes/hairline"`. Each function takes an element and the options, draws into the element, and returns `{ update(options), destroy() }`. In `update`, a key set to `undefined` goes back to its default and a key left out stays as it is.
```ts
import { terrain } from "@lucasmarkes/hairline";
const figure = terrain(document.getElementById("figure")!);
figure.update({ intensity: 0.8 });
figure.destroy();
```
Without a bundler:
```html
```
A figure fills its element's width at a 5:4 aspect ratio.
## Options
- `intensity` (number, default 0.5): How strongly the figure answers the pointer, from 0 (subtle) to 1 (strong). Outside 0…1 is clamped; anything that is not a number is 0.5.
- `theme` ("auto" | "light" | "dark", default "auto"): "auto" follows the page: an ancestor with class dark or data-theme="dark", then the page's color-scheme.
- `label` (string, default a description): The accessible name. In React, aria-label does the same.
- `onRead` ((text: string) => void): The figure's caption, each time it changes. Called once at mount with the rest caption.
## Figures
What a higher `intensity` does to each figure, and the number it sets inside the figure (two straight lines through these three points):
### Riffle
A tray of eight cards. The card under the pointer stands up and its neighbours lean after it. The arrow keys walk the cards.
Higher intensity: The ripple spreads further from the pulled card. (stagger 0 ms at 0, 40 ms at 0.5, 90 ms at 1.)
### Terrain
Eighty-one pillars on a plinth. They rise around the pointer and settle back into a dune with two rises.
Higher intensity: A wider area rises. (radius 1.5 cells at 0, 3 cells at 0.5, 5 cells at 1.)
### Exploded
An app window taken apart into four layers. Moving across opens the gap; moving down picks a layer.
Higher intensity: The layers open further. (gap 12 viewBox units at 0, 28 viewBox units at 0.5, 40 viewBox units at 1.)
### Phosphor
A seven by seven dot matrix playing a loop. Where the pointer paints, the dots fade like phosphor.
Higher intensity: The trail lingers longer. (afterglow 150 ms at 0, 520 ms at 0.5, 1500 ms at 1.)
### Slow
Crates riding a belt through a gate. Hovering slows the clock without stopping it.
Higher intensity: Time slows down more. (rate 0.6× normal speed at 0, 0.2× normal speed at 0.5, 0.05× normal speed at 1.)
### Turntable
Blocks on a turntable. A flick across it spins it, and it settles on the nearest quarter turn.
Higher intensity: The spin coasts longer. (coast 200 ms at 0, 650 ms at 0.5, 1500 ms at 1.)
### Keyboard
Sixty keys in a block. The key under the pointer sinks and its neighbours follow it down, less the further away.
Higher intensity: A wider patch of keys sinks. (radius 1 keys at 0, 2 keys at 0.5, 3.5 keys at 1.)
### Elevator
Four floors with the shaft open and the car inside. The pointer's height picks the floor; the car travels there.
Higher intensity: The car travels faster between floors. (stiffness 40 spring units at 0, 100 spring units at 0.5, 220 spring units at 1.)
### Phone
A phone in layers: glass, board, battery, shell. Moving across opens the gap; moving down picks a layer.
Higher intensity: The layers open further. (gap 16 viewBox units at 0, 28 viewBox units at 0.5, 40 viewBox units at 1.)
### Laptop
A thin laptop, open on its hinge. The pointer's height sets the lid; it follows on a spring.
Higher intensity: The lid opens wider. (lid 100 degrees at 0, 125 degrees at 0.5, 150 degrees at 1.)
### Terminal
A terminal window with its history in rows. The pointer's height scrolls back; the line under it lifts and its neighbours follow.
Higher intensity: The lift spreads further. (spread 1 lines at 0, 2 lines at 0.5, 3.5 lines at 1.)
### Cabinet
A rack of twelve blades, a few half out. The pointer's height pulls the nearest ones out, the farther the less.
Higher intensity: More blades come out. (reach 1.5 blades at 0, 3 blades at 0.5, 5 blades at 1.)
### Branches
A commit graph with a branch forking off main and merging back. The commit under the pointer rises, and its history rises after it.
Higher intensity: More of the history rises. (reach 1 commits at 0, 3 commits at 0.5, 6 commits at 1.)
### Vault
A vault door with a dial and three bolts. The pointer turns the dial; detents catch every ten, and on the combination the bolts draw back.
Higher intensity: The dial coasts longer. (coast 250 ms at 0, 600 ms at 0.5, 1500 ms at 1.)
### Lockers
A bank of twelve lockers, one ajar at rest. The locker under the pointer opens; the one at rest closes.
Higher intensity: The door opens wider. (opening 55 degrees at 0, 90 degrees at 0.5, 120 degrees at 1.)
### Padlock
A padlock with its shackle in. As the pointer comes near the shackle lifts out and swings open.
Higher intensity: The shackle swings further. (swing 45 degrees at 0, 90 degrees at 0.5, 100 degrees at 1.)
### Patch
A patch panel of twenty-four ports with cables. The cable under the pointer lifts and its neighbours lean away.
Higher intensity: The lean spreads further. (radius 1 ports at 0, 2.5 ports at 0.5, 5 ports at 1.)
### Dish
A parabolic dish on a two-axis gimbal. The pointer aims the dish; it follows on a spring.
Higher intensity: The dish swings further. (reach 30 degrees at 0, 50 degrees at 0.5, 70 degrees at 1.)
### Router
A router with its antennas up. Each antenna leans toward the pointer, the nearest most.
Higher intensity: The lean spreads further. (spread 0.5 antennas at 0, 1.5 antennas at 0.5, 3 antennas at 1.)
## Theme
Six CSS custom properties, set on the figure or on any ancestor. Without them a figure is light, or dark when an ancestor has class `dark` or `data-theme="dark"`, or when the page's `color-scheme` is dark.
- `--hairline-plate` (light: #ffffff): The fill of every plate. It hides what is drawn behind, so it must be the colour the figure sits on.
- `--hairline-hi` (light: #232327): The stroke of what is lit: the card pulled, the layer picked, a dot that is on.
- `--hairline-edge` (light: #a4a4ac): Silhouettes, and dots at half strength.
- `--hairline-mid` (light: #c3c3c9): Every other stroke.
- `--hairline-lo` (light: #e0e0e4): What recedes: guides, and dots that are off.
- `--hairline-stroke` (light: 0.9): The stroke width, in CSS pixels at any size.
## Accessibility
A figure is an image with a description you can replace with `label`. Riffle is a focusable group: the arrow keys walk its cards and a live region reads out the card's number. Under prefers-reduced-motion, Phosphor and Slow hold still, and every figure still answers the pointer.
## Make your own
hairline-create is a skill for coding agents. Give it an idea and it draws a new figure to Hairline's ten rules, on the same engine as the nineteen in the docs, as one HTML file.
```sh
npx skills add lucasmarkes/hairline
```
Then type `/hairline-create
` in the agent. Four ideas, each with the page the skill wrote for it, and the change asked for next where there was one:
- `/hairline-create a sales funnel`: https://hairline.lucasmarkes.com/skill/hairline-funnel.html
- `/hairline-create a rate limiter`: https://hairline.lucasmarkes.com/skill/hairline-clearance.html
- `/hairline-create git branches`, then "The rails almost disappear and the trains read as loose blocks. Make it read as a railway at a glance.": https://hairline.lucasmarkes.com/skill/hairline-sidings.html
- `/hairline-create weather over a city`, then "The cloud looks like a stack of cylinders. Make it read as a cloud at a glance.": https://hairline.lucasmarkes.com/skill/hairline-storm.html
More: https://hairline.lucasmarkes.com/skill
## Links
- Site: https://hairline.lucasmarkes.com
- Source: https://github.com/lucasmarkes/hairline
- npm: https://www.npmjs.com/package/@lucasmarkes/hairline
- The essay the figures come from: https://lucasmarkes.com/lab/hairline
- shadcn registry item: https://hairline.lucasmarkes.com/r/hairline.json