Grouped numeric control surfaces for Svelte
manifold-ui is a Svelte 5 component library for manipulating groups of related numeric values through drag-to-edit inputs, contextual HUD overlays, and modifier-key rerouting. It handles XYZ positions, RGBA colors, rotation angles, or any N-dimensional numeric tuple -- with zero runtime dependencies beyond Svelte itself.
rgba({r}, {g}, {b}, {a}))fieldset/legend structure, aria-live announcements, full keyboard controlnpm install manifold-ui
Requires Svelte 5 (^5.0.0) as a peer dependency.
Pass a config object. The <Manifold> component creates a controller internally and renders everything for you.
<script lang="ts">
import { Manifold } from 'manifold-ui';
import type { ManifoldSchema, DragHandler } from 'manifold-ui';
const handler: DragHandler = (dx, dy, current, start, member) => {
current[member] = start[member] + dx * 0.5;
};
const schema: ManifoldSchema = {
title: 'Transform',
groups: [
{
id: 'position',
members: ['x', 'y', 'z'],
initial: { x: 0, y: 0, z: 0 },
config: { step: 0.1 },
inputDrag: {
base: { type: 'slider_1d', handler }
}
}
]
};
let values = $state<Record<string, Record<string, number>>>();
</script>
<Manifold {schema} bind:values />
Use ManifoldPanel, ManifoldGroup, and ManifoldInput for custom layouts. Create a controller with createManifold and pass it to the panel.
<script lang="ts">
import {
createManifold,
ManifoldPanel,
ManifoldGroup,
ManifoldInput
} from 'manifold-ui';
const controller = createManifold({
groups: [
{
id: 'color',
members: ['r', 'g', 'b'],
initial: { r: 128, g: 128, b: 128 },
config: { min: 0, max: 255, step: 1 },
inputDrag: {
base: {
type: 'slider_1d',
handler: (dx, dy, current, start, member) => {
current[member] = start[member] - dy * 0.5;
}
}
}
}
]
});
</script>
<ManifoldPanel {controller} title="Color">
<ManifoldGroup id="color" label="RGB">
<ManifoldInput member="r" label="R" />
<ManifoldInput member="g" label="G" />
<ManifoldInput member="b" label="B" />
</ManifoldGroup>
</ManifoldPanel>
The builder is a reactive state machine. No DOM, no components -- just state and methods.
import { createManifold } from 'manifold-ui';
const manifold = createManifold({
groups: [
{ id: 'position', members: ['x', 'y', 'z'], initial: { x: 0, y: 5, z: 0 } }
]
});
// Reactive state (Svelte 5 runes internally)
manifold.values; // { position: { x: 0, y: 5, z: 0 } }
manifold.activeGroup; // 'position'
manifold.modifier; // 'base'
// Imperative updates
manifold.set('position', { x: 10, y: 20 }); // partial update
manifold.reset('position'); // reset to initial values
manifold.undo(); // revert last commit
manifold.redo();
// Events
const unsub = manifold.onChange(({ groupId, member, oldValue, newValue }) => {
console.log(`${groupId}.${member}: ${oldValue} -> ${newValue}`);
});
unsub(); // unsubscribe
Six visual HUD overlays appear at the drag origin during interaction:
| Type | Visual | Best For |
|---|---|---|
slider_1d |
Vertical bar with sliding thumb | Single-axis values, scale, opacity |
axis_2d |
Crosshair circle with moving dot | XY position, 2D offsets |
axis_3d |
XY crosshair + Z cylinder (on Shift) | 3D position with Z-axis toggle |
axis_3d_tilt |
XY crosshair with CSS perspective tilt | 3D position with visual depth |
dial |
Dashed circle with rotating pointer | Rotation angles |
color_wheel |
HSL wheel, SV square (Ctrl), alpha slider (Shift) | Color picking with mode switching |
Display multiple values in a single field using format patterns:
<ManifoldCompoundInput
members={['r', 'g', 'b', 'a']}
pattern="rgba({r}, {g}, {b}, {a})"
label="Color"
/>
Click to edit the full expression. The input parses numbers from typed text using the pattern structure, with a fallback to extracting all numeric values in order.
Built-in color conversion functions for use with the color wheel HUD:
import { rgbToHsl, hslToRgb, rgbToHex, hexToRgb, rgbToCmyk, cmykToRgb } from 'manifold-ui';
import { xyToHueSat, hueSatToXy } from 'manifold-ui';
const hsl = rgbToHsl(157, 78, 221); // { h: 268, s: 70, l: 59 }
const rgb = hslToRgb(268, 70, 59); // { r: 157, g: 78, b: 221 }
const hex = rgbToHex(157, 78, 221); // '#9d4edd'
Override CSS custom properties on a parent container:
.my-app {
/* Panel */
--manifold-bg: #1a1a2e;
--manifold-border: #2d2d44;
--manifold-accent: #10b981;
--manifold-text: #f8fafc;
--manifold-text-dim: #94a3b8;
--manifold-text-label: #cbd5e1;
--manifold-radius: 6px;
--manifold-radius-panel: 12px;
/* Inputs */
--manifold-input-bg: #1a1525;
--manifold-input-bg-focus: #211a30;
--manifold-input-border: #3b2f56;
--manifold-input-border-focus: #9d4edd;
/* HUD */
--manifold-hud-accent: #06b6d4;
--manifold-hud-accent-alt: #c084fc;
}
Strip all default styles with the unstyled prop:
<Manifold schema={schema} unstyled />
All components accept a class prop for additional class names.
See docs/API.md for the full API reference, including all types, component props, HUD behavior details, and CSS custom properties.
pnpm installpnpm devpnpm testpnpm checkThe dev sandbox at localhost:5173 has interactive demos of all features.
MIT -- Michael Blum