Library / SDK
joshpuckett/dialkit avatar
joshpuckett/dialkit

DialKit puts a live parameter panel on top of your running interface

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

1,252 stars67 forksTypeScriptMIT

At a glance

What is it?
DialKit is a TypeScript library that lets you expose interface parameters as controls, edit them while the app runs, and read the values back, with adapters for React, Solid, Svelte, Vue and plain JavaScript. It fits a designer or developer tuning spacing, colour and motion in a real app, and it does not fit a production build where the panel and its dependencies should not ship.
Who is it for?
DialKit fits a team that wants to tune spacing, colour and animation timing inside a running application rather than by editing source and reloading, and it does not fit a production bundle that must not carry a dev panel and its animation dependency.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The control definition is a plain object, and the shape is the type

DialKit is a library to help you dial in interface parameters of any kind, and the mechanism is unusually compact. You define controls with an object, keys become labels, and returned values keep the same nesting.

The control table in the README is the whole vocabulary. A slider is written as radius: [24, 0, 64] and returns a number, and the tuple form is [default, min, max, step?], so a step of 2 makes radius: [24, 0, 64, 2]. A bare number infers a slider with a sensible range, so scale: 1.2 returns a number. A toggle is visible: true and returns a boolean. Text is title: "Hello" returning a string. A colour is accent: "#a78bfa" returning a CSS colour string. A select is layout: { type: "select", options: ["stack", "grid"] } returning the selected string. An image is cover: { type: "image", options: ["/cover.jpg"] } returning a URL or data URL. An XY pad is position: { type: "pad" } returning { x, y }.

Then there are the ones that are not numeric parameters. A spring is motion: { type: "spring", visualDuration: 0.3, bounce: 0.2 } and an easing is motion: { type: "easing", duration: 0.3, ease: [0.25, 0.1, 0.25, 1] }, and both return a TransitionConfig rather than a number. An action is replay: { type: "action" }, which calls onAction(path). A folder is shadow: { blur: [12, 0, 40] }, which nests values.

That last row is the one to read carefully. A folder groups related controls, and adding _collapsed: true inside a folder starts it closed. Folder metadata is omitted from returned values, so the folder itself does not appear in the data your app receives.

The design consequence is that a control definition is simultaneously a UI spec and a schema. The same object describes what the panel shows and what your component reads, so there is no separate model to keep in sync.

Values come back in each framework's own idiom, and that is the friction

Each adapter returns values the way its framework expects, and the differences are real enough to plan for.

In React, useDialKit("Card", {...}) returns a values object, and you read values.radius directly. In Solid, createDialKit returns an accessor, so you call values().radius. In Svelte, values are reactive properties, so the template uses values.radius directly, and DialRoot injects the styles, so no CSS import is needed. In Vue, the value is a computed ref, so in script you use values.value.radius while templates unwrap the ref automatically.

Vanilla JavaScript is the outlier and also the cleanest. createDialKit returns a kit, and you subscribe:

js
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`;
});

The subscribe function runs immediately and on panel changes. A snapshot is read with kit.values or kit.getValues(). When you remove the panel from the page you call kit.destroy() and root.destroy().

The React example shows the whole pattern, and it is a pattern worth copying rather than a snippet to paste:

tsx
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 />
    </>
  );
}

One root component, DialRoot, is mounted to display all registered panels. In Next.js App Router, you use DialKit in a client component.

A controller lets you drive values from code, not only from the panel

A panel you can only drag is half a tool. The panel API documents a second entry point for driving values from application code, and this is the part that makes DialKit useful in an automated test or a scripted demo rather than only by hand.

In React or Vue you use useDialKitController, in Solid or Svelte you use createDialKitController, and in vanilla createDialKit already returns a controller. The controller gives you:

tsx
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);

Reading follows the same per-framework pattern as before: dial.values in React, Svelte and vanilla, dial.values() in Solid, and dial.values.value in Vue. getValues() reads the latest snapshot and getOpen() reads the panel state. resetValues() restores the config defaults and clears the active preset.

The options table adds the bits that make it fit a real project. An id lets you share values across mounts using a stable panel ID, so two components bound to the same id read and write the same values. persist saves values and presets to browser storage and defaults to false, which is the right default for a development tool. defaultCollapsed overrides the root's initial open state so a panel can start closed. onAction receives the path of a clicked action, such as "replay", and shortcuts assigns keyboard shortcuts by control path.

That last combination, onAction plus shortcuts, is what turns a control panel into a small remote control for your interface, which is the point of the library.

Motion parameters are first class, and they carry a dependency

The animation controls are the part of DialKit that reaches beyond number tweaking, and they are where the dependency weight appears.

Spring and easing controls share a single editor with Easing, Time and Physics modes. A spring takes visualDuration and bounce, an easing takes a duration and a cubic bezier ease array such as [0.25, 0.1, 0.25, 1], and both produce a TransitionConfig.

The dependency table at the top of the README explains the cost. The React adapter is installed with npm install dialkit motion, the Solid adapter with the same pair, the Vue adapter with npm install dialkit motion motion-v, while Svelte and plain JavaScript need only npm install dialkit. So the animation library is a peer of the framework adapters that use it, and the Vue adapter needs an additional package on top.

Colour controls accept hex, RGB, HSL, OKLCH and Display P3, which is a broader set than most design tools expose in a live panel and reflects the point of the library, which is to tune against what the browser will actually render. Image controls support local uploads up to 10 MB, and the README states the files stay in the browser.

Framework support is stated as React 18+, Solid 1.6+, Svelte 5.8+ and Vue 3.3+. Those floors are worth checking against your app, because a Svelte 5 requirement in particular means a Svelte 4 project cannot use the adapter at all.

The vanilla adapter is described as having no runtime dependencies, and the package manifest agrees in spirit: it lists the vanilla entry separately and the README notes that the vanilla build is available as a browser global.

The no-bundler path is a real deployment option, not a footnote

The plain JavaScript adapter is documented in enough detail to be used without a bundler at all, and that path is worth understanding because it changes what the library costs you.

You copy two files from the package into your project, dist/vanilla/browser.global.js and dist/vanilla/styles.css, and reference them with a plain script tag:

html
<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, and the README warns to use one entry consistently so the root and the controls share the same store. That warning is a real constraint rather than a formality: the panel and the values are two objects that must be talking to the same store instance, and mixing the global build with the module build gives you two stores and a panel that does not drive the app.

The vanilla adapter being dependency-free makes it a reasonable fit for a static site, a prototype, or an embedded widget where adding a bundler and an animation library would be the larger cost.

Where DialKit is the wrong tool

The first wrong-tool case is a production bundle. This is a development instrument. The control definitions live in your component tree, the values feed your styles, and the panel is a floating overlay. None of that belongs in a build that ships to users, and the library does not describe a production-mode switch in the README. The persist option saves to browser storage, which is a convenience for a designer and a privacy consideration in production, so the usual approach is to strip the panel at build time, which means the control definitions have to live somewhere you can exclude.

The second is a case where the parameters are not the problem. If the issue is that a design system has the wrong scale, a live panel will let you discover the right number and then you still have to put that number in the design system. DialKit does not write back to your token source. The values it returns are runtime values in your app, and the README does not describe an export path from the panel into a token file, so a designer who finds the right spacing still has a manual step.

The third is a comparison workflow. The tagline mentions comparing versions, and the panel API supports sharing values across mounts with an id, and presets with persistence. But the mechanism for comparing two states at once is not documented in the README, so a team whose actual need is a side-by-side visual diff of two parameter sets should check the reference documentation in docs/reference.md before assuming it works.

The fourth is the animation dependency. For the React, Solid and Vue adapters, motion is required, and for Vue, motion-v as well. A project with a strict bundle budget or a policy against animation libraries has to weigh that before adding a development panel, even if the panel is stripped from production.

The project is MIT licensed, at version 2.0.1, with the last push on 2026-09-25 and no GitHub releases published.

Editorial conclusion

DialKit fits a team that wants to tune spacing, colour and animation timing inside a running application rather than by editing source and reloading, and it does not fit a production bundle that must not carry a dev panel and its animation dependency. Before integrating it, decide how the panel will be excluded from production, because the control definitions live in your component tree, and check the optional dependency cost, since the React, Solid and Vue adapters each pull in motion and the Vue adapter additionally pulls in motion-v.

Frequently asked questions

How do I install DialKit?

The React and Solid adapters are installed with npm install dialkit motion, the Vue adapter with npm install dialkit motion motion-v, and the Svelte and plain JavaScript adapters with npm install dialkit. You import from dialkit, dialkit/solid, dialkit/svelte, dialkit/vue or dialkit/vanilla depending on the adapter.

How do I define a control in DialKit?

Controls are defined in a plain object where keys become labels and returned values keep the same nesting. A slider is [default, min, max, step?], a toggle is a boolean, a colour is a CSS colour string, a select uses { type: "select", options: [...] }, an XY pad uses { type: "pad" }, and a folder groups nested values and can start closed with _collapsed: true.

Can I change DialKit values from code?

Yes. Use useDialKitController in React or Vue and createDialKitController in Solid or Svelte, and in vanilla createDialKit already returns a controller. The controller exposes setValue, setValues, resetValues and setOpen, along with getValues and getOpen for reading state.

Can DialKit be used without a bundler?

Yes. The vanilla adapter ships as a browser global at dist/vanilla/browser.global.js with dist/vanilla/styles.css, and both work offline. You create the root with DialKit.createDialRoot() and the kit with DialKit.createDialKit, and you should use one entry point consistently so the root and the controls share the same store.

Official sources

  1. Issues
  2. joshpuckett/dialkit on GitHub
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/joshpuckett-dialkit.svg)](https://hysenlabs.com/projects/joshpuckett-dialkit)

Was this useful?