One stylesheet · every subdomain

nitaimaarek/ui

Frosted components and a cursor-reactive background, behind a single link tag. Bare HTML is styled with no markup changes.

Install

2 lines

The 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
  1. Bare HTML is already styled. Add the <link> and change nothing else.
  2. Reach for .ui-* only when a page needs a widget 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:190".
  5. Behaviour attaches through data-ui-* attributes, never through classes.

Tokens

:root

Every colour, corner, gap and blur derives from these. Drag anything — the whole page reacts live, including the background.

265
70%
240
14px
20px
1
presets
TokenDefaultDrives
--ui-hue --ui-sat265 · 70%Accent: links, focus, checked controls, background particles
--ui-tint --ui-tint-sat240 · 6%Neutral cast of every surface, border and text tone
--ui-radius14pxEvery corner, via -xs ×.35, -sm ×.65, -lg ×1.35
--ui-blur20pxFrosted-glass strength on panels; 0 makes them flat
--ui-lift-2pxHow far buttons and cards rise on hover
--ui-density1Multiplies the spacing scale --ui-1…--ui-8
--ui-fs --ui-lh15px · 1.6Root type size and line height
--ui-display--ui-fontFace for h1–h4 and .ui-display
--ui-glass --ui-glass-2derivedPanel fills — translucent so the background reads through
--ui-glass-line --ui-glass-hiderivedHairline border and the inset top highlight
--ui-solid --ui-solid-inkink · pageFilled 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-slow150 · 240 · 420msMotion, 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.

— fps
1
1
1
128px
ThemeLooks likeReacts to the cursor by
starsDepth-layered star field, near stars brighterDrifting toward it — near layers lead, far layers lag
meshLinked constellationShouldering points aside as it passes
auroraSlow blurred colour fieldsParallax drift against the pointer
gridDot lattice, slowly breathingDots swell and light up under the pointer
rippleDot fieldSwelling 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>
Built to stay cheap. Counts scale to viewport area, neighbour links use a spatial hash instead of the usual O(n²) sweep, segments are batched into five stroke calls, DPR is capped at 1.75, and the loop stops dead when the tab is hidden. Under prefers-reduced-motion it renders one static frame and never animates.

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

Buttons

.ui-btn

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

Searchable select

data-ui-select

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

Tabs

data-ui-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>

Card

.ui-card

media-studio

up

yt-dlp + ffmpeg, port 8092.

--accent · left rule
--flat · no blur, no shadow
<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
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
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="">

Table

<table>
ServicePortState
jetson-studio8091up
media-studio8092up
luac-studio8102restarting
<div class="ui-scroll-x">
  <table>…</table>
</div>

Alert

.ui-alert
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()
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
});

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

ClassEffect
.ui-displayOversized hero type; .ui-dim greys part of it
.ui-glassApplies the frosted-panel recipe to anything
.ui-muted .ui-faint .ui-accentText colour by emphasis
.ui-mono .ui-small .ui-big .ui-boldType adjustments
.ui-center .ui-right .ui-truncate .ui-nowrapAlignment and clamping
.ui-mt-0…3 .ui-mb-0…3 .ui-fullMargin steps, full width
.ui-hide .ui-hide-sm .ui-srHide always, below 640px, visually
.ui-scroll-x .ui-revealScroll container; fade in on scroll
body.ui-wideDrops the centred column cap

JS API

window.UI
UI.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-*
AttributeOnBehaviour
data-ui-bg="theme"bodyStarts the background; -density -speed -opacity -link -color -interactive tune it
data-ui-bg-set="theme"anySwitches the background on click
data-ui-dropdowntriggerToggles the sibling .ui-menu
data-ui-modal="#id"anyOpens that <dialog>
data-ui-closeanyCloses the enclosing dialog
data-ui-tabs · data-ui-tab="id"strip · tabTab wiring
data-ui-selectselectSearchable dropdown; stays a real select
data-ui-copy="text|#id"anyCopies to clipboard and toasts
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
aria-busy="true".ui-btnSpinner, hidden label, clicks blocked
data-themehtmllight / dark; absent follows the OS