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 — коллекция из девятнадцати маленьких изометрических линейных фигур — вроде клавиатуры, сейфа, ноутбука или роутера, — которые оживают, когда ведёшь по ним указателем. Поставляется как крошечная библиотека: годится для React или для любого обычного DOM-проекта.
Это библиотека только для React или работает и с чистым JavaScript?В ядре она не зависит от фреймворков: каждая фигура — обычная функция, рисующая в любой DOM-элемент и дающая update/destroy, а React — лишь опциональный peer, нужный для отдельной точки входа @lucasmarkes/hairline/react с готовыми компонентами.
Как добавить его в проект?Установите пакет (ноль зависимостей, только ESM), затем либо отрендерьте React-компонент вроде Riffle, либо вызовите функцию фигуры на DOM-элементе и используйте её методы update/destroy; команда shadcn может также добавить пакет вместе с тематизированной обёрткой, читающей ваши дизайн-токены.
Работает ли он с серверным рендерингом?Да: на сервере компонент рендерится просто пустым блоком, резервирующим пропорции 5:4 фигуры, чтобы ничего не прыгало, но сам код рисования требует настоящего DOM, поэтому запускать его нужно внутри effect, onMount или скрипта, исполняющегося после появления элемента.
Отключает ли prefers-reduced-motion фигуры полностью?Нет: фигуры, анимирующиеся сами по себе (Phosphor и Slow), при prefers-reduced-motion замирают, но каждая фигура по-прежнему отвечает, когда ведёшь по ней указателем.
Можно ли перекрасить фигуры под цвета моего сайта?Да, каждая фигура открывает шесть CSS-переменных, задаваемых на элементе или предке; ключевая — --hairline-plate, ведь подложки являются залитыми фигурами и должны совпадать с реальным фоном.
Навредит ли производительности много фигур на одной странице?Все фигуры делят один цикл requestAnimationFrame, а фигура вне экрана или в покое не делает никакой работы, так что цикл крутится, только пока что-то реально анимируется.
Можно ли использовать Hairline со Svelte, хотя отдельного пакета для Svelte нет?Да: функции update/destroy, возвращаемые фигурой, в точности совпадают с тем, что Svelte ждёт от action, поэтому фигуру можно привязать прямо как Svelte action безо всяких обёрток.
Size
5.9 MB
Stars
2.2K
Age
17d
Commits · 90 Days