# hairline > @lucasmarkes/hairline: nineteen isometric line figures that answer the pointer. SVG, no dependencies, ESM only. A function per figure, and a React component per figure. Every figure takes the same four options. ## Install ```sh npm i @lucasmarkes/hairline pnpm add @lucasmarkes/hairline yarn add @lucasmarkes/hairline bun add @lucasmarkes/hairline npx shadcn@latest add https://hairline.lucasmarkes.com/r/hairline.json ``` ## Use React: `import { Riffle, Terrain, Exploded, Phosphor, Slow, Turntable, Keyboard, Elevator, Phone, Laptop, Terminal, Cabinet, Branches, Vault, Lockers, Padlock, Patch, Dish, Router } from "@lucasmarkes/hairline/react"`. Each component renders a `
`, takes the options below and any `
` 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