Skip to content
MekwekPublic

About

A library to help you dial in interface parameters of any kind

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

DialKit

Live controls for tuning interfaces. Adjust values, compare versions, and edit animation timing in your running app.

Works with React, Solid, Svelte, Vue, and plain JavaScript. The vanilla adapter has no runtime dependencies.

DialKit parameter panel

Quick start · Controls · Panel API · Timeline · Reference

Quick start

Install the adapter's dependencies in your existing app:

Adapter Install Import from
React npm install dialkit motion dialkit
Solid npm install dialkit motion dialkit/solid
Svelte npm install dialkit dialkit/svelte
Vue npm install dialkit motion motion-v dialkit/vue
Plain JavaScript npm install dialkit dialkit/vanilla

Framework support: React 18+, Solid 1.6+, Svelte 5.8+, and Vue 3.3+. Mount one root to display all registered panels.

React

import { DialRoot, useDialKit } from "dialkit";
import "dialkit/styles.css";

export default function App() {
  const values = useDialKit("Card", {
    radius: [24, 0, 64],
    color: "#a78bfa",
  });

  return (
    <>
      <div style={{ borderRadius: values.radius, background: values.color }}>
        Card
      </div>
      <DialRoot />
    </>
  );
}

In Next.js App Router, use DialKit in a client component.

Solid

import { createDialKit, DialRoot } from "dialkit/solid";
import "dialkit/styles.css";

export default function App() {
  const values = createDialKit("Card", { radius: [24, 0, 64] });

  return (
    <>
      <div style={{ "border-radius": `${values().radius}px` }}>Card</div>
      <DialRoot />
    </>
  );
}

Read values through the returned accessor: values().radius.

Svelte

<script>
  import { createDialKit, DialRoot } from "dialkit/svelte";

  const values = createDialKit("Card", { radius: [24, 0, 64] });
</script>

<div style:border-radius={`${values.radius}px`}>Card</div>
<DialRoot />

Values are reactive properties. DialRoot injects the styles; no CSS import is needed.

Vue

<script setup>
import { DialRoot, useDialKit } from "dialkit/vue";
import "dialkit/styles.css";

const values = useDialKit("Card", { radius: [24, 0, 64] });
</script>

<template>
  <div :style="{ borderRadius: `${values.radius}px` }">Card</div>
  <DialRoot />
</template>

Values are a computed ref: use values.value.radius in script. Templates unwrap the ref automatically.

Plain JavaScript

Add an element with id="card" to your page, then bind its styles:

import { createDialKit, createDialRoot } from "dialkit/vanilla";
import "dialkit/vanilla/styles.css";

const card = document.querySelector("#card");
const root = createDialRoot();
const kit = createDialKit("Card", { radius: [24, 0, 64] });

kit.subscribe(values => {
  card.style.borderRadius = `${values.radius}px`;
});

subscribe runs immediately and on panel changes. Read a snapshot with kit.values or kit.getValues(). Call kit.destroy() and root.destroy() when removing them from the page.

Without a bundler: copy dist/vanilla/browser.global.js and dist/vanilla/styles.css from the package into your project:

<link rel="stylesheet" href="./vendor/dialkit/styles.css">
<div id="card">Card</div>
<script src="./vendor/dialkit/browser.global.js"></script>
<script>
  const root = DialKit.createDialRoot();
  const kit = DialKit.createDialKit("Card", { radius: [24, 0, 64] });
  kit.subscribe(values => {
    document.getElementById("card").style.borderRadius = values.radius + "px";
  });
</script>

The script and stylesheet work offline. A self-contained ES module is also available at dist/vanilla/index.js. Use one entry consistently so the root and controls share the same store.

See the plain HTML example.

Controls

Define controls with an object. Keys become labels; returned values keep the same nesting.

Control Definition Returned value
Slider radius: [24, 0, 64] number
Slider with step radius: [24, 0, 64, 2] number
Slider with unit rotate: [0, -180, 180, 1, "°"] number
Inferred slider scale: 1.2 number
Toggle visible: true boolean
Text title: "Hello" string
Color accent: "#a78bfa" CSS color string
Select layout: { type: "select", options: ["stack", "grid"] } Selected string
Segmented select layout: { type: "select", options: ["stack", "grid"], display: "segmented" } Selected string
Image cover: { type: "image", options: ["/cover.jpg"] } URL or data URL
XY pad position: { type: "pad" } { x, y }
Anchor origin: { type: "anchor" } { x, y }
Background backdrop: { type: "background" } BackgroundValue
Spring motion: { type: "spring", visualDuration: 0.3, bounce: 0.2 } TransitionConfig
Easing motion: { type: "easing", duration: 0.3, ease: [0.25, 0.1, 0.25, 1] } TransitionConfig
Action replay: { type: "action" } Calls onAction(path)
Folder shadow: { blur: [12, 0, 40] } Nested values
Field row scale: { _fields: true, x: [1, 0, 3], y: [1, 0, 3] } Nested numbers

Slider tuples use [default, min, max, step?, unit?]; the unit shows after the value. Add _collapsed: true inside a folder to start it closed, or _reset: true to show a reset button beside its title once a value inside changes. The button restores that folder's defaults, nested folders included. Spring, easing and background controls get the same button in their own header, without _reset. Add _hints to explain its controls. Folder metadata is omitted from returned values.

An XY pad takes the same tuples for x and y; add hideLabel: true to let its axis fields fill the row, and dragLabels: true to drag an axis label to change its value.

A segmented select shows a short list as one row of options. Add hideLabel: true to let the options fill the row, and give an option an icon to show in place of its text.

Add _fields: true to a folder of two to four numbers to show them as one row of fields. Drag a field's label to scrub it, or click its value to type. A boolean in the same folder becomes a lock that scales the numbers together. Once a value changes, a reset appears beside the label.

Spring and easing controls share an editor with Easing, Time, and Physics tabs above the curve. Easing mode edits the four Bézier values as fields and has an Intensity slider that strengthens or flattens the curve. The editor's Presets folder lists 40 named curves and 15 named springs, and its header names the one in use; Custom holds the ones you save, kept with a panel's presets when it persists. Hover the ··· in the header to copy a transition, or paste it into another one, mode included. Color controls accept hex, RGB, HSL, OKLCH, and Display P3; the picker edits in each, with a field per channel you can type, drag by its letter, or step with the arrow keys. Image controls support local uploads up to 10 MB; files stay in the browser. A background is a solid color, a gradient (linear, radial, angular, or diamond, with up to eight stops, each with a position, a color, and an opacity), or an image or video; save a gradient to reuse it from any background in the panel. An anchor sets both axes with one click on a 3 × 3 grid: x is "left", "center", or "right", and y is "top", "center", or "bottom".

See the control reference for configuration and editing details.

Conditional controls

Wrap any control or folder in withVisibility to show it only while another control has a given value. It slides in and out as the value changes.

import { withVisibility } from "dialkit";

const values = useDialKit("Card", {
  outline: true,
  outlineWidth: withVisibility([2, 0, 8], { field: "outline", is: true }),
  layout: { type: "select", options: ["stack", "grid", "carousel"] },
  columns: withVisibility([3, 1, 6, 1], { field: "layout", not: ["stack", "carousel"] }),
});

is and not take a value or a list of values. field is the watched control's path, such as "shadow.enabled" inside a folder. Hidden controls keep their values, still appear in the returned values, and ignore their shortcuts.

Panel API

Each panel takes a name, a control config, and optional settings:

useDialKit(name, config, options);    // React and Vue
createDialKit(name, config, options); // Solid, Svelte, and vanilla
Option Purpose
id Share values across mounts using a stable panel ID
persist Save values and presets to browser storage; defaults to false
defaultCollapsed Override the root's initial open state; true starts this panel closed
onAction Receive the path of a clicked action, such as "replay"
shortcuts Assign shortcuts by control path

Update values from code

Use useDialKitController in React or Vue, or createDialKitController in Solid or Svelte. Vanilla's createDialKit already returns a controller.

const dial = useDialKitController("Card", {
  radius: [24, 0, 64],
  shadow: { blur: [12, 0, 40] },
});

// In event handlers:
dial.setValue("radius", 32);
dial.setValues({ radius: 16, shadow: { blur: 8 } });
dial.resetValues();
dial.setOpen(false);

Read dial.values in React, Svelte, and vanilla; dial.values() in Solid; or dial.values.value in Vue. getValues() reads the latest snapshot, and getOpen() reads panel state. resetValues() restores the config defaults and clears the active preset.

Versions and persistence

Save version captures your page, flies the capture into the panel as a thumbnail, and selects the new version. Edits update the selected version automatically; V1 holds the editable base values. Click a thumbnail to switch versions, double-click its name to rename it, or hover it to delete. The grid button zooms the page out to every version; pick one to zoom back into it. Copy puts the current values and an instruction for applying them to your config on the clipboard.

Captures are made in the browser with no dependencies and never leave it. WebGL canvases are supported; web fonts render with system fallbacks. For the classic version menu instead, set versions="menu" on the root (createDialRoot({ versions: "menu" }) in vanilla). Timelines always use the menu. Behavior is the same across React, Solid, Svelte, Vue, and vanilla JavaScript.

In the version menu, a checkmark identifies the current version and New version (New sequence in a timeline) saves and selects a new one. Each saved version has a menu icon that opens Rename, Lock, and Delete; drag a version (long-press it first on touch), or press Alt+Arrow keys on it, to reorder. A locked version keeps its saved values: edits still move the controls but are not saved into it, and it cannot be deleted until unlocked, from the menu or its thumbnail.

Use a stable id to retain values across unmounts. Add persist: true to retain them across page reloads:

const values = useDialKit("Card", {
  radius: [24, 0, 64],
}, { id: "card", persist: true });

Persistence includes values, saved versions (with their names, order, and locks), their thumbnails, the active version, and saved gradients. It uses localStorage with the key dialkit:card in this example. Panel open state stays in memory and is not persisted. See storage options for custom keys, session storage, and saving values without presets.

Panel layout

<DialRoot position="top-right" theme="dark" />

Floating panels are draggable and collapse to an icon. Multiple registered panels appear as sections in one root. Panel, section, and folder headers stay pinned while their contents scroll. Rows fade out under the pinned headers, and floating panels fade at the bottom edge while more controls sit below. CSS scroll-state queries drive the fades, and where a browser lacks them a small scroll listener sets the same states instead, so the fades work in every browser. Set mode="inline" to fill a container in your layout:

<aside style={{ width: 300 }}>
  <DialRoot mode="inline" />
</aside>

In vanilla, use createDialRoot({ mode: "inline", target: container }).

Setting Default Options
position "top-right" "top-right", "top-left", "bottom-right", "bottom-left"
theme "system" "system", "light", "dark"
mode "popover" "popover", "inline"
versions "thumbnails" "thumbnails", "menu"
defaultOpen true Initial root state; a panel's defaultCollapsed takes precedence
accordion false Keep one top-level folder open in each panel
onOpenChange — Callback for root open state; @open-change in Vue
<DialRoot accordion />

In vanilla, use createDialRoot({ accordion: true }). Opening a folder closes the others in its panel, and the first folder without _collapsed starts open. Nested folders keep their own state and get a dashed divider.

Framework roots are hidden in production builds unless productionEnabled is set to true. Vanilla roots are enabled by default; set productionEnabled: false to hide them. This setting controls the editor UI, not the code consuming its values.

Shortcuts

Assign shortcuts to sliders and toggles through panel options:

const values = useDialKit("Card", {
  radius: [24, 0, 64],
  visible: true,
}, {
  shortcuts: {
    radius: { key: "r", mode: "fine" },
    visible: { key: "v" },
  },
});

Hold R and scroll to adjust the radius; press V to toggle visibility. Sliders also support holding a key while dragging or moving the pointer, and scrolling without a key. Shortcuts pause while inputs, buttons, or keyboard controls have focus.

Controls support keyboard navigation without assigned shortcuts. Tab to a slider and use arrow keys to adjust it, or press Enter to edit its number. See the keyboard reference for the full key map and shortcut options.

Hints

Add _hints inside a folder, or at the top of the config, to explain its controls. Hold H over a control to read its hint while the rest of the panel dims:

const values = useDialKit("Card", {
  _hints: {
    radius: "Rounds the corners of the card.",
    shadow: "The shadow under the card.",
  },
  radius: [24, 0, 64],
  shadow: {
    _hints: { blur: "Softens the edge of the shadow." },
    blur: [12, 0, 40],
  },
});

Holding H also explains the focused control, and the hint describes that control for screen readers. The key is ignored while typing in a field or with a modifier held, and a shortcut assigned to H takes precedence. DialKit's own parts, such as the version gallery, the copy button, and the transition editor, have hints with no setup. Until a hint first shows, every open panel ends with a line that points to the key (the panel header has a built-in hint).

Timeline

Define animation clips in code. Use the timeline dock to scrub, move, and resize them, or edit their values and curves.

import { DialTimeline, useDialTimeline } from "dialkit";
import "dialkit/styles.css";

export default function App() {
  const timeline = useDialTimeline("Card", {
    enter: {
      at: 0,
      duration: 0.6,
      from: { y: 24, opacity: 0 },
      to: { y: 0, opacity: 1 },
      transition: { type: "spring", bounce: 0.2 },
    },
  });

  return (
    <>
      <div style={{
        opacity: timeline.enter.current.opacity,
        transform: `translateY(${timeline.enter.current.y}px)`,
      }}>
        Card
      </div>
      <DialTimeline />
    </>
  );
}

Bind clip.current while tuning so your UI follows the playhead. Timelines support sequences, independent property tracks, groups, loops, presets, and persistence.

Adapter Create a timeline Read a clip
React useDialTimeline timeline.enter.current
Solid createDialTimeline timeline().enter.current
Svelte createDialTimeline timeline.enter.current
Vue useDialTimeline timeline.value.enter.current in script
Vanilla createDialTimeline timeline.values.enter.current or subscribe(callback)

For clips that play one after another, put them on a single track. Each bar shows its name (label, or the formatted key) and fills in as the playhead crosses it, and a dragged clip stops at its neighbors:

const timeline = useDialTimeline("Story", {
  intro: { at: 0, duration: 0.6 },
  s42: { at: 0.6, duration: 1.2, label: "Title card" },
}, { track: "single" });

Hold Cmd (Ctrl on Windows and Linux) while dragging a plain clip or its edges to snap to other clips' edges and the paused playhead. The Loop button turns looping on and off.

Mount <DialTimeline /> once, or createDialTimelineRoot() in vanilla. The dock works independently of DialRoot and follows the same production visibility defaults. Use play(), pause(), replay(), and seek(seconds) to control playback.

After tuning, use Copy to apply the settings to your production animation. Replace the sampled current bindings before removing the timeline; hiding the dock alone leaves them running. The timeline guide covers timing, interpolation, loops, and the dock API.

TypeScript and custom layouts

Config and value types are exported from each adapter. Values are inferred from your config; use satisfies DialConfig when defining a config separately.

Individual controls and DialStore are also exported for custom layouts. Vanilla controls use mountSlider(host, props) and similar functions, each returning update(props) and destroy(). See custom layouts.

Contributing

  • Open an issue before submitting a pull request, and reference it in the PR.
  • Keep each PR focused on one feature or fix.
  • Add dependencies only when necessary, and explain why.

Run npm test, npm run typecheck, and npm run build before submitting changes.

Created by Josh Puckett, author of Interface Craft.

MIT License.

About

A library to help you dial in interface parameters of any kind

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages