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.

Import Styles

<link rel="stylesheet" href="proxy.php?url=https%3A%2F%2Fhandfish.noisefactor.io%2F0.9.0%2Fstyles%2Ftokens.css">
<link rel="stylesheet" href="proxy.php?url=https%3A%2F%2Fhandfish.noisefactor.io%2F0.9.0%2Fstyles%2Fthemes%2Fneutral.css">

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

<link rel="stylesheet" href="proxy.php?url=https%3A%2F%2Fhandfish.noisefactor.io%2F0.9.0%2Fstyles%2Findex.css">

Import Components

Using an import map (recommended):

<script type="importmap">
{
    "imports": {
        "handfish": "https://handfish.noisefactor.io/0.9.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.9.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="proxy.php?url=https%3A%2F%2Ffonts.noisefactor.io%2Ffonts%2Fnunito%2FNunito-Blank.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="proxy.php?url=https%3A%2F%2Ffonts.noisefactor.io%2Ffonts%2Fnoto-sans-mono%2FNotoSansMono-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: