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)

Searching Database…

这是什么?Hairline 是一组共 19 个小的等距线框图形——比如键盘、金库、笔记本或路由器——指针在上面移动时会实时动起来。它以一个小库的形式发布,可以用在 React 里,也可以丢进任何纯 DOM 项目。
它是 React 专属库,还是配纯 JavaScript 也能用?它的核心与框架无关:每个图形都是一个普通函数,画进任意 DOM 元素并提供 update/destroy;React 只是可选的 peer 依赖,只有要用带现成组件的 @lucasmarkes/hairline/react 入口时才需要。
怎么把它加进项目?安装包(零依赖,仅 ESM),然后要么渲染 Riffle 这类 React 组件,要么在 DOM 元素上调用图形的函数并用它的 update/destroy 方法;也可以用一条 shadcn 命令装上该包,外加一个读取你设计令牌的主题化封装。
它支持服务端渲染吗?支持:在服务端,组件只会渲染成一个预留图形 5:4 宽高比的空盒子,避免布局跳动;但实际的绘制代码需要真实 DOM,所以你得在 effect、onMount 或元素存在后才运行的脚本里触发它。
prefers-reduced-motion 会把图形动画彻底关掉吗?不会,会自行播放动画的图形(Phosphor 和 Slow)在 prefers-reduced-motion 下会保持静止,但每个图形在指针滑过时依然会响应。
能调整图形配色来配合我网站的颜色吗?可以,每个图形暴露 6 个 CSS 自定义属性,可以设在元素或其祖先上,其中 --hairline-plate 最关键,因为底板是填充形状,应该和你的实际背景一致。
一个页面上放很多图形会影响性能吗?所有图形共用一个 requestAnimationFrame 循环,任何在屏幕外或静止中的图形都不做工作,所以只有真正有东西在动时循环才会运转。
虽然没有专门的 Svelte 包,Hairline 能配合 Svelte 用吗?可以,图形返回的 update/destroy 函数和 Svelte 对 action 的结构要求完全一致,所以可以直接把图形绑定为 Svelte action,不需要任何额外封装。
Size
5.9 MB
Stars
2.4K
Age
19d
Commits · 90 Days