Features
- Web Components — Framework-agnostic custom elements that work anywhere
- Light/Dark Mode — Automatic theme support via
prefers-color-schemeor manualdata-themeattribute - Accessible — ARIA-compliant components with keyboard navigation
- Glassmorphism — Modern UI effects with backdrop blur and semi-transparent surfaces
- Form Integration — Components support
ElementInternalsfor native form participation - CSS Custom Properties — Easily customize design tokens without modifying source
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:
handfish.noisefactor.io/0— Rolling latest within major 0. Auto-tracks every minor and patch release (e.g.0.10.0→0.10.1→0.11.0) until a1.0ships, at which point this URL freezes and consumers explicitly migrate to/1. Recommended default for most integrations.handfish.noisefactor.io/0.10— Rolling latest within the 0.10 minor series. Stays on0.10.xeven if0.11or1.0ships. Use when you want patch-level updates with explicit control over minor upgrades.handfish.noisefactor.io/0.10.1— Exact pin, immutable. Contents never change once published. Use for reproducible builds and frozen historical versions.
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>
| Attribute | Type | Default | Description |
|---|---|---|---|
checked | boolean | false | Current checked state |
label | string | '' | Label text |
name | string | '' | Form field name |
disabled | boolean | false | Disabled 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>
| Attribute | Type | Default | Description |
|---|---|---|---|
min | number | 0 | Minimum value |
max | number | 100 | Maximum value |
value | number | 0 | Current value |
step | number | 0.01 | Step increment |
type | string | 'float' | Value type: int or float |
name | string | '' | Form field name |
disabled | boolean | false | Disabled state |
Events
inputchange
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>
| Attribute | Type | Default | Description |
|---|---|---|---|
value | string | '' | Selected option value |
name | string | '' | Form field name |
disabled | boolean | false | Disabled 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>
| Attribute | Type | Default | Description |
|---|---|---|---|
value | string | 'left' | Current value: left, center, right |
name | string | '' | Form field name |
disabled | boolean | false | Disabled state |
Color Picker
A dropdown color picker with swatch trigger.
<color-picker
name="fillColor"
value="#a5b8ff"
alpha="1"
mode="hsv"
></color-picker>
| Attribute | Type | Default | Description |
|---|---|---|---|
value | string | '#000000' | Hex color value |
alpha | number | 1 | Alpha/opacity (0-1) |
mode | string | 'hsv' | Color mode: hsv, oklab, or oklch |
inline | boolean | false | Always show wheel (no dropdown) |
name | string | '' | Form field name |
required | boolean | false | Required field |
disabled | boolean | false | Disabled state |
Events
inputchangecolorinput(detail:{ value, alpha, rgb, hsv, oklch })openclose
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>
| Attribute | Type | Default | Description |
|---|---|---|---|
value | string | '#000000' | Hex color value |
alpha | number | 1 | Alpha/opacity (0-1) |
mode | string | 'hsv' | Color mode: hsv, oklab, or oklch |
name | string | '' | Form field name |
required | boolean | false | Required field |
disabled | boolean | false | Disabled state |
Methods
getColor()— Returns{ value, alpha, rgb, hsv, oklch }setColor({ value, alpha, mode })— Set color programmatically
Events
input— Fires during color selectionchange— Fires when selection is finalizedcolorinput— Fires withdetail: { 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>
| Attribute | Type | Default | Description |
|---|---|---|---|
color | string | '#000000' | Hex color value |
size | number | 32 | Swatch size in pixels |
selected | boolean | false | Selected state (shows outline ring) |
editable | boolean | false | Enable double-click to edit |
show-tooltip | boolean | false | Show hex tooltip on hover |
disabled | boolean | false | Disabled state |
Events
select(detail:{ color })edit(detail:{ color })
Gradient Stops
Draggable color stop handles for positioning colors in a gradient.
<gradient-stops></gradient-stops>
| Attribute | Type | Default | Description |
|---|---|---|---|
disabled | boolean | false | Disabled state |
Methods
setStops(colors, positions)— Set colors (RGB 0-1 arrays) and positions (0-1)getPositions()— Get current position arraygetSelectedIndex()— Get selected stop indexsetSelectedIndex(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>
| Attribute | Type | Default | Description |
|---|---|---|---|
value | string | '0,0' | Comma-separated X,Y values |
min | number | -1 | Minimum axis value |
max | number | 1 | Maximum axis value |
step | number | 0.01 | Step increment |
normalized | boolean | false | Normalize to unit vector |
name | string | '' | Form field name |
disabled | boolean | false | Disabled state |
Events
inputchange
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>
| Attribute | Type | Default | Description |
|---|---|---|---|
value | string | '0,0,1' | Comma-separated X,Y,Z values |
min | number | -1 | Minimum axis value |
max | number | 1 | Maximum axis value |
step | number | 0.01 | Step increment |
normalized | boolean | false | Normalize to unit vector |
name | string | '' | Form field name |
disabled | boolean | false | Disabled state |
Events
inputchange
Code Editor
A code editor with line numbers and pluggable syntax highlighting.
<code-editor
value="// Hello world"
placeholder="Enter code..."
line-numbers
></code-editor>
| Attribute | Type | Default | Description |
|---|---|---|---|
value | string | '' | Editor content |
placeholder | string | '' | Placeholder text |
readonly | boolean | false | Read-only mode |
disabled | boolean | false | Disabled state |
spellcheck | boolean | false | Enable spell check |
line-numbers | boolean | true | Show line numbers |
font-family | string | — | Override font |
font-size | string | — | Override font size |
background-color | string | — | Override background |
background-opacity | string | — | Override background opacity |
text-color | string | — | Override text color |
caret-color | string | — | Override caret color |
selection-color | string | — | Override 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>
| Attribute | Type | Default | Description |
|---|---|---|---|
active | boolean | false | Show/hide the magnifier |
zoom | number | 8 | Zoom level |
size | number | 120 | Magnifier diameter in pixels |
Methods
attachToCanvas(canvas)— Set the source canvas to magnifyupdate(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.
| Token | Description |
|---|---|
--hf-color-1 through --hf-color-7 | Base palette (dark to light) |
--hf-accent-1 through --hf-accent-4 | Accent colors (higher chroma) |
--hf-red, --hf-green, --hf-yellow, --hf-blue | Semantic colors |
Semantic Aliases
These reference the base palette and auto-resolve when the theme changes:
| Token | Maps to | Description |
|---|---|---|
--hf-bg-base | --hf-color-1 | Page background |
--hf-bg-surface | --hf-color-2 | Card/panel background |
--hf-bg-elevated | --hf-color-3 | Elevated surface (inputs, buttons) |
--hf-bg-muted | --hf-color-4 | Muted/hover background |
--hf-text-muted | --hf-color-4 | Muted text |
--hf-text-dim | --hf-color-5 | Dim/secondary text |
--hf-text-normal | --hf-color-6 | Normal text |
--hf-text-bright | --hf-color-7 | Bright/emphasized text |
--hf-border-subtle | --hf-color-4 | Subtle borders |
--hf-titlebar-bg | --hf-color-3 | Title bar background |
--hf-accent-bg | --hf-accent-1 | Accent background |
--hf-accent | --hf-accent-3 | Primary accent |
--hf-accent-hover | --hf-accent-4 | Accent hover state |
--hf-border | — | Semi-transparent accent border |
--hf-border-hover | — | Border hover state |
--hf-border-focus | — | Border focus state |
Typography Tokens
| Token | Value |
|---|---|
--hf-font-family | Nunito, 'Nunito Blank' |
--hf-font-family-mono | 'Noto Sans Mono', 'Noto Sans Mono Blank' |
--hf-font-family-icon | 'Material Symbols Outlined' |
--hf-size-xs | 0.625rem (10px) |
--hf-size-sm | 0.75rem (12px) |
--hf-size-base | 0.875rem (14px) |
--hf-size-md | 1rem (16px) |
--hf-size-lg | 1.125rem (18px) |
--hf-size-xl | 1.25rem (20px) |
--hf-size-2xl | 1.5rem (24px) |
--hf-weight-normal | 400 |
--hf-weight-medium | 500 |
--hf-weight-semibold | 600 |
--hf-weight-bold | 700 |
--hf-leading-tight | 1.2 |
--hf-leading-normal | 1.5 |
--hf-leading-relaxed | 1.75 |
--hf-tracking-tight | -0.025em |
--hf-tracking-normal | 0 |
--hf-tracking-wide | 0.05em |
Spacing Tokens
| Token | Value |
|---|---|
--hf-space-0 | 0 |
--hf-space-1 | 0.25rem (4px) |
--hf-space-2 | 0.5rem (8px) |
--hf-space-3 | 0.75rem (12px) |
--hf-space-4 | 1rem (16px) |
--hf-space-5 | 1.25rem (20px) |
--hf-space-6 | 1.5rem (24px) |
--hf-space-8 | 2rem (32px) |
--hf-space-10 | 2.5rem (40px) |
--hf-space-12 | 3rem (48px) |
Border Radius Tokens
| Token | Value |
|---|---|
--hf-radius-none | 0 |
--hf-radius-sm | 0.25rem (4px) |
--hf-radius-md | 0.375rem (6px) |
--hf-radius | 0.5rem (8px) |
--hf-radius-lg | 0.75rem (12px) |
--hf-radius-xl | 1rem (16px) |
--hf-radius-pill | 999px |
--hf-radius-full | 50% |
Shadow Tokens
| Token | Value |
|---|---|
--hf-shadow-sm | 0 1px 2px rgba(0, 0, 0, 0.1) |
--hf-shadow | 0 2px 4px rgba(0, 0, 0, 0.15) |
--hf-shadow-md | 0 4px 8px rgba(0, 0, 0, 0.2) |
--hf-shadow-lg | 0 8px 16px rgba(0, 0, 0, 0.25) |
--hf-shadow-xl | 0 16px 32px rgba(0, 0, 0, 0.3) |
--hf-glow-accent | 0 0 12px accent glow |
Control Tokens
| Token | Value |
|---|---|
--hf-control-height-sm | 1.5rem (24px) |
--hf-control-height | 1.875rem (30px) |
--hf-control-height-lg | 2.25rem (36px) |
--hf-control-padding | 0.25rem 0.5rem |
--hf-titlebar-height | 2.25rem (36px) |
Transition Tokens
| Token | Value |
|---|---|
--hf-transition-fast | 0.1s ease |
--hf-transition | 0.15s ease |
--hf-transition-slow | 0.3s ease |
--hf-transition-color | color 0.15s ease |
--hf-transition-bg | background-color 0.15s ease |
--hf-transition-border | border-color 0.15s ease |
--hf-transition-opacity | opacity 0.15s ease |
--hf-transition-transform | transform 0.15s ease |
Z-Index Scale
| Token | Value |
|---|---|
--hf-z-base | 0 |
--hf-z-dropdown | 100 |
--hf-z-sticky | 200 |
--hf-z-fixed | 300 |
--hf-z-modal-backdrop | 400 |
--hf-z-modal | 500 |
--hf-z-popover | 600 |
--hf-z-tooltip | 700 |
Glassmorphism Tokens
| Token | Value |
|---|---|
--hf-glass-blur | blur(20px) |
--hf-glass-blur-sm | blur(8px) |
--hf-glass-blur-lg | blur(32px) |
--hf-backdrop | rgba(0, 0, 0, 0.6) |
--hf-surface-opacity | 92% |
--hf-surface-transparency | 8% |
--hf-panel-opacity | 85% |
--hf-panel-transparency | 15% |
--hf-header-opacity | 65% |
--hf-header-transparency | 35% |
--hf-chrome-highlight-blend | 86% |
--hf-chrome-highlight-tint | 14% |
--hf-chrome-shadow-blend | 72% |
--hf-chrome-shadow-shade | 28% |
Border Tokens
| Token | Value |
|---|---|
--hf-border-width | 1px |
Focus Ring Tokens
| Token | Value |
|---|---|
--hf-focus-ring-width | 1px |
--hf-focus-ring-offset | 2px |
--hf-focus-ring-color | var(--hf-accent) |
Text Transform Tokens
| Token | Value |
|---|---|
--hf-text-transform | lowercase |
--hf-text-transform-heading | uppercase |
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
- Chrome 90+
- Firefox 90+
- Safari 15.4+
- Edge 90+
Requires support for:
- Custom Elements v1
- CSS Custom Properties
oklch()color valuescolor-mix()CSS functionbackdrop-filter(with fallback)