Keystone demo
Style + icons + behavior, each loaded from a single tag. This page uses all three; comment any one out in the source to see the others still work.
Accent theme
Swap the accent color with data-ks-accent on
<html> (or any container). The primary steel-blue
is unchanged; -subtle / -muted
tints derive via color-mix so every preset works in light and dark.
Buttons
Form (with validation)
Form niceties
Rich-text editor
Combobox (searchable & multi-select)
Badges, chips & alerts
Overlays & feedback
Click outside or press Esc to close.
Right-click the Keystone site link (or any selected text) to open the custom context menu.
Promise-based dialogs
await Keystone.confirm() / .alert() / .prompt() — built on the modal, resolved on choice.
Result: (none yet)
Tabs & accordion
Data & status
| Name | Status | Uptime |
|---|---|---|
| Server A | Online | 99.98% |
| Server B | Degraded | 97.40% |
| Server C | Offline | 0.00% |
| Fleet | 3 nodes | 65.79% |
Table utilities
Sortable columns, a frozen first column, a toolbar, and a responsive
card-stack — all composable on the same .ks-table.
Click a header (or focus it and press Enter/Space) to cycle
ascending → descending → original. Type is auto-detected
per column. Narrow the window below 640px to see rows stack into labeled
cards. Without JS it stays a plain static table.
| Host | Status | Uptime | Last seen | Actions |
|---|---|---|---|---|
| web-01 | Online | 99.98% | Jun 30, 2026 |
|
| db-02 | Degraded | 97.40% | Jun 28, 2026 |
|
| cache-03 | Offline | 0.00% | May 11, 2026 |
|
| edge-04 | Online | 99.10% | Jun 30, 2026 |
|
| api-05 | Online | 98.72% | Jun 29, 2026 |
|
| queue-06 | Degraded | 95.30% | Jun 27, 2026 |
|
Empty state (as a full-width <td> row):
| Host | Status | Uptime |
|---|---|---|
|
No devices found
|
||
Table pack
Four behaviors over one shared controller, so they compose: the pipeline is always filter → sort → page. Type in the filter and the pager recounts; sort a column and you land back on page 1; select-all ticks the whole filtered set, not just this page; export writes what you are looking at, in the order you are looking at it, minus any column you hid. Rows added later by an API call are picked up automatically. Without JS it is one plain, complete table.
| Host | Team | Priority | Open |
|---|---|---|---|
| web-01 | Service desk | P1 | 62 |
| db-02 | NOC | P2 | 45 |
| cache-03 | Projects | P3 | 44 |
| edge-04 | Field | P2 | 34 |
| api-05 | Service desk | P3 | 28 |
| queue-06 | NOC | P1 | 21 |
| mail-07 | Projects | P3 | 19 |
| vpn-08 | Field | P2 | 17 |
| file-09 | Service desk | P3 | 14 |
| print-10 | NOC | P3 | 12 |
| backup-11 | Projects | P1 | 9 |
| dns-12 | Field | P3 | 6 |
| nas-13 | Service desk | P2 | 4 |
| wifi-14 | NOC | P3 | 2 |
Commands, time & clipboard
Press ⌘K (or Ctrl+K)
for the palette — it only binds once a page registers a command, so pages that
don't use it keep their shortcut. Timestamps rewrite themselves and keep
current on one shared tick. Copy buttons read a literal, a selector, or the
nearest <pre>. And this whole page has a print
stylesheet — try ⌘P.
These two register declaratively — the attribute makes the control a command, and the palette runs it by clicking it.
| Event | When |
|---|---|
| Backup finished | |
| Alert raised | |
| Ticket opened | |
| Maintenance window |
Styles: default, compact,
short. Hover for the absolute timestamp.
Keystone.commands.open()
This line is .ks-no-print — it won't appear on paper.
Printed copy — hidden on screen (.ks-print-only).
Charts — real axes
data-ks-x="time" reads each row label as a date and
plots it at its true position. The two charts below hold
the same readings: on the left a category axis spaces them
evenly, on the right a time axis shows what actually happened — three samples
a minute apart, then a six-hour hole, then two more. Also here:
data-ks-ref threshold lines, click-a-key-to-isolate,
and PNG / CSV export.
| Time | web-01 | db-02 |
|---|---|---|
| 08:00 | 41 | 55 |
| 08:01 | 44 | 57 |
| 08:02 | 43 | 56 |
| 14:02 | 92 | 61 |
| 14:03 | 88 | 60 |
| Time | web-01 | db-02 |
|---|---|---|
| 08:00 | 41 | 55 |
| 08:01 | 44 | 57 |
| 08:02 | 43 | 56 |
| 14:02 | 92 | 61 |
| 14:03 | 88 | 60 |
Click a legend key to isolate that series — the value axis rescales to what's left, and clicking it again brings everyone back. CSV always exports the full dataset, including a series you've isolated out of view.
Async regions
The four-state dance every data screen does — loading → error+retry → empty →
data — as markup instead of app code. Presentation only:
Keystone owns which slot shows, the retry button, the poll timer and the stale
clock; you own the fetch. Force each state below.
Note that a failed reload keeps the last good numbers on screen and
just marks the region stale — blanking a working board because one refresh
timed out is worse than slightly old data.
| Host | Problem | Severity |
|---|---|---|
| Server-rendered rows — what a no-JS visitor sees. | ||
App primitives
Building blocks for data-heavy app screens — all style-only, working in light and dark.
-
09:42
Deployment v1.4.0 succeeded on all nodes.
-
09:31
Node B reported degraded latency during rollout.
-
09:20
Release pipeline started by gkuhl.
Charts
Bar, line/area, donut and sparklines, rendered from real
<table> markup (or inline JSON / the JS API).
Without the module the tables below simply render; with it, each table stays
reachable behind “View data” — it is the chart's screen-reader representation.
Series colors are the validated --ks-chart-1…8
slots (colorblind-safe in light and dark, assigned in fixed order — never
rank, never a state color). Hover or focus any mark for its values.
| Quarter | Managed IT | Cloud | Security |
|---|---|---|---|
| Q1 | 48,290 | 21,400 | 9,800 |
| Q2 | 51,120 | 24,900 | 12,300 |
| Q3 | 46,700 | 28,150 | 14,050 |
| Q4 | 54,380 | 30,600 | 15,900 |
| Site | Hardware | Network | Software |
|---|---|---|---|
| Columbus | 34 | 21 | 46 |
| Dublin | 18 | 12 | 25 |
| Westerville | 11 | 17 | 14 |
| Remote | 6 | 28 | 31 |
| Week | p50 | p95 |
|---|---|---|
| W1 | 62 | 212 |
| W2 | 58 | 198 |
| W3 | 66 | 184 |
| W4 | 54 | 190 |
| W5 | 51 | 171 |
| W6 | 49 | 163 |
| OS | Devices |
|---|---|
| Windows | 412 |
| macOS | 96 |
| Linux | 44 |
| Other | 17 |
Inline sparklines ride along in text or table cells:
CPU ,
memory ,
deploys —
colored via currentColor, defaulting to slot 1.
Charts — live data, morph & annotations
An update doesn't redraw a chart, it moves it. Marks are keyed by meaning — series × category, tick value — so a bar that changed is the same bar resizing, a reading that scrolls off a rolling window slides out the left, and isolating a series (click a legend key) rescales the axis smoothly. Table-fed charts watch their table: re-render its rows from an async poll and the chart follows. Hit targets, tooltips and the table are right the instant the data lands; reduced motion simply swaps.
| Queue | Open |
|---|---|
| Service desk | 42 |
| Projects | 18 |
| Network | 27 |
| Security | 11 |
| Onboarding | 23 |
| Hour | p95 |
|---|---|
| 07:00 | 142 |
| 08:00 | 168 |
| 09:00 | 181 |
| 10:00 | 236 |
| 11:00 | 214 |
| 12:00 | 171 |
| 13:00 | 158 |
| 14:00 | 139 |
| OS | Devices |
|---|---|
| Windows 10 | 188 |
| Windows 11 | 224 |
| macOS | 96 |
| Linux | 44 |
Infographics — gauges, grids, boards & flows
Eight forms in the charts module, each built from markup you'd write anyway — a
<meter> or a <table>,
which stays as the chart's screen-reader twin and its no-JS fallback. Color does one
job per form: categorical slots for identity, the steel sequential ramp
for magnitude, the ordinal ramp for ordered stages, and state tones only
for state — always with a word and a glyph. Dense grids are one tab stop: focus one and
walk it with the arrow keys.
| Day | 7a | 8a | 9a | 10a | 11a | 12p | 1p | 2p | 3p | 4p | 5p | 6p |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Mon | 6 | 14 | 22 | 19 | 15 | 9 | 12 | 14 | 11 | 9 | 6 | 3 |
| Tue | 5 | 11 | 17 | 16 | 13 | 8 | 11 | 12 | 10 | 8 | 5 | 2 |
| Wed | 4 | 10 | 16 | 15 | 12 | 8 | 10 | 13 | 11 | 7 | 5 | 2 |
| Thu | 5 | 12 | 18 | 14 | 12 | 7 | 9 | 11 | 10 | 8 | 4 | 2 |
| Fri | 4 | 9 | 13 | 12 | 10 | 6 | 7 | 8 | 6 | 5 | 3 | 1 |
| Sat | 0 | 1 | 2 | 3 | 2 | 2 | 1 | 1 | 1 | 0 | 0 | 0 |
| Sun | 0 | 0 | 1 | 1 | 1 | 1 |
| Status | Devices |
|---|---|
| Fully patched | 446 |
| Day | Microsoft 365 | Site-to-site VPN | Cloud backup | Help desk portal |
|---|
| Date | Alerts |
|---|
| Measure | Actual | Target | Poor | Fair | Max |
|---|---|---|---|---|---|
| First response in SLA | 92% | 95% | 80 | 90 | 100 |
| Resolved in SLA | 88% | 85% | 70 | 80 | 100 |
| Customer satisfaction | 4.7 | 4.5 | 3.5 | 4.2 | 5 |
| Patch compliance | 94% | 98% | 85 | 95 | 100 |
| Stage | Quotes |
|---|---|
| Drafted | 240 |
| Sent | 196 |
| Viewed | 151 |
| Approved | 78 |
| Device | Age (years) | Tickets (12 mo) | Users | Site |
|---|
Motion — controls, progress & reveal
State changes animate: the check mark springs in, the radio core scales up,
the switch knob slides while its track eases off→on. Progress eases width
changes and offers a looping --indeterminate
sweep. All of this respects prefers-reduced-motion.
This card rises and fades in as it enters the viewport — scroll-timeline where supported, IntersectionObserver fallback otherwise.
Motion — sequence, constancy & live values
Motion that shows change: a list arrives in order, a sorted row glides to its new
place, a polled number ticks from what it showed to what it is now. The navbar's
motion button (or Keystone.motion.set('reduce'))
switches all of it off in-app — persisted, scoped like density, and never able to
turn motion back on over an OS that asked for less.
Effective now: full motion.
data-ks-motion="reduce" on any container
silences just that subtree.
- Backup verified — Columbus
- Patch window closed — Dublin
- New device enrolled — Remote
- Firewall policy synced
- Ticket #4182 escalated
| Device | Site | CPU % | Last seen |
|---|---|---|---|
| COL-WS-014 | Columbus | 34 | Oct 20, 09:12 |
| DUB-SRV-02 | Dublin | 81 | Oct 20, 09:40 |
| WES-WS-007 | Westerville | 12 | Oct 19, 17:05 |
| COL-SRV-01 | Columbus | 67 | Oct 20, 09:41 |
| REM-LT-221 | Remote | 5 | Oct 18, 22:30 |
| DUB-WS-031 | Dublin | 48 | Oct 20, 08:55 |
| COL-NAS-01 | Columbus | 22 | Oct 20, 09:38 |
| WES-SRV-01 | Westerville | 93 | Oct 20, 09:42 |
Draw
.ks-draw on an inline SVG draws its line work in when it
is first seen; labels fade in behind. For diagrams and process illustrations — fully
drawn without JS, under reduced motion, and in print.
Infographics — meter, ring, steps & proportion
CSS-only forms with a non-visual twin built in: <meter>
is announced natively, rings carry role="progressbar",
steps are an ordered list, a proportion bar's legend is its text. Tone signals state
(always beside a word), and every one of them renders with only
keystone.css loaded.
- Installed 412
- Pending 38
- Failed 9
- Email 214
- Phone 132
- Portal 88
- Chat 41
- DiscoverySite survey complete
- AssessmentRisk report delivered
- MigrationM365 tenant cutover
- TrainingStaff sessions
- Go-liveManaged support begins
- Quote draftedOct 14 · Account manager
- Internal reviewOct 15 · Team lead
- Client approval — changes requestedOct 17 · Pricing on line 4
- Signed
Density modes
Scale control heights, padding and font for data-dense screens without re-theming.
Add data-ks-density="compact",
"default" or "comfortable" to
<html> or any container.
Compact clamps interactive controls to a 32px minimum tap target.
The middle column below writes "default" explicitly — it is
pixel-identical to omitting the attribute, and it is the only way to reset a
subtree to 1x inside a comfortable ancestor (see below).
| Host | State | Load |
|---|---|---|
| node-01 | Up | 0.42 |
| node-02 | Warn | 0.88 |
- Item one A
- Item two B
| Host | State | Load |
|---|---|---|
| node-01 | Up | 0.42 |
| node-02 | Warn | 0.88 |
- Item one A
- Item two B
| Host | State | Load |
|---|---|---|
| node-01 | Up | 0.42 |
| node-02 | Warn | 0.88 |
- Item one A
- Item two B
Resetting inside a dense page. Density nests, so a container inside a
comfortable ancestor inherits 1.25x — and until now there was no way back. Writing
data-ks-density="default" returns a subtree to 1x, which is
what a data table on an otherwise roomy page wants.
This table is "default" inside the comfortable card:
| Host | State | Load |
|---|---|---|
| node-01 | Up | 0.42 |
| node-02 | Warn | 0.88 |
Sound & haptics
Optional official plugins, shipped off in plugins/config.json. Enable them below to feel/hear them — then click any button on the page. Haptics need a device with a vibration motor.
Stateful fields
Add the stateful attribute and a field remembers its value
across reloads — persisted to the browser's private file system (a port of
pio). Type below, then refresh.
Requires the File System Access API (Chromium/WebKit). Other browsers no-op silently. Radios persist by group name.
Icons
Rendered from the embedded webfont via .ks-i-<category>-<name>.