Skip to content
lucasmarkesPublic

About

Six isometric line figures that answer the pointer. For React and for anything with a DOM.

Topics

Resources

Contributing

Stars

1.8k stars

Watchers

2 watching

Forks

Repository files navigation

hairline

Thirty-three isometric line figures that answer the pointer. For React and for anything with a DOM.

npm CI license

The six figures: a tray of cards, a field of pillars, a window in layers, a dot matrix, a conveyor belt and a turntable

Live, with a slider for intensity: hairline.lucasmarkes.com

Install

npm i @lucasmarkes/hairline

No dependencies. ESM only. React 18 or later is an optional peer, needed only by @lucasmarkes/hairline/react.

With shadcn, which adds the package and a wrapper that reads your theme's tokens:

npx shadcn@latest add https://hairline.lucasmarkes.com/r/hairline.json

Use

React

import { Terrain } from "@lucasmarkes/hairline/react";

export function Hero() {
  return <Terrain />;
}

The figure fills its parent's width at a 5:4 aspect ratio. The entry is a client module, so a Server Component can render it with no "use client" of its own. Every component takes the options below and every <div> attribute, and forwards its ref to the <div>.

Anything else

import { terrain } from "@lucasmarkes/hairline";

const figure = terrain(document.getElementById("figure")!, { play: true });

figure.update({ intensity: 0.8 });
figure.destroy();

A figure draws into the element you give it, at the element's width and a 5:4 aspect ratio. update changes options on the running figure; destroy removes what the figure added. That is the shape of a Svelte action, so use:terrain={{ intensity }} works as it is.

The figures

Function Component What it is A stronger intensity
riffle Riffle A tray of eight cards. The card under the pointer stands up; the arrow keys walk the cards. The ripple spreads further from the pulled card.
terrain Terrain Eighty-one pillars on a plinth that rise around the pointer. A wider area rises.
exploded Exploded An app window in four layers. Moving across opens the gap; moving down picks a layer. The layers open further.
phosphor Phosphor A dot matrix that plays a loop, and fades like phosphor where the pointer paints it. The trail lingers longer.
slow Slow Crates riding a belt through a gate. Hovering slows the clock without stopping it. Time slows down more.
turntable Turntable Blocks on a turntable. A flick spins it; it settles on the nearest quarter turn. The spin coasts longer.
keyboard Keyboard A sixty-key board. The key under the pointer sinks, and its neighbours follow it down. A wider patch of keys sinks.
elevator Elevator Four floors beside an open shaft. The pointer's height picks a floor; the car travels there. The car travels faster between floors.
phone Phone A phone in layers: glass, board, battery, shell. Moving across opens the gap; moving down picks a layer. The layers open further.
laptop Laptop A thin laptop, open on its hinge. The pointer's height sets the lid; it follows on a spring. The lid opens wider.
terminal Terminal A terminal window with its history in rows. The pointer's height scrolls back; the line under it lifts and its neighbours follow. The lift spreads further.
cabinet Cabinet A rack of twelve blades, a few half out. The pointer's height pulls the nearest ones out, the farther the less. More blades come out.
branches 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. More of the history rises.
vault 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. The dial coasts longer.
lockers Lockers A bank of twelve lockers, one ajar at rest. The locker under the pointer opens; the one at rest closes. The door opens wider.
padlock Padlock A padlock with its shackle in. As the pointer comes near the shackle lifts out and swings open. The shackle swings further.
patch Patch A patch panel of twenty-four ports with cables. The cable under the pointer lifts and its neighbours lean away. The lean spreads further.
dish Dish A parabolic dish on a two-axis gimbal. The pointer aims the dish; it follows on a spring. The dish swings further.
router Router A router with its antennas up. Each antenna leans toward the pointer, the nearest most. The lean spreads further.
loupe Loupe A stand loupe on a blank ruled sheet. The pointer drags it across; the rules pass enlarged under the glass, with nothing between them. The glass magnifies more.
sieve Sieve Three test sieves stacked over a pan. The pointer's height picks one; it rises clear of the stack, and every mesh is bare. The gap opens further.
rail Rail A garment rail with seven bare hangers. The pointer brushes them; each rocks away, the nearest most, and settles. The brush reaches more hangers.
plug Plug A wall socket, and a plug lying on the floor at the end of its cord. The pointer draws the plug up toward the socket; it stops short, and falls back. The plug comes closer to the socket.
query Query A question mark built as a bent bar over a loose ball. The hook turns toward the pointer, and the ball rolls after it. The hook turns further.
drawer Drawer A cabinet of three drawers. The pointer's height picks one; it slides out and shows two dividers with nothing between them. The drawer opens further.
basket Basket A wire basket under a bail handle. It tilts toward the pointer and shows its bare floor; the handle swings after it. The basket tilts further.
plot Plot A bar chart with seven flat tabs where the bars would stand. The pointer brushes them; each lifts a little and drops back to zero. The tabs lift higher.
hub Hub A hub with eight tiles around it on dashed links. The tile under the pointer rises and its link turns solid. The tiles rise higher.
relay Relay A hub with four branches of tiles. The path to the leaf under the pointer lights hop by hop. Each hop waits longer.
settle Settle Twelve tiles lie crooked round a hub. As the pointer nears it, they slide into a tree and the links draw in. The tree forms from further away.
format Format A file of ten crooked lines. The pointer runs the formatter down it, and every line above snaps square. The lines start more crooked.
rebuild Rebuild A tree of packages. The one under the pointer rises, and every package that depends on it rises after it. The packages rise higher.
stack Stack A call stack of five frames. The pointer's height picks one, and the frames above lift away to open it. The frames above lift further.

Options

Every figure takes the same five, all optional:

Option Type Default
intensity number 0.5 How strongly the figure answers the pointer, from 0 (subtle) to 1 (strong). A number outside 0…1 is clamped; anything that is not a number is 0.5.
theme "auto" | "light" | "dark" "auto" "auto" follows the page: an ancestor with class dark or data-theme="dark", then the page's color-scheme.
label string a description in English The accessible name. In React, aria-label does the same.
onRead (text: string) => void The figure's caption, each time it changes: "03", "gap 28.0", "rate 0.20×".
play boolean false Walks the figure through its answer on its own, in a loop, until the pointer or focus arrives; it resumes after they leave.

In update, a key set to undefined goes back to its default, and a key left out stays as it is.

Theme

Six custom properties, set on the figure or on anything above it:

.figures {
  --hairline-plate: #101014; /* the fill of every plate: the colour the figure sits on */
  --hairline-hi: #fafafa;    /* what is lit */
  --hairline-edge: #a1a1aa;  /* silhouettes */
  --hairline-mid: #52525b;   /* every other stroke */
  --hairline-lo: #27272a;    /* what recedes */
  --hairline-stroke: 0.9;    /* stroke width, in CSS pixels at any size */
}

--hairline-plate is the one to get right. Plates are filled, not transparent, because a plate hides what is drawn behind it; on a background that is neither white nor #08090a, set it to that background.

The figure's styles have no specificity, so any rule of yours wins without !important.

Notes

  • Accessibility. A figure is an image with a description you can replace with label. Riffle is the exception: it is a focusable group, the arrow keys walk its cards, and a live region reads the card out.
  • Reduced motion. With prefers-reduced-motion, the figures that play on their own (Phosphor and Slow) hold still, a figure given play rests, and every figure still answers the pointer.
  • Performance. Every figure on a page shares one requestAnimationFrame loop. A figure off screen, or at rest, does no work, and the loop stops when nothing is moving; a figure given play keeps it running while it is on screen.
  • Server rendering. On the server a component is an empty box with a 5:4 aspect ratio, so nothing shifts when it draws. The functions need a DOM: call them in an effect, in onMount, or in a script after the element.
  • Shadow DOM. A figure mounted inside a shadow root styles itself there.

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 thirty-three above, as one HTML file. Every figure it makes declares a tour, the stops a hand would visit, and the page it writes has a play button that walks it.

npx skills add lucasmarkes/hairline

Then type the command with an idea in the agent:

/hairline-create a sales funnel

It offers two or three concepts, draws the one you pick, checks it against the rules and in a browser, and hands over hairline-<name>.html: one file with no dependencies that opens from disk. It runs on any agent that reads skills: Claude Code, Cursor, Codex and others.

Four figures it drew, each beside its prompt: hairline.lucasmarkes.com/skill.

More

License

MIT © Lucas Marques

About

Six isometric line figures that answer the pointer. For React and for anything with a DOM.

Topics

Resources

Contributing

Stars

1.8k stars

Watchers

2 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages