Jelly UI is a zero-runtime-dependency Web Components library for soft,
tactile product interfaces. It combines real form controls — native focus,
keyboard behavior, standard events and FormData participation — with
soft-body physics surfaces, so every press, toggle and drag settles with a
wobble.
It is written in strict TypeScript, bundled with Vite into a single
ESM file (dist/jelly.js) plus type declarations, and covered by
real-browser tests (Vitest + Playwright). Consumers still get the whole
library in one <script> tag — the build is ours, not theirs.
<script type="module" src="https://jelly-ui.com/package.js"></script>
<jelly-theme mode="auto">
<jelly-button variant="mint">Publish</jelly-button>
<jelly-input name="email" type="email" placeholder="you@example.com"></jelly-input>
<jelly-switch checked>Notifications</jelly-switch>
</jelly-theme>package.js is the public entry point; it re-exports the built bundle
(dist/jelly.js), which registers every component and carries every named
export for JavaScript consumers. Self-hosting? Serve package.js next to a
dist/ you built with npm run build.
npm install
npm run build # emit dist/jelly.js + dist/jelly.d.ts
npm run docs # generate the docs data (docs/content/data.js, untracked)
npm run serve # static server → http://localhost:4173For an inner loop, run the watch build and the server side by side — the bundle rebuilds on every save; reload the page:
npm run dev # vite build --watch (rebuilds dist/ on change)
npm run serve # in a second terminal/— the showcase: scroll through every component live, with a sticky preview stage/api— the API reference: every attribute, property, event, slot and token
| Script | What it does |
|---|---|
npm run build |
Bundle src/jelly.ts → dist/jelly.js (+ .d.ts) with Vite |
npm run build:site |
Build + docs, then assemble the Pages site into _site/ |
npm run dev |
Rebuild the bundle on every change (vite build --watch) |
npm run serve |
Serve the static site with the dependency-free dev server |
npm run typecheck |
tsc --noEmit in strict mode |
npm test |
Run the real-browser test suite (Vitest + Playwright Chromium) |
npm run docs |
Regenerate the API docs data from the component sources |
docs/showcase/index.html Showcase page: one interactive playground per component
docs/showcase/index.js Showcase renderer (controls, live code, scroll choreography)
docs/showcase/playgrounds.js Showcase demo config (preview markup, controls, bindings)
docs/api/index.html API reference page: aligned tables for every attribute/event/token
docs/api/index.js Reference renderer (sidebar and aligned API tables)
docs/shared/common.js Shared docs behavior (theme + motion toggles)
docs/content/content.js Curated component docs (edit this to document components)
docs/content/data.js Generated docs data the pages render (do not edit)
docs/lottie/index.js Lazy-loads the hero + outro Lottie illustrations
docs/{shared/site,showcase/showcase,api/reference}.css Page chrome and per-page styles
package.js Public entry point (re-exports the built bundle, dist/jelly.js)
dist/jelly.js Built ESM bundle (generated by `npm run build`; gitignored)
_site/ Assembled Pages site (generated by `npm run build:site`; gitignored)
scripts/build-site.mjs Assembles _site/ — builds, runs docs, then copies just the site
scripts/build-docs-data.mjs Reconciles docs/content/content.js against the manifest → data.js
vite.config.ts Library-mode build (single ESM bundle + bundled .d.ts)
vitest.config.ts Real-browser test runner (Playwright Chromium)
tsconfig.json Strict TypeScript configuration
custom-elements-manifest.config.mjs Manifest analyzer config (docs source of truth)
server.mjs Tiny dependency-free static server (mirrors the built site)
src/jelly.ts Barrel module — registers every component
src/theme/ Design tokens (light + dark), variants, theme runtime
src/core/ Soft-body physics: config, the JellyBody simulation, the shared loop
src/element/ Base class for canvas-backed components (+ its shape/paint types)
src/anchor/ Overlay helpers (positioning, scroll lock, modality, motion)
src/utilities/ Shared utilities (events, ids, RTL, keyboard, size, motion)
src/icons/ Icon renderer (index.ts) over src/icons/*.svg (typed IconName union)
src/styles/ Shared CSS partials (base + variants), imported ?inline
src/testing/ Test-only helpers (mount / settle) shared by the component tests
src/components/ One directory per component: <name>/index.ts + <name>.css + <name>.test.ts
.github/workflows/ GitHub Pages deployment (builds, tests, then publishes _site/)
| Area | Components |
|---|---|
| Theming | jelly-theme |
| Actions | jelly-button, jelly-icon-button |
| Forms | jelly-input, jelly-textarea, jelly-select + jelly-option, jelly-checkbox, jelly-radio + jelly-radio-group, jelly-switch, jelly-slider, jelly-range, jelly-segmented + jelly-segment, jelly-otp, jelly-label |
| Feedback | jelly-alert, jelly-badge, jelly-progress, jelly-spinner, jelly-skeleton, jellyToast() + jelly-toaster |
| Surfaces | jelly-card, jelly-chip, jelly-kbd, jelly-divider, jelly-collapsible, jelly-accordion |
| Navigation | jelly-tabs + jelly-tab-panel, jelly-breadcrumbs, jelly-pagination, jelly-resizable |
| Overlays | jelly-tooltip, jelly-popover, jelly-menu + jelly-menu-item, jelly-dialog, jelly-drawer |
The full API reference — every attribute, property, event, slot, shadow part,
CSS custom property and keyboard map — lives on the API reference
(/api), with live examples on the showcase (/), both
generated from the component sources (see Documenting components).
All colors flow through CSS custom properties (--jelly-color-*) defined in
src/theme/, with verified WCAG AA light and dark values. Three ways to
theme:
1. The theme provider — scope a mode (or accent) to any subtree:
<jelly-theme mode="dark">
<jelly-card>Dark-token subtree</jelly-card>
</jelly-theme>
<jelly-theme mode="auto" accent="#7C3AED">
<jelly-slider></jelly-slider>
</jelly-theme>mode="auto" (the default) follows the operating system.
2. Document-wide mode — from JavaScript:
import { setThemeMode } from './package.js';
setThemeMode('dark'); // 'light' | 'dark' | 'auto'3. Token overrides — any token, any scope, plain CSS:
:root {
--jelly-color-background-accent: #7C3AED;
--jelly-color-background-azure: #4F46E5;
}Per-instance overrides still work too:
<jelly-button style="--jelly-fill: #5A5A5A; --jelly-label: #FFFFFF">Custom</jelly-button>Fillable components accept variant="white | rose | amber | azure | mint | platinum | graphite". Variants map to theme tokens, so they adapt to dark
mode automatically. Without a variant, filled controls use the shared accent
fill (--jelly-color-background-accent / --jelly-color-foreground-on-accent).
| Property | Use |
|---|---|
--jelly-color-* |
The global token set (surfaces, text, hues, ring, accent) |
--jelly-font-display / -text / -mono |
The three shared font stacks |
--jelly-fill |
Jelly surface fill of one instance |
--jelly-label |
Text/label color of one instance |
--jelly-accent |
Active tint for slider, select, progress, … |
--jelly-on |
Active tint for checkbox, radio, switch, chip |
--jelly-ring |
Focus ring color for canvas-backed controls |
--jelly-ring-width / -gap |
Focus ring thickness and offset |
--jelly-shadow-raised / -overlay |
Elevation shadows on cards and overlays |
Each component also exposes its own --jelly-<component>-* knobs (padding,
radius, fonts…) — see its docs section.
Dark mode is token-driven: by default components follow
prefers-color-scheme; force it with <jelly-theme mode="dark">, the
data-jelly-mode="dark" attribute on <html>, or setThemeMode('dark').
Canvas-painted surfaces listen for theme changes and repaint themselves.
Components use CSS logical properties and direction-aware interaction: set
dir="rtl" on any ancestor and layout mirrors, horizontal arrow keys flip to
match reading direction (sliders, radios, segmented controls, resizable
splitters), switches travel toward the inline-end, overlays accept logical
placement="start|end" and toasts slide in from the inline-end. One-time-code
inputs stay left-to-right by design, like the codes themselves.
- WCAG AA contrast for the text, surface and variant tokens in both light and dark.
- Native focusable controls inside shadow roots; visible (jiggling!) focus
rings, with
forced-colorsfallbacks. - Correct ARIA patterns: combobox/listbox select, tablist tabs, menu with roving focus, modal dialogs with scroll lock + background inerting + focus restore, window-splitter resizable panes.
- Full keyboard support, including Shift+Arrow fast-stepping on sliders.
prefers-reduced-motionswaps wobble for calm, instant feedback.- Form controls participate in
FormDataviaElementInternalsand accept alabelattribute for accessible naming.
Form controls behave like native ones and emit composed events:
const plan = document.querySelector('jelly-select');
plan.addEventListener('change', () => console.log(plan.value));
const data = new FormData(myForm); // jelly controls are includedButtons with type="submit" / type="reset" drive the closest light-DOM
<form>. Interaction events stay unprefixed: chips emit cancelable remove,
collapsibles emit toggle, menus emit select, and OTP emits change on a
complete code (plus a complete alias that carries the value).
The docs are automatic where it counts. Every component's public surface —
attributes, events, slots, shadow parts — is read from a Custom Elements
Manifest that cem analyze generates from the TypeScript sources and their
JSDoc. Curated prose (summaries, descriptions, examples, keyboard maps) lives
in docs/content/content.js. Running npm run docs reconciles the two and writes
the generated docs/content/data.js the pages render — warning aloud if a component
gains or loses an attribute/event/slot/part the docs don't reflect, so the reference
can't silently drift from the code. docs/showcase/playgrounds.js configures each
component's live demo: the preview markup, the values the scroll-through
showcase steps and the events that write back into the table.
The full recipe — fixing a typo, adding a component, changing a demo — is
in docs/README.md.
.github/workflows/deploy-pages.yml type-checks and tests, then runs
npm run build:site to assemble a clean _site/ (the library bundle, the docs
assets and the two pages — the API page as api/index.html, so it serves at
/api) and publishes only that on every push to main (or manually from the
Actions tab). One-time setup: Settings → Pages → Source → "GitHub Actions".
Each canvas-backed component owns a JellyBody: a closed ring of
spring-coupled membrane points around a rounded rectangle, with pressure-based
volume preservation, pointer-following press targets, tilt and perspective.
A single shared requestAnimationFrame loop steps every live body — in
fixed-size substeps so the simulation stays stable at any frame rate — and
parks itself when everything is at rest, so an idle page costs nothing. If a
body ever produces a non-finite number it resets itself instead of wedging.
prefers-reduced-motion bypasses the impulses entirely.
Current Chrome, Edge, Safari and Firefox. Jelly UI leans on Custom Elements,
Shadow DOM, ElementInternals, ResizeObserver, Canvas 2D, color-mix(),
and CSS logical properties.
MIT — see LICENSE. Bundles Fluent System Icons
(also MIT) as standalone SVGs in src/icons/, rendered by src/icons/index.ts.