UI kit & styling
Inside its box, a card is an ordinary web page: any framework, any CSS, canvas, WebGL, WebAssembly. Two ways to style it:
- Native look — the optional kit
ui.css: a small dark set of buttons, fields, lists and badges on the app’s live theme. Cards built with it look like part of NeuroSquad and follow its theme. - Your own design — ignore the kit. The theme variables are still there if you want to borrow a colour.
The UI kit
<link rel="stylesheet" href="vendor/ui.css" /> <!-- plain template -->import '@neurosquad/card-sdk/ui.css' // with a bundler (React template)Put class="ns-kit" on <body> for the base typography, background and scrollbars, then use the
classes:
<body class="ns-kit">
<header class="ns-spread">
<h1 class="ns-title ns-truncate">Test radar</h1>
<span class="ns-badge ns-badge--danger">3 failed</span>
</header>
<ul class="ns-list ns-scroll">
<li class="ns-list-item"><span class="ns-dot ns-dot--danger"></span> checkout › pays</li>
<li class="ns-list-item"><span class="ns-dot ns-dot--success"></span> auth › logs in</li>
</ul>
<label class="ns-field">
<span class="ns-label">Filter</span>
<input class="ns-input" placeholder="checkout" />
</label>
<div class="ns-row">
<button class="ns-btn ns-btn--primary">Run</button>
<button class="ns-btn ns-btn--ghost ns-btn--sm">Clear</button>
</div>
</body>| Group | Classes |
|---|---|
| Layout | ns-kit, ns-stack (vertical), ns-row (horizontal), ns-spread (space-between), ns-grow, ns-scroll, ns-pad, ns-divider |
| Surfaces | ns-surface, ns-surface--inset, ns-callout, ns-callout--success, ns-callout--danger, ns-empty |
| Text | ns-title, ns-subtitle, ns-muted, ns-small, ns-mono, ns-truncate, ns-kbd, ns-help, ns-error |
| Buttons | ns-btn + --primary, --secondary, --outline, --ghost, --danger, --sm, --lg, --icon |
| Fields | ns-field, ns-label, ns-input, ns-textarea, ns-select, ns-switch |
| Lists | ns-list, ns-list-item |
| Status | ns-badge + --accent, --success, --warning, --danger; ns-dot + the same; ns-spinner, ns-progress |
Outside the app (a browser preview), the kit falls back to the app’s dark theme, so the preview looks right too.
Theme variables
connect() puts the app’s live theme on <html> as --ns-<token> variables — --ns-background,
--ns-surface, --ns-foreground, --ns-muted, --ns-accent, --ns-success, --ns-warning,
--ns-danger, their -foreground and -soft variants, --ns-border, --ns-focus,
--ns-field-*, --ns-radius, --ns-font-sans, --ns-font-mono and more (the full list is on
Theme). Use them in your own CSS and your card follows the app:
:root { color-scheme: dark; }
body {
margin: 0;
background: var(--ns-background, #0b0b0f);
color: var(--ns-foreground, #fafafa);
font: 13px/1.45 var(--ns-font-sans, system-ui, sans-serif);
}
.chip {
border-radius: calc(var(--ns-radius, 0.5rem) * 2);
background: var(--ns-accent-soft);
color: var(--ns-accent-soft-foreground);
}Give the variables fallbacks (as above) if your page must also render outside the app. To keep your
own colours untouched, connect with connect({ theme: false }).
Sandbox rules for pages
Your page is served from its own package with a strict content security policy. In practice:
- No inline scripts or
onclick="…"attributes — put code in.jsfiles.evalandnew Functionare blocked too (WebAssembly is allowed). - No external files. Scripts, styles, fonts and images must be inside the package; a
<link href="https://fonts…">or<img src="https://…">is blocked.data:andblob:images work. For remote data, usecard.net.fetchand turn images intoblob:URLs. - Inline styles are fine (
style="…"and<style>). - Relative URLs. The page is served from
nscard://<id>/<path>; build with relative asset paths (Vite:base: './', as in the React template). - No
localStorage, cookies,alert, pop-ups, downloads, fullscreen, camera or microphone. Usecard.storage,card.ui.confirm,card.openLink. - No writing to the clipboard from the page.
copy/cuthandlers anddocument.execCommand('copy')do nothing (otherwise any card could replace the clipboard on a click). Usecard.copyText; the user’s own copy of selected text still works. - No nested frames.
iframe,frame,objectandembedelements are removed. - Only blob workers.
new Worker('worker.js')fails in the card’s origin; fetch the script, wrap it in aBloband startnew Worker(URL.createObjectURL(blob)). - Never trust
messageevents. Other cards canpostMessageyour frame.
npx @neurosquad/card-sdk validate warns about inline scripts, inline handlers and external files in
your entry page.
Forms never navigate. A <form>’s submit event fires and your handler runs, but the app
cancels the submission itself and form.submit() does nothing — a card page cannot post anywhere
or reload. Call event.preventDefault() in your handler anyway and do the work in JavaScript.
Designing for the canvas
- Small and large. A card is often 400×300 and sometimes expanded to fill the screen. Make both
look finished: use
card.lifecycle.onExpandedoruseExpanded()to switch layouts. - Empty, loading, error. Show something sensible before data arrives, when there is none, and when a call fails.
- The overview tile is your card zoomed out. Keep
setOverviewcurrent with the one fact that matters. - Quiet when unseen. Stop animations when
usePaused()is true — a canvas can hold fifty cards. - The header is the app’s. Do not repeat the card’s name or a close button inside the body; the header already has them.
Anything inside the card body is the card’s own, and users are told so. Do not imitate
NeuroSquad’s dialogs or ask for passwords and keys inside your card — use secret settings, which
the app asks for in its own form.