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
  1. Bare HTML is already styled. Add the <link> and change nothing else.
  2. Reach for .ui-* only when a page needs a widget the plain elements don't cover.
  3. Never write per-site CSS or a second palette. Fix ui.css so every site benefits.
  4. Restyle by setting tokens on <html>, e.g. style="--ui-hue:30".
  5. ui.js is optional; everything degrades to working HTML without it.
  6. Behaviour attaches through data-ui-* attributes, never through classes.

Tokens

:root

Every 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.

168
42%
40
6px
1
15px
presets
TokenDefaultDrives
--ui-hue --ui-sat168 · 42%Accent: links, focus, checked controls, active markers
--ui-tint --ui-tint-sat40 · 4%Neutral cast of every surface, border and text tone
--ui-radius6pxEvery corner in the framework, via -xs ×.4, -sm ×.7, -lg ×1.7
--ui-density1Multiplies the spacing scale --ui-1--ui-8
--ui-fs --ui-lh15px · 1.6Root type size and line height
--ui-display--ui-monoFace used for h1–h4; set to --ui-font for sans headings
--ui-width --ui-measure1080px · 74chContent column; prose line length
--ui-solid --ui-solid-inkink · pageFilled surfaces: primary button, solid badge
--ui-bg --ui-bg-alt --ui-surface --ui-surface-2derivedBackgrounds, back to front
--ui-line --ui-line-softderivedBorders: visible, hairline
--ui-text --ui-muted --ui-faintderivedText emphasis levels
--ui-ok --ui-warn --ui-bad --ui-infoStatus colours, each with a -wash background
--ui-fast --ui-mid --ui-slow90 · 160 · 280msMotion, 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 classes

What 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-grid

Flex and grid primitives. .ui-spacer pushes what follows to the far edge.

rowitemsend
auto
fit
grid
<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-btn

Works 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-segmented

Mutually 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-group

Label, control, hint or error. The controls themselves stay bare elements.

Host and port.
Token no longer valid.
<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-radio

Wrappers 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-slider

Wraps <input type="range"> with a label and a live value; ui.js paints the filled track.

6
116
192 kbps
<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-select

Upgrades 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 -->

Tabs

data-ui-tabs

A tab's data-ui-tab is the id of its panel. Arrow keys move between tabs.

Panels toggle with the 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>

Card

.ui-card

A surface. Use <a class="ui-card"> for a clickable one.

media-studio

up

yt-dlp + ffmpeg, port 8092.

--accent · left rule
--flat · recessed
<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
18services
99.4%uptime 30d
<div class="ui-stat">
  <span class="ui-stat__value">18</span>
  <span class="ui-stat__label">services</span>
</div>

Badge

.ui-badge .ui-dot

Monospace status label. .ui-dot is the bullet; --pulse for live state.

default accent ok warn bad info solid pill running
<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
wasm il2cpp
<span class="ui-chip">wasm
  <button class="ui-chip__x" aria-label="Remove">×</button>
</span>

Avatar

.ui-avatar
NM KR LG
<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.

ServicePortState
jetson-studio8091up
media-studio8092up
luac-studio8102restarting
<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-alert

Inline and persistent. For transient feedback use a toast.

Deployed. All checks passed.
DNS is still propagating.
Build failed at step 3.
Assets are cached for five minutes.
<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();

Tooltip

data-ui-tip

Pure 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-spinner

Bare <progress> when the value is known; .ui-progress when you drive the width yourself.

working
<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.

ClassEffect
.ui-muted .ui-faint .ui-accentText colour by emphasis
.ui-mono .ui-small .ui-big .ui-boldType adjustments
.ui-center .ui-rightText alignment
.ui-truncate .ui-nowrapSingle-line clamp; no wrapping
.ui-mt-0…3 .ui-mb-0…3Top and bottom margin steps
.ui-fullwidth: 100%
.ui-hide .ui-hide-smHide always; hide below 640px
.ui-srScreen-reader only
.ui-scroll-xHorizontal scroll container
.ui-revealFades in when scrolled into view
body.ui-wideDrops the centred column cap

JS API

window.UI

Auto-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-*
AttributeOnBehaviour
data-ui-dropdowntriggerToggles the sibling .ui-menu
data-ui-modal="#id"anyOpens that <dialog>
data-ui-closeanyCloses the enclosing dialog
data-ui-tabsstripWires the tabs inside it
data-ui-tab="id"tabThe panel this tab shows
data-ui-selectselectSearchable dropdown; stays a real select
data-ui-search="off"selectSuppresses the search box
data-ui-copy="text|#id"anyCopies to clipboard and toasts
data-ui-copy-msganyOverrides the toast text
data-ui-copy="off"preNo copy button on that block
data-ui-confirm="msg"anyAsks before the click proceeds
data-ui-theme-toggleanyCycles light and dark
data-ui-tip="text"anyTooltip; data-ui-tip-pos="bottom|right"
data-ui-autosizetextareaGrows with content
data-ui-prefix data-ui-suffixrangeUnits around the live value
data-ui-segmentedsegmentedMoves aria-pressed on click
aria-busy="true".ui-btnSpinner, hidden label, clicks blocked
data-themehtmllight / dark; absent follows the OS