hairlinev0.2.0

Nineteen figures, one set of options.

Every figure takes the same four options and draws itself in SVG, with no dependencies. Install the package, paste a figure, and turn intensity up or down.

Install

Install the package, then import a figure wherever your interface runs.

i @lucasmarkes/hairline

Click the command to switch package manager. ESM only, no dependencies, 39.9 kB gzipped for all nineteen; a bundle that imports one carries one.

Quick start

Paste a figure. It fills its parent’s width at a 5:4 aspect ratio.

app/page.tsx
import { Terrain } from "@lucasmarkes/hairline/react";

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

Options

Every figure takes the same four options, all optional.

OptionTypeDefaultWhat it does
intensitynumber0.5How 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""auto""auto" follows the page: an ancestor with class dark or data-theme="dark", then the page's color-scheme.
labelstringa descriptionThe accessible name. In React, aria-label does the same.
onRead(text: string) => voidThe figure's caption, each time it changes. Called once at mount with the rest caption.

React

Signature
<Terrain
  intensity?: number
  theme?: "auto" | "light" | "dark"
  label?: string
  onRead?: (text: string) => void
  {...divProps}
/>

Import any of the nineteen from @lucasmarkes/hairline/react. Each renders a <div>, takes any <div> attribute and forwards its ref. The entry is a client module, so a Server Component renders it without writing "use client".

app/page.tsx
import { Terrain } from "@lucasmarkes/hairline/react";

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

Vanilla

Signature
terrain(element: HTMLElement, options?: HairlineOptions): {
  update(options: HairlineOptions): void
  destroy(): void
}

One function per figure, named in lower case: riffle, terrain, exploded, phosphor, slow, turntable, keyboard, elevator, phone, laptop, terminal, cabinet, branches, vault, lockers, padlock, patch, dish, router. In update, a key set to undefined goes back to its default and a key left out stays as it is. destroy removes the drawing and its listeners.

main.ts
import { terrain } from "@lucasmarkes/hairline";

const figure = terrain(document.getElementById("figure")!);

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

CDN

Without a bundler, import from esm.sh in a module script.

index.html
<div id="figure" style="width: 400px"></div>

<script type="module">
  import { terrain } from "https://esm.sh/@lucasmarkes/hairline";

  terrain(document.getElementById("figure"));
</script>

Figures

Every figure takes intensity, from 0 to 1. Here is what it turns up.

FigureParameter00.51
Rifflestagger0 ms40 ms90 ms
Terrainradius1.5 cells3 cells5 cells
Explodedgap12 viewBox units28 viewBox units40 viewBox units
Phosphorafterglow150 ms520 ms1500 ms
Slowrate0.6× normal speed0.2× normal speed0.05× normal speed
Turntablecoast200 ms650 ms1500 ms
Keyboardradius1 keys2 keys3.5 keys
Elevatorstiffness40 spring units100 spring units220 spring units
Phonegap16 viewBox units28 viewBox units40 viewBox units
Laptoplid100 degrees125 degrees150 degrees
Terminalspread1 lines2 lines3.5 lines
Cabinetreach1.5 blades3 blades5 blades
Branchesreach1 commits3 commits6 commits
Vaultcoast250 ms600 ms1500 ms
Lockersopening55 degrees90 degrees120 degrees
Padlockswing45 degrees90 degrees100 degrees
Patchradius1 ports2.5 ports5 ports
Dishreach30 degrees50 degrees70 degrees
Routerspread0.5 antennas1.5 antennas3 antennas
Try each one on the figures page →

Theme

Six 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.

PropertyLightRole
--hairline-plate#ffffffThe fill of every plate. It hides what is drawn behind, so it must be the colour the figure sits on.
--hairline-hi#232327The stroke of what is lit: the card pulled, the layer picked, a dot that is on.
--hairline-edge#a4a4acSilhouettes, and dots at half strength.
--hairline-mid#c3c3c9Every other stroke.
--hairline-lo#e0e0e4What recedes: guides, and dots that are off.
--hairline-stroke0.9The stroke width, in CSS pixels at any size.
globals.css
/* On a figure or anything above it. Without them a figure is light,
   or dark when the page says so. --hairline-plate must be the colour
   the figure sits on: it hides what is drawn behind each plate. */
.figures {
  --hairline-plate: #ffffff;
  --hairline-hi: #232327;
  --hairline-edge: #a4a4ac;
  --hairline-mid: #c3c3c9;
  --hairline-lo: #e0e0e4;
  --hairline-stroke: 0.9;
}

Accessibility

Anything missing? Open an issue on GitHub.