UI reference
The shared stylesheet behind every *.nitaimaarek.com site. Layer one styles
bare HTML, so a page with no classes is already themed. Layer two adds .ui-*
components. Everything below is documented with the markup that produces it.
<link rel="stylesheet" href="https://framework.nitaimaarek.com/ui.css">
<script src="https://framework.nitaimaarek.com/ui.js"></script> <!-- optional -->
Same 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 the 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:30". ui.jsis optional; everything degrades to working HTML without it.- Behaviour attaches through
data-ui-*attributes, never through classes.
Tokens
:rootEvery colour, corner and gap derives from these. One value changes a whole site;
changing the default in ui.css changes the network. Drag anything below.
| Token | Default | Drives |
|---|---|---|
--ui-hue --ui-sat | 168 · 42% | Accent: links, focus, checked controls, active markers |
--ui-tint --ui-tint-sat | 40 · 4% | Neutral cast of every surface, border and text tone |
--ui-radius | 6px | Every corner in the framework, via -xs ×.4, -sm ×.7, -lg ×1.7 |
--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-mono | Face used for h1–h4; set to --ui-font for sans headings |
--ui-width --ui-measure | 1080px · 74ch | Content column; prose line length |
--ui-solid --ui-solid-ink | ink · page | Filled surfaces: primary button, solid badge |
--ui-bg --ui-bg-alt --ui-surface --ui-surface-2 | derived | Backgrounds, back to front |
--ui-line --ui-line-soft | derived | Borders: visible, hairline |
--ui-text --ui-muted --ui-faint | derived | Text emphasis levels |
--ui-ok --ui-warn --ui-bad --ui-info | — | Status colours, each with a -wash background |
--ui-fast --ui-mid --ui-slow | 90 · 160 · 280ms | Motion, disabled under prefers-reduced-motion |
<html style="--ui-hue:30"> <!-- recolour a site -->
<html style="--ui-radius:0px; --ui-density:.85"> <!-- square and compact -->
<html style="--ui-sat:0%"> <!-- monochrome -->
<html style="--ui-display:var(--ui-font)"> <!-- sans headings -->
<html data-theme="light"> <!-- force a mode -->
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 a hairline 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 -->
<small>Small print.</small>
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>
<input type="submit" value="Submit">
<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>
<div class="ui-scroll-x">…</div>
Buttons
.ui-btnWorks on <button> and <a>. Primary is ink rather than
colour; --accent is there when a page wants the hue instead.
aria-busy="true" swaps the label for a spinner and blocks clicks.
<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">
<button class="ui-btn">L</button><button class="ui-btn">R</button>
</span>
Segmented
.ui-segmentedMutually exclusive choice. With data-ui-segmented, ui.js
moves aria-pressed.
<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-groupLabel, control, hint or error. The controls themselves stay bare elements.
<div class="ui-field">
<label for="x">Listen address</label>
<input id="x" type="text" placeholder="127.0.0.1:8102">
<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" type="text" readonly>
<button class="ui-btn" data-ui-copy="#v">Copy</button>
</div>
<textarea data-ui-autosize></textarea>
Toggles
.ui-switch .ui-check .ui-radioWrappers that align a real input with its label. The inputs stay native.
<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-sliderWraps <input type="range"> with a label and a live value;
ui.js paints the filled track.
<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 class="ui-slider__ticks"><span>1</span><span>16</span></div>
</div>
Searchable select
data-ui-selectUpgrades a real <select> — still submits with the form, still works
without JS. Search appears past seven options.
<select data-ui-select name="service">
<option>jetson-studio</option>
<option>media-studio</option>
</select>
<!-- data-ui-search="off" suppresses the search box -->
Dropdown
.ui-dropdown .ui-menuTrigger carries data-ui-dropdown; the menu is its sibling. Closes on
outside click or Esc and flips at the viewport edge.
<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-tabsA tab's data-ui-tab is the id of its panel. Arrow keys move between tabs.
hidden 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>
Pagination
.ui-pagination<nav class="ui-pagination">
<a href="?p=1">1</a>
<span aria-current="page">2</span>
<a href="?p=3">3</a>
</nav>
Breadcrumb
.ui-breadcrumb<ol class="ui-breadcrumb">
<li><a href="/">root</a></li>
<li>media-studio</li>
</ol>
Card
.ui-cardA surface. Use <a class="ui-card"> for a clickable one.
media-studio
upyt-dlp + ffmpeg, port 8092.
<div class="ui-card"> <!-- --flat --hover --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-dotMonospace status label. .ui-dot is the bullet; --pulse for live state.
<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="">
<!-- --round --lg -->
Table
<table>No classes. Wrap in .ui-scroll-x when it can outgrow the column.
| Service | Port | State |
|---|---|---|
| jetson-studio | 8091 | up |
| media-studio | 8092 | up |
| luac-studio | 8102 | restarting |
<div class="ui-scroll-x">
<table>
<thead><tr><th>Service</th><th>Port</th></tr></thead>
<tbody><tr><td>jetson-studio</td><td>8091</td></tr></tbody>
</table>
</div>
Alert
.ui-alertInline and persistent. For transient feedback use a toast.
<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()JS only. Stacks bottom-right, auto-dismisses, returns a handle with .close().
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
});
const t = UI.toast("Working…", { duration: 1e9 }); t.close();
Modal
.ui-modal · UI.modal()A native <dialog>, so focus trapping and Esc come from the
browser. The JS form returns a Promise.
<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 }); // → true | false
Tooltip
data-ui-tipPure CSS. Shows on hover and on keyboard focus.
<button class="ui-btn" data-ui-tip="Explanation">Hover</button>
<!-- data-ui-tip-pos="bottom" | "right" -->
Progress
.ui-progress .ui-spinnerBare <progress> when the value is known; .ui-progress when
you drive the width yourself.
<div class="ui-progress">
<div class="ui-progress__bar" style="width:42%"></div>
</div>
<div class="ui-progress ui-progress--indeterminate">
<div class="ui-progress__bar"></div>
</div>
<span class="ui-spinner"></span> <!-- --lg -->
Skeleton
.ui-skeleton<div class="ui-skeleton" style="width:40%"></div>
<div class="ui-skeleton" style="height:120px"></div>
Utilities
Deliberately few — use tokens and components before reaching here.
| Class | Effect |
|---|---|
.ui-muted .ui-faint .ui-accent | Text colour by emphasis |
.ui-mono .ui-small .ui-big .ui-bold | Type adjustments |
.ui-center .ui-right | Text alignment |
.ui-truncate .ui-nowrap | Single-line clamp; no wrapping |
.ui-mt-0…3 .ui-mb-0…3 | Top and bottom margin steps |
.ui-full | width: 100% |
.ui-hide .ui-hide-sm | Hide always; hide below 640px |
.ui-sr | Screen-reader only |
.ui-scroll-x | Horizontal scroll container |
.ui-reveal | Fades in when scrolled into view |
body.ui-wide | Drops the centred column cap |
JS API
window.UIAuto-initialises on load and re-scans on DOM mutation, so injected markup works too.
Load ui.js in <head> without defer to avoid a theme flash.
UI.toast(msg, "ok"|"warn"|"bad" | {type, title, duration}) → {close()}
UI.modal({title, body|html, size:"sm"|"lg", dismissable,
buttons:[{label, variant, value}]}) → Promise<value>
UI.confirm(msg, {danger, okLabel, cancelLabel}) → Promise<bool>
UI.alert(msg) → Promise
UI.theme() // "dark" | "light"
UI.theme("light"|"dark"|"auto") // persists for all *.nitaimaarek.com
UI.hue(30) // recolour + persist
UI.copy(text) → Promise
UI.on("click", ".ui-card", fn) // delegated listener
UI.init(scope) // rescan a subtree by hand
UI.$(sel) / UI.$$(sel) // querySelector / querySelectorAll → Array
Attributes
data-ui-*| Attribute | On | Behaviour |
|---|---|---|
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 | strip | Wires the tabs inside it |
data-ui-tab="id" | tab | The panel this tab shows |
data-ui-select | select | Searchable dropdown; stays a real select |
data-ui-search="off" | select | Suppresses the search box |
data-ui-copy="text|#id" | any | Copies to clipboard and toasts |
data-ui-copy-msg | any | Overrides the toast text |
data-ui-copy="off" | pre | No copy button on that block |
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 |
data-ui-segmented | segmented | Moves aria-pressed on click |
aria-busy="true" | .ui-btn | Spinner, hidden label, clicks blocked |
data-theme | html | light / dark; absent follows the OS |