Handfish Design System

A modern, accessible component library for creative tools

Live Demo

Features

Usage

Handfish is served as a versioned ESM bundle from handfish.noisefactor.io. No npm install needed. We respect privacy: we do not track users, and we don't log beyond what is necessary for server operations.

Pinning Levels

The CDN exposes three URL shapes for every release. Pick the one that matches how much drift between deploys your application can tolerate:

The examples below use /0 (the recommended default). Substitute /0.10 or /0.10.1 if you want a stricter pin.

Import Styles

<link rel="stylesheet" href="https://handfish.noisefactor.io/0/styles/tokens.css">
<link rel="stylesheet" href="https://handfish.noisefactor.io/0/styles/themes/neutral.css">

Or load all styles (tokens + forms + menus + tags):

<link rel="stylesheet" href="https://handfish.noisefactor.io/0/styles/index.css">

Import Components

Using an import map (recommended):

<script type="importmap">
{
    "imports": {
        "handfish": "https://handfish.noisefactor.io/0/handfish.esm.min.js"
    }
}
</script>
// Then import by name
import { ToggleSwitch, SliderValue, ColorPicker } from 'handfish'

Or import directly:

import { ToggleSwitch, SliderValue, ColorPicker } from 'https://handfish.noisefactor.io/0/handfish.esm.min.js'

Use Components

<toggle-switch label="Enable feature"></toggle-switch>

<slider-value min="0" max="100" value="50" step="1" type="int"></slider-value>

<select-dropdown value="option1">
    <option value="option1">Option 1</option>
    <option value="option2">Option 2</option>
</select-dropdown>

<color-picker value="#a5b8ff"></color-picker>

Font Loading (Zero-CLS)

Handfish uses Fontaine placeholder fonts to eliminate Cumulative Layout Shift. For each web font, a metric-matched Blank variant (~3-15KB) reserves exact glyph widths with invisible shapes. The blank loads near-instantly with font-display: block, then the real font swaps in via font-display: swap with zero reflow.

Fontaine also provides Block placeholders (solid rectangles, useful during development) via {FontName}-Block.woff2.

Generic fallbacks like sans-serif are intentionally omitted — system fonts have different metrics and would cause layout shift.

Add this to your <head>, before any Handfish stylesheets:

<!-- Preload blank fonts (tiny, loads in one packet) -->
<link rel="preload" href="https://fonts.noisefactor.io/fonts/nunito/Nunito-Blank.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="https://fonts.noisefactor.io/fonts/noto-sans-mono/NotoSansMono-Blank.woff2" as="font" type="font/woff2" crossorigin>

<style>
/* Blank placeholders: font-display: block */
@font-face {
    font-family: 'Nunito Blank';
    src: url('https://fonts.noisefactor.io/fonts/nunito/Nunito-Blank.woff2') format('woff2');
    font-weight: 100 900;
    font-display: block;
}
@font-face {
    font-family: 'Noto Sans Mono Blank';
    src: url('https://fonts.noisefactor.io/fonts/noto-sans-mono/NotoSansMono-Blank.woff2') format('woff2');
    font-weight: 100 900;
    font-display: block;
}

/* Real fonts: font-display: swap */
@font-face {
    font-family: Nunito;
    src: url('https://fonts.noisefactor.io/fonts/nunito/Nunito[wght].woff2') format('woff2');
    font-weight: 100 900;
    font-display: swap;
}
@font-face {
    font-family: 'Noto Sans Mono';
    src: url('https://fonts.noisefactor.io/fonts/noto-sans-mono/NotoSansMono[wdth,wght].woff2') format('woff2');
    font-weight: 100 900;
    font-display: swap;
}

/* Fallback chain: real font → blank placeholder. No generics. */
html { font-family: Nunito, 'Nunito Blank'; }
</style>

Components

Toggle Switch

A boolean toggle control that replaces <input type="checkbox">.

<toggle-switch
    name="darkMode"
    label="Dark Mode"
    checked
></toggle-switch>
AttributeTypeDefaultDescription
checkedbooleanfalseCurrent checked state
labelstring''Label text
namestring''Form field name
disabledbooleanfalseDisabled state

Slider Value

A range slider with editable numeric value display. Uses display: contents to participate in parent grid layouts. Click the value to type an exact number.

<slider-value
    name="volume"
    min="0"
    max="100"
    value="50"
    step="1"
    type="int"
></slider-value>
AttributeTypeDefaultDescription
minnumber0Minimum value
maxnumber100Maximum value
valuenumber0Current value
stepnumber0.01Step increment
typestring'float'Value type: int or float
namestring''Form field name
disabledbooleanfalseDisabled state

Events

  • input
  • change

Select Dropdown

A custom dropdown select with keyboard navigation and search.

<select-dropdown name="effect" value="blur">
    <option value="none">None</option>
    <option value="blur">Blur</option>
    <option value="glow">Glow</option>
</select-dropdown>
AttributeTypeDefaultDescription
valuestring''Selected option value
namestring''Form field name
disabledbooleanfalseDisabled state

Features:

  • Type-ahead search when focused
  • Arrow key navigation
  • Auto-switches to dialog mode with 6+ options
  • Escape to close

Justify Button Group

A segmented control for text alignment selection.

<justify-button-group
    name="alignment"
    value="center"
></justify-button-group>
AttributeTypeDefaultDescription
valuestring'left'Current value: left, center, right
namestring''Form field name
disabledbooleanfalseDisabled state

Color Picker

A dropdown color picker with swatch trigger.

<color-picker
    name="fillColor"
    value="#a5b8ff"
    alpha="1"
    mode="hsv"
></color-picker>
AttributeTypeDefaultDescription
valuestring'#000000'Hex color value
alphanumber1Alpha/opacity (0-1)
modestring'hsv'Color mode: hsv, oklab, or oklch
inlinebooleanfalseAlways show wheel (no dropdown)
namestring''Form field name
requiredbooleanfalseRequired field
disabledbooleanfalseDisabled state

Events

  • input
  • change
  • colorinput (detail: { value, alpha, rgb, hsv, oklch })
  • open
  • close

Color Wheel

The full color wheel interface (used inside Color Picker). Supports three color modes: HSV, OkLab, and OKLCH.

<color-wheel
    value="#6bffa5"
    mode="hsv"
></color-wheel>
AttributeTypeDefaultDescription
valuestring'#000000'Hex color value
alphanumber1Alpha/opacity (0-1)
modestring'hsv'Color mode: hsv, oklab, or oklch
namestring''Form field name
requiredbooleanfalseRequired field
disabledbooleanfalseDisabled state

Methods

  • getColor() — Returns { value, alpha, rgb, hsv, oklch }
  • setColor({ value, alpha, mode }) — Set color programmatically

Events

  • input — Fires during color selection
  • change — Fires when selection is finalized
  • colorinput — Fires with detail: { value, alpha, rgb, hsv, oklch }

Color Swatch

A single color display with selection and tooltip.

<color-swatch
    color="#a5b8ff"
    size="32"
    selected
    show-tooltip
></color-swatch>
AttributeTypeDefaultDescription
colorstring'#000000'Hex color value
sizenumber32Swatch size in pixels
selectedbooleanfalseSelected state (shows outline ring)
editablebooleanfalseEnable double-click to edit
show-tooltipbooleanfalseShow hex tooltip on hover
disabledbooleanfalseDisabled state

Events

  • select (detail: { color })
  • edit (detail: { color })

Gradient Stops

Draggable color stop handles for positioning colors in a gradient.

<gradient-stops></gradient-stops>
AttributeTypeDefaultDescription
disabledbooleanfalseDisabled state

Methods

  • setStops(colors, positions) — Set colors (RGB 0-1 arrays) and positions (0-1)
  • getPositions() — Get current position array
  • getSelectedIndex() — Get selected stop index
  • setSelectedIndex(index) — Set selected stop

Events

  • select (detail: { index })
  • input (detail: { index, position, positions })
  • change (detail: { index, positions })
  • delete (detail: { index, positions, colors })

Vector 2D Picker

A 2D vector picker with interactive XY pad and sliders in a dialog modal.

<vector2d-picker
    value="0.5,0.5"
    min="-1"
    max="1"
    step="0.01"
    normalized
></vector2d-picker>
AttributeTypeDefaultDescription
valuestring'0,0'Comma-separated X,Y values
minnumber-1Minimum axis value
maxnumber1Maximum axis value
stepnumber0.01Step increment
normalizedbooleanfalseNormalize to unit vector
namestring''Form field name
disabledbooleanfalseDisabled state

Events

  • input
  • change

Vector 3D Picker

A 3D vector picker with interactive sphere gizmo and XYZ sliders in a dialog modal.

<vector3d-picker
    value="0,1,0"
    min="-1"
    max="1"
    step="0.01"
    normalized
></vector3d-picker>
AttributeTypeDefaultDescription
valuestring'0,0,1'Comma-separated X,Y,Z values
minnumber-1Minimum axis value
maxnumber1Maximum axis value
stepnumber0.01Step increment
normalizedbooleanfalseNormalize to unit vector
namestring''Form field name
disabledbooleanfalseDisabled state

Events

  • input
  • change

Code Editor

A code editor with line numbers and pluggable syntax highlighting.

<code-editor
    value="// Hello world"
    placeholder="Enter code..."
    line-numbers
></code-editor>
AttributeTypeDefaultDescription
valuestring''Editor content
placeholderstring''Placeholder text
readonlybooleanfalseRead-only mode
disabledbooleanfalseDisabled state
spellcheckbooleanfalseEnable spell check
line-numbersbooleantrueShow line numbers
font-familystringOverride font
font-sizestringOverride font size
background-colorstringOverride background
background-opacitystringOverride background opacity
text-colorstringOverride text color
caret-colorstringOverride caret color
selection-colorstringOverride selection color

Methods

  • setTokenizer(fn) — Set a syntax highlighting function: (line: string) => Array<{type, text}>
  • get/set value — Editor content

Events

  • input (detail: { value })
  • forcerecompile
// Use with DSL tokenizer
import { dslTokenizer } from 'handfish'
editor.setTokenizer(dslTokenizer)

Image Magnifier

A zoomed-in view of a canvas under the cursor for precise color picking. Shows crosshairs and the hex value of the center pixel.

<image-magnifier zoom="8" size="120"></image-magnifier>
AttributeTypeDefaultDescription
activebooleanfalseShow/hide the magnifier
zoomnumber8Zoom level
sizenumber120Magnifier diameter in pixels

Methods

  • attachToCanvas(canvas) — Set the source canvas to magnify
  • update(x, y) — Update magnifier position and render

Toast Notifications

Lightweight notification toasts with auto-dismiss. Exported as standalone functions (not a custom element).

import { showToast, showSuccess, showError, showWarning, showInfo } from 'handfish'

showSuccess('Palette saved')
showError('Failed to load', { duration: 6000 })
showWarning('Unsaved changes')
showInfo('Copied to clipboard')
  • showToast(message, { type, duration }) — General toast (type: info, success, error, warning)
  • showSuccess(message, options) — Success toast (default 2s)
  • showError(message, options) — Error toast (default 6s)
  • showWarning(message, options) — Warning toast (default 2s)
  • showInfo(message, options) — Info toast (default 2s)

Utilities

Escape Handler

Stack-based escape key management for closing modals and dropdowns in the correct order.

import { registerEscapeable, unregisterEscapeable, initEscapeHandler } from 'handfish'

initEscapeHandler()
registerEscapeable(element, () => closeMyModal())
unregisterEscapeable(element)

Exports: registerEscapeable, unregisterEscapeable, closeTopmost, hasOpenEscapeables, initEscapeHandler

Tooltips

Hover tooltips for any element with a data-title attribute.

import { initializeTooltips } from 'handfish'

initializeTooltips()
<button data-title="Save palette">Save</button>

Color Conversions

Comprehensive color conversion utilities. All RGB objects use {r, g, b} with 0-255 values.

import { rgbToHex, parseHex, rgbToHsv, hsvToRgb, rgbToOklch, oklchToRgb } from 'handfish'

Design Tokens

All visual values are controlled via CSS custom properties with the --hf- prefix. Colors use OKLCH for perceptually uniform color manipulation. Override any token in your CSS:

:root {
    /* Colors (OKLCH: lightness, chroma, hue) */
    --hf-color-1: oklch(13.9% 0.010 264);
    --hf-accent-3: oklch(79.5% 0.103 264);

    /* Typography */
    --hf-font-family: 'Your Font', sans-serif;

    /* Spacing */
    --hf-space-4: 1rem;
}

Color Tokens

Colors are defined in OKLCH format. Dark mode uses hue 264, light mode uses hue 90.

TokenDescription
--hf-color-1 through --hf-color-7Base palette (dark to light)
--hf-accent-1 through --hf-accent-4Accent colors (higher chroma)
--hf-red, --hf-green, --hf-yellow, --hf-blueSemantic colors

Semantic Aliases

These reference the base palette and auto-resolve when the theme changes:

TokenMaps toDescription
--hf-bg-base--hf-color-1Page background
--hf-bg-surface--hf-color-2Card/panel background
--hf-bg-elevated--hf-color-3Elevated surface (inputs, buttons)
--hf-bg-muted--hf-color-4Muted/hover background
--hf-text-muted--hf-color-4Muted text
--hf-text-dim--hf-color-5Dim/secondary text
--hf-text-normal--hf-color-6Normal text
--hf-text-bright--hf-color-7Bright/emphasized text
--hf-border-subtle--hf-color-4Subtle borders
--hf-titlebar-bg--hf-color-3Title bar background
--hf-accent-bg--hf-accent-1Accent background
--hf-accent--hf-accent-3Primary accent
--hf-accent-hover--hf-accent-4Accent hover state
--hf-borderSemi-transparent accent border
--hf-border-hoverBorder hover state
--hf-border-focusBorder focus state

Typography Tokens

TokenValue
--hf-font-familyNunito, 'Nunito Blank'
--hf-font-family-mono'Noto Sans Mono', 'Noto Sans Mono Blank'
--hf-font-family-icon'Material Symbols Outlined'
--hf-size-xs0.625rem (10px)
--hf-size-sm0.75rem (12px)
--hf-size-base0.875rem (14px)
--hf-size-md1rem (16px)
--hf-size-lg1.125rem (18px)
--hf-size-xl1.25rem (20px)
--hf-size-2xl1.5rem (24px)
--hf-weight-normal400
--hf-weight-medium500
--hf-weight-semibold600
--hf-weight-bold700
--hf-leading-tight1.2
--hf-leading-normal1.5
--hf-leading-relaxed1.75
--hf-tracking-tight-0.025em
--hf-tracking-normal0
--hf-tracking-wide0.05em

Spacing Tokens

TokenValue
--hf-space-00
--hf-space-10.25rem (4px)
--hf-space-20.5rem (8px)
--hf-space-30.75rem (12px)
--hf-space-41rem (16px)
--hf-space-51.25rem (20px)
--hf-space-61.5rem (24px)
--hf-space-82rem (32px)
--hf-space-102.5rem (40px)
--hf-space-123rem (48px)

Border Radius Tokens

TokenValue
--hf-radius-none0
--hf-radius-sm0.25rem (4px)
--hf-radius-md0.375rem (6px)
--hf-radius0.5rem (8px)
--hf-radius-lg0.75rem (12px)
--hf-radius-xl1rem (16px)
--hf-radius-pill999px
--hf-radius-full50%

Shadow Tokens

TokenValue
--hf-shadow-sm0 1px 2px rgba(0, 0, 0, 0.1)
--hf-shadow0 2px 4px rgba(0, 0, 0, 0.15)
--hf-shadow-md0 4px 8px rgba(0, 0, 0, 0.2)
--hf-shadow-lg0 8px 16px rgba(0, 0, 0, 0.25)
--hf-shadow-xl0 16px 32px rgba(0, 0, 0, 0.3)
--hf-glow-accent0 0 12px accent glow

Control Tokens

TokenValue
--hf-control-height-sm1.5rem (24px)
--hf-control-height1.875rem (30px)
--hf-control-height-lg2.25rem (36px)
--hf-control-padding0.25rem 0.5rem
--hf-titlebar-height2.25rem (36px)

Transition Tokens

TokenValue
--hf-transition-fast0.1s ease
--hf-transition0.15s ease
--hf-transition-slow0.3s ease
--hf-transition-colorcolor 0.15s ease
--hf-transition-bgbackground-color 0.15s ease
--hf-transition-borderborder-color 0.15s ease
--hf-transition-opacityopacity 0.15s ease
--hf-transition-transformtransform 0.15s ease

Z-Index Scale

TokenValue
--hf-z-base0
--hf-z-dropdown100
--hf-z-sticky200
--hf-z-fixed300
--hf-z-modal-backdrop400
--hf-z-modal500
--hf-z-popover600
--hf-z-tooltip700

Glassmorphism Tokens

TokenValue
--hf-glass-blurblur(20px)
--hf-glass-blur-smblur(8px)
--hf-glass-blur-lgblur(32px)
--hf-backdroprgba(0, 0, 0, 0.6)
--hf-surface-opacity92%
--hf-surface-transparency8%
--hf-panel-opacity85%
--hf-panel-transparency15%
--hf-header-opacity65%
--hf-header-transparency35%
--hf-chrome-highlight-blend86%
--hf-chrome-highlight-tint14%
--hf-chrome-shadow-blend72%
--hf-chrome-shadow-shade28%

Border Tokens

TokenValue
--hf-border-width1px

Focus Ring Tokens

TokenValue
--hf-focus-ring-width1px
--hf-focus-ring-offset2px
--hf-focus-ring-colorvar(--hf-accent)

Text Transform Tokens

TokenValue
--hf-text-transformlowercase
--hf-text-transform-headinguppercase

Theming

Automatic (System Preference)

Colors automatically adapt to prefers-color-scheme.

Manual Theme

<html data-theme="dark">
<!-- or -->
<html data-theme="light">
// Toggle theme
document.documentElement.dataset.theme =
    document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'

Utility Classes

Typography

<p class="hf-text-sm">Small text</p>
<p class="hf-font-bold">Bold text</p>
<p class="hf-mono">Monospace text</p>
<p class="hf-uppercase">Uppercase</p>
<p class="hf-text-accent">Accent color</p>

Layout

<div class="hf-flex hf-gap-2 hf-items-center">
    <!-- Flexbox with gap -->
</div>

Surfaces

<div class="hf-panel">Glass panel with border</div>
<div class="hf-card">Card with padding</div>

Buttons

<button class="hf-btn">Default</button>
<button class="hf-btn hf-btn-primary">Primary</button>
<button class="hf-btn hf-btn-ghost">Ghost</button>

Browser Support

Requires support for: