hairline README

The project's README from github.com/lucasmarkes/hairline. As one markdown file: README.md.

hairline

Nineteen 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")!);

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.

Options

Every figure takes the same four, 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×".

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

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 above, as one HTML file.

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

hairline (lucasmarkes/hairline)

svelte
Searching Database…

What is this?Hairline is a collection of nineteen small isometric line figures - like a keyboard, vault, laptop or router - that animate live as you move the pointer over them. It ships as a tiny library you can use in React or drop into any plain DOM project.
Is this a React-only library or does it work with plain JavaScript too?It is framework-agnostic at its core: each figure is a plain function that draws into any DOM element and exposes update/destroy, and React is only an optional peer needed for the separate @lucasmarkes/hairline/react entry with ready-made components.
How do I add it to a project?Install the package (zero dependencies, ESM only), then either render a React component such as Riffle or call a figure's function on a DOM element and use its update/destroy methods; a shadcn command can also add the package plus a themed wrapper that reads your design tokens.
Does it work with server-side rendering?Yes: on the server a component just renders as an empty box reserving the figure's 5:4 aspect ratio so nothing shifts, but the actual drawing code needs a real DOM, so you have to trigger it inside an effect, onMount, or a script that runs after the element exists.
Does prefers-reduced-motion turn the figures off completely?No, figures that animate on their own (Phosphor and Slow) hold still under prefers-reduced-motion, but every figure still responds when you move the pointer over it.
Can I restyle the figures to match my site's colors?Yes, each figure exposes six CSS custom properties you set on the element or an ancestor, with --hairline-plate being the key one since plates are filled shapes that should match your actual background.
Will having many figures on one page hurt performance?All figures share a single requestAnimationFrame loop, and any figure that's off-screen or at rest does no work, so the loop only runs while something is actually animating.
Can I use Hairline with Svelte even though there's no dedicated Svelte package?Yes, the update/destroy functions a figure returns match the exact shape Svelte expects from an action, so you can bind a figure directly as a Svelte action without any extra wrapper.
Size
5.9 MB
Stars
2.2K
Age
17d
Commits · 90 Days