nitaimaarek/ui
Frosted components and a cursor-reactive background, behind a single link tag. Bare HTML is styled with no markup changes.
Install
2 linesThe CSS stands alone. ui.js adds behaviour and the
background loader; ui-bg.js is pulled in on demand and can be preloaded.
<link rel="stylesheet" href="https://framework.nitaimaarek.com/ui.css">
<script src="https://framework.nitaimaarek.com/ui.js"></script>
<body data-ui-bg="mesh"> <!-- optional: turn the background on -->
Whole reference as plain markdown, one request:
curl framework.nitaimaarek.com/ui.md
Rules
- Bare HTML is already styled. Add the
<link>and change nothing else. - Reach for
.ui-*only when a page needs a widget plain elements don't cover. - Never write per-site CSS or a second palette. Fix
ui.cssso every site benefits. - Restyle by setting tokens on
<html>, e.g.style="--ui-hue:190". - Behaviour attaches through
data-ui-*attributes, never through classes.
Tokens
:rootEvery colour, corner, gap and blur derives from these. Drag anything — the whole page reacts live, including the background.
| Token | Default | Drives |
|---|---|---|
--ui-hue --ui-sat | 265 · 70% | Accent: links, focus, checked controls, background particles |
--ui-tint --ui-tint-sat | 240 · 6% | Neutral cast of every surface, border and text tone |
--ui-radius | 14px | Every corner, via -xs ×.35, -sm ×.65, -lg ×1.35 |
--ui-blur | 20px | Frosted-glass strength on panels; 0 makes them flat |
--ui-lift | -2px | How far buttons and cards rise on hover |
--ui-density | 1 | Multiplies the spacing scale --ui-1…--ui-8 |
--ui-fs --ui-lh | 15px · 1.6 | Root type size and line height |
--ui-display | --ui-font | Face for h1–h4 and .ui-display |
--ui-glass --ui-glass-2 | derived | Panel fills — translucent so the background reads through |
--ui-glass-line --ui-glass-hi | derived | Hairline border and the inset top highlight |
--ui-solid --ui-solid-ink | ink · page | Filled surfaces: primary button, solid badge |
--ui-ok --ui-warn --ui-bad --ui-info | — | Status colours, each with a -wash background |
--ui-fast --ui-mid --ui-slow | 150 · 240 · 420ms | Motion, disabled under prefers-reduced-motion |
<html style="--ui-hue:190"> <!-- recolour a site -->
<html style="--ui-radius:0px; --ui-blur:0px"> <!-- square, no glass -->
<html style="--ui-display:var(--ui-mono)"> <!-- mono headings -->
<html data-theme="light"> <!-- force a mode -->
Interactive background
ui-bg.js · UI.bg()One fixed canvas behind the page. Five themes, all cursor-reactive — move the
mouse, and click while ripple is on. Your pick is remembered across every
subdomain.
| Theme | Looks like | Reacts to the cursor by |
|---|---|---|
stars | Depth-layered star field, near stars brighter | Drifting toward it — near layers lead, far layers lag |
mesh | Linked constellation | Shouldering points aside as it passes |
aurora | Slow blurred colour fields | Parallax drift against the pointer |
grid | Dot lattice, slowly breathing | Dots swell and light up under the pointer |
ripple | Dot field | Swelling under the cursor; clicks send out waves |
<body data-ui-bg="stars"> <!-- markup -->
UI.bg("aurora") <!-- runtime, persists -->
UI.bg(false) <!-- stop, remove canvas -->
UI.bg("mesh", { density: 1.4, speed: .6, opacity: .8, color: "#7dd3c0" })
<!-- tuning as attributes -->
<body data-ui-bg="stars"
data-ui-bg-density="1.4" <!-- particle count multiplier -->
data-ui-bg-speed=".6" <!-- drift speed -->
data-ui-bg-opacity=".8"
data-ui-bg-link="150" <!-- link distance, px -->
data-ui-bg-color="#7dd3c0" <!-- defaults to --ui-accent -->
data-ui-bg-interactive="off">
<!-- any element can switch it -->
<button class="ui-btn" data-ui-bg-set="grid">Grid</button>
prefers-reduced-motion it renders one static frame and never
animates.Base elements
no classesWhat an existing page gets from the <link> alone.
Heading
Body text with a link, bold, italic,
inline code, Ctrl+C and a highlight.
- List items
- with quiet markers
Quotes sit behind an accent rule.
Micro heading
Small print, muted.
<h4>Heading</h4>
<p>Body with a <a>link</a>, <strong>bold</strong>,
<code>inline code</code>, <kbd>Ctrl</kbd>, <mark>mark</mark>.</p>
<ul><li>List items</li></ul>
<blockquote>Quotes.</blockquote>
<h6>Micro heading</h6> <!-- h5/h6 are mono eyebrows -->
Native details is an accordion
No class, no JS.
<label for="x">Label</label>
<input id="x" type="text" placeholder="Bare input">
<select><option>Rust</option></select>
<label><input type="checkbox"> Checkbox</label>
<input type="range" min="0" max="10" value="7">
<button>Button</button>
<details><summary>Title</summary><p>Body</p></details>
Layout
.ui-row .ui-stack .ui-gridFlex and grid primitives. .ui-spacer pushes what follows to the far edge.
<div class="ui-row">…<span class="ui-spacer"></span>…</div>
<!-- --between --end --center --top --nowrap -->
<div class="ui-stack">…</div> <!-- --sm --lg -->
<div class="ui-grid ui-grid--4">…</div> <!-- --2 --4 -->
<div class="ui-container ui-container--narrow">…</div>
<div class="ui-divider"></div>
Buttons
.ui-btnWorks on <button> and <a>. Every one lifts on
hover and clicks with a cue. Primary fills with ink; --accent uses the hue.
<button class="ui-btn ui-btn--primary">Primary</button>
<a class="ui-btn ui-btn--outline" href="/x">Outline</a>
<!-- variants: --primary --accent --outline --ghost
--danger --success --link -->
<!-- sizes: --sm --lg --block --pill --icon -->
<button class="ui-btn" aria-busy="true">Saving</button>
<button class="ui-btn" data-ui-confirm="Sure?">Delete</button>
<span class="ui-btn-group">…</span>
Segmented
.ui-segmented<span class="ui-segmented" data-ui-segmented>
<button aria-pressed="true">1h</button>
<button aria-pressed="false">24h</button>
</span>
Fields
.ui-field .ui-input-group<div class="ui-field">
<label for="x">Listen address</label>
<input id="x" type="text">
<span class="ui-field__hint">Host and port.</span>
</div>
<div class="ui-field ui-field--invalid">
… <span class="ui-field__error">Not valid.</span>
</div>
<div class="ui-input-group">
<input id="v" readonly>
<button class="ui-btn" data-ui-copy="#v">Copy</button>
</div>
<textarea data-ui-autosize></textarea>
Toggles
.ui-switch .ui-check .ui-radio<label class="ui-switch"><input type="checkbox" checked> Stream logs</label>
<label class="ui-check"><input type="checkbox"> Checkbox</label>
<label class="ui-radio"><input type="radio" name="d"> Fast</label>
Slider
.ui-slider<div class="ui-slider">
<div class="ui-slider__head">
<label for="s">Workers</label>
<span class="ui-slider__value">6</span>
</div>
<input id="s" type="range" min="1" max="16" value="6"
data-ui-prefix="~" data-ui-suffix=" kbps">
</div>
Searchable select
data-ui-selectUpgrades a real <select> — still submits with the form. Search
appears past seven options.
<select data-ui-select name="service">
<option>jetson-studio</option>
</select>
<!-- data-ui-search="off" suppresses the search box -->
Dropdown
.ui-dropdown .ui-menu<div class="ui-dropdown">
<button class="ui-btn" data-ui-dropdown aria-expanded="false">Actions</button>
<div class="ui-menu"> <!-- .ui-menu--right aligns right -->
<div class="ui-menu-label">Service</div>
<button class="ui-menu-item">Restart</button>
<div class="ui-menu-sep"></div>
<a class="ui-menu-item" href="/_status">Open</a>
</div>
</div>
Tabs
data-ui-tabshidden attribute.<div class="ui-tabs" data-ui-tabs>
<button class="ui-tab" data-ui-tab="t1" aria-selected="true">Source</button>
<button class="ui-tab" data-ui-tab="t2">Bytecode</button>
</div>
<div class="ui-tabpanel" id="t1">…</div>
<div class="ui-tabpanel" id="t2" hidden>…</div>
Card
.ui-cardmedia-studio
upyt-dlp + ffmpeg, port 8092.
<div class="ui-card ui-card--hover"> <!-- --flat --accent -->
<h4 class="ui-card__title">media-studio</h4>
<p class="ui-card__sub">yt-dlp + ffmpeg, port 8092.</p>
<div class="ui-card__foot">…</div>
</div>
<a class="ui-card ui-card--hover" href="/x">Clickable</a>
Stat
.ui-stat<div class="ui-stat">
<span class="ui-stat__value">18</span>
<span class="ui-stat__label">services</span>
</div>
Badge
.ui-badge .ui-dot<span class="ui-badge ui-badge--ok">ok</span>
<!-- --accent --ok --warn --bad --info --solid --pill -->
<span class="ui-badge ui-badge--ok">
<span class="ui-dot ui-dot--pulse"></span> running
</span>
Chip
.ui-chip<span class="ui-chip">wasm
<button class="ui-chip__x" aria-label="Remove">×</button>
</span>
Avatar
.ui-avatar<span class="ui-avatar">NM</span>
<img class="ui-avatar ui-avatar--round" src="/me.png" alt="">
Table
<table>| Service | Port | State |
|---|---|---|
| jetson-studio | 8091 | up |
| media-studio | 8092 | up |
| luac-studio | 8102 | restarting |
<div class="ui-scroll-x">
<table>…</table>
</div>
Alert
.ui-alert<div class="ui-alert ui-alert--ok">
<div><span class="ui-alert__title">Deployed.</span> All checks passed.</div>
</div>
<!-- --ok --warn --bad --info -->
Toast
UI.toast()UI.toast("Saved to disk");
UI.toast("Service is back up", "ok"); // ok | warn | bad
UI.toast("Connection lost", {
type: "bad", title: "Error", duration: 6000
});
Modal
.ui-modal · UI.modal()<button class="ui-btn" data-ui-modal="#m">Open</button>
<dialog class="ui-modal" id="m"><div class="ui-modal__box">
<div class="ui-modal__head">
<h3 class="ui-modal__title">Title</h3>
<button class="ui-modal__x" data-ui-close>×</button>
</div>
<p>Body</p>
<div class="ui-modal__foot">
<button class="ui-btn" data-ui-close>Cancel</button>
</div>
</div></dialog>
await UI.confirm("Restart?", { danger: true });
Tooltip
data-ui-tip<button class="ui-btn" data-ui-tip="Explanation">Hover</button>
<!-- data-ui-tip-pos="bottom" | "right" -->
Progress
.ui-progress .ui-spinner .ui-skeleton<div class="ui-progress">
<div class="ui-progress__bar" style="width:42%"></div>
</div>
<div class="ui-progress ui-progress--indeterminate">…</div>
<span class="ui-spinner"></span> <!-- --lg -->
<div class="ui-skeleton" style="width:45%"></div>
Utilities
| Class | Effect |
|---|---|
.ui-display | Oversized hero type; .ui-dim greys part of it |
.ui-glass | Applies the frosted-panel recipe to anything |
.ui-muted .ui-faint .ui-accent | Text colour by emphasis |
.ui-mono .ui-small .ui-big .ui-bold | Type adjustments |
.ui-center .ui-right .ui-truncate .ui-nowrap | Alignment and clamping |
.ui-mt-0…3 .ui-mb-0…3 .ui-full | Margin steps, full width |
.ui-hide .ui-hide-sm .ui-sr | Hide always, below 640px, visually |
.ui-scroll-x .ui-reveal | Scroll container; fade in on scroll |
body.ui-wide | Drops the centred column cap |
JS API
window.UIUI.toast(msg, "ok"|"warn"|"bad" | {type, title, duration}) → {close()}
UI.modal({title, body|html, size, dismissable, buttons}) → Promise<value>
UI.confirm(msg, {danger, okLabel, cancelLabel}) → Promise<bool>
UI.alert(msg) → Promise
UI.theme() / UI.theme("light"|"dark"|"auto") // persists per subdomain
UI.hue(190) // recolour + persist
UI.bg("stars"|"mesh"|"aurora"|"grid"|"ripple"|false, opts)
UI.copy(text) → Promise
UI.on("click", ".ui-card", fn) // delegated listener
UI.init(scope) · UI.$(sel) · UI.$$(sel)
Attributes
data-ui-*| Attribute | On | Behaviour |
|---|---|---|
data-ui-bg="theme" | body | Starts the background; -density -speed -opacity -link -color -interactive tune it |
data-ui-bg-set="theme" | any | Switches the background on click |
data-ui-dropdown | trigger | Toggles the sibling .ui-menu |
data-ui-modal="#id" | any | Opens that <dialog> |
data-ui-close | any | Closes the enclosing dialog |
data-ui-tabs · data-ui-tab="id" | strip · tab | Tab wiring |
data-ui-select | select | Searchable dropdown; stays a real select |
data-ui-copy="text|#id" | any | Copies to clipboard and toasts |
data-ui-confirm="msg" | any | Asks before the click proceeds |
data-ui-theme-toggle | any | Cycles light and dark |
data-ui-tip="text" | any | Tooltip; data-ui-tip-pos="bottom|right" |
data-ui-autosize | textarea | Grows with content |
data-ui-prefix data-ui-suffix | range | Units around the live value |
aria-busy="true" | .ui-btn | Spinner, hidden label, clicks blocked |
data-theme | html | light / dark; absent follows the OS |