Loading the documentation…
Xenon is a design system for browser applications: foundations, components, workflow patterns, and application layouts for dense data screens, long forms, and focused tasks. Zx is its implementation. It ships as plain ES2022 modules with no runtime dependencies — no framework or bundler required. The same source you read here runs unchanged in the browser. Xenon was developed at ZeyOS, but its public contracts are product- and framework-agnostic.
Everything is built on native platform features: <dialog>, the popover
attribute, ElementInternals, :has(), and CSS custom properties.
Components follow the WAI-ARIA Authoring Practices patterns, so keyboard support and screen
reader semantics come from the pattern rather than from bolted-on attributes.
The layout gallery puts the components to work: complete application templates — mail, an issue tracker, a knowledge base, invoicing, a CRM, and more — and the screen patterns inside them, each running live with its full source, down to a real ZeyOS invoices screen driven by a live schema.
Building with an AI agent? llms.txt is the machine-readable index, and docs/llms.md is the full agent reference — the same text the Reference tab shows for each component.
Evergreen browsers from roughly 2023 onwards: Chrome and Edge 114+, Firefox 121+, Safari 17+. There is no transpilation step and no polyfill bundle.
npm run build writes four asset families in regular and minified form, plus
generated declarations, to dist/. Pick the entry points that match the host:
| File | Format | Use it when |
|---|---|---|
zx.esm.js |
ES module | The application uses import — the default choice. |
zx.global.js |
IIFE → window.zx |
A classic screen with plain <script> tags. |
zx-zeyos.esm.js |
ES module | The application uses the optional schema, module-icon, launcher, or entity bindings. |
zx.css |
Stylesheet | Always. Load it once per document. |
<link rel="stylesheet" href="/assets/zx.css">
<body class="zx-scope">
<script type="module">
import { Table, Message } from '/assets/zx.esm.js';
const table = new Table('#records', {
columns: [
{ id: 'number', label: 'Number', sortable: true, width: '1fr' },
{ id: 'customer', label: 'Customer', sortable: true, width: '2fr' }
],
data: records,
rowId: 'ID',
sortMode: 'local',
onrowclick: (event) => Message.info('Opening ' + event.detail.row.number)
});
</script>
</body>
The global bundle exposes the identical API under window.zx, which is what
classic ZeyOS screens use.
<link rel="stylesheet" href="/assets/zx.css">
<script src="/assets/zx.global.js"></script>
<script>
var toggle = new zx.Toggle(null, { label: 'Notifications', checked: true });
document.body.append(toggle.toElement());
</script>
zx-scope class
Base typography, box sizing, and the default chrome for unstyled native controls live under
.zx-scope. Put it on <body> — or on a single container when
Zx has to coexist with another design system on the same page.
npm run serve # http://127.0.0.1:8321/website/docs.html
npm test # unit tests + design-token lint
npm run build # dist/ bundles
No build step is required to develop: the server serves the source modules and the browser imports them directly.
Every component takes the same two arguments: a target and an options object.
new Component(target, options)
target can be:
querySelector;null — the component builds and owns its own root, which
you retrieve with .toElement() and place yourself.
import { Select, Message } from '/assets/zx.esm.js';
const select = new Select(null, {
items: [
{ ID: 'open', name: 'Open' },
{ ID: 'paid', name: 'Paid' }
],
value: 'open',
filter: 'local',
placeholder: 'Find a status'
});
document.querySelector('#status').append(select.toElement());
Subscribe with .on(type, handler), or pass an on<type>
option to the constructor. Handlers receive a CustomEvent whose
detail is a plain object. Event names are lowercase and carry no
on prefix.
select.on('change', (event) => {
Message.success('Status: ' + event.detail.item.name);
});
// …identical to:
new Select(null, {
onchange: (event) => Message.success('Status: ' + event.detail.item.name)
});
Each emit also dispatches a bubbling, composed zx-<type> event on the
component's root element, so an ancestor can listen without holding the instance:
document.querySelector('#workspace').addEventListener('zx-change', (event) => {
console.log(event.target, event.detail);
});
Call .destroy() when a screen closes. It aborts every listener the component
registered and removes the nodes it created, leaving the DOM as it found it. Creating and
destroying a component twice on the same target is safe.
select.destroy();
h() builds elements, icon() returns an inline SVG, and
button() is a plain factory rather than a class. See
Helper functions for the full surface.
import { h, button, icon } from '/assets/zx.esm.js';
const toolbar = h('div', { class: 'toolbar' },
button({ label: 'New invoice', kind: 'primary', icon: 'plus', onclick: create }),
button({ label: 'Refresh', icon: 'reload', onclick: reload })
);
The theme studio puts every component family on one page under a live theme: cycle the standard presets, change the accent, spacing, radius, and type, and copy the result out as CSS. It is the fastest way to read the rest of this page.
Set data-zx-theme (light, dark, auto) on
any ancestor — usually <html>. It cascades, so a dark panel inside a
light application is just another attribute.
<html data-zx-theme="dark">
Xenon has one geometry, and it is set by tokens rather than modes. Every spacing step is a
multiple of --zx-space-unit (4px: steps of 4, 8, 12, 16, 20, 24, 32, and
40px), every radius step is a fraction of --zx-radius (4px: 2, 3, 4, and 6px),
and controls are --zx-control-height tall (30px). Change those three and
padding, gaps, indents, corners, and rows all follow together — on the whole application,
or on one region of it.
/* A tighter application: a smaller unit and shorter controls, type unchanged. */
:root {
--zx-space-unit: 3.5px;
--zx-control-height: 28px;
}
Xenon separates ambient light from material. Add one Aurora to an application canvas, then let semantic surface roles decide how much of that light shows through. Persistent chrome is translucent but calm; raised feature cards reveal a little more; menus and dialogs use the overlay tiers. Tables, forms, code, long lists, and docked work areas stay opaque for sustained reading.
import { Aurora } from '/assets/zx.esm.js';
const light = new Aurora('#application-canvas', {
preset: 'source',
colors: ['#21cc75', '#2b7fff', '#9a67ff'],
intensity: 'subtle'
});
// Components on the canvas consume the shared chrome, raised, and overlay material tokens.
// They never need their own Aurora instance.
The Theme Studio's Flat, Glass, and Deep Glass modes change the response of those materials without changing Aurora itself. Reduced-transparency and forced-colour modes suppress ambient illumination and return every material to an opaque semantic surface.
data-zx-preset is the second attribute, and it is orthogonal to the first:
it picks the accent ramp, while data-zx-theme decides which
end of that ramp each surface uses. Six ship with the library:
| Preset | Accent | For |
|---|---|---|
zx | ZeyOS green | The default. Set nothing to get it. |
zeyos | ZeyOS gold | Screens that sit inside the ZeyOS product marque. |
ocean | Blue | Tools meant to read as infrastructure. |
violet | Violet | The most saturated of the six. |
rose | Rose | Warm and loud; check it against the danger colour. |
slate | Near-neutral | Data-heavy screens that should carry their own colour. |
<html data-zx-theme="dark" data-zx-preset="ocean">
A preset is five tier-1 declarations — --zx-accent-300 through
--zx-accent-700 — and nothing else. The semantic tier reads
600 and 700 on light surfaces and 300, 400, and 500 on dark ones, so one block rethemes
light, dark, and auto together, with no second list to keep in sync.
[data-zx-preset="acme"] {
--zx-accent-300: #c4b4ff;
--zx-accent-400: #a684ff;
--zx-accent-500: #8e51ff;
--zx-accent-600: #7f22fe;
--zx-accent-700: #7008e7;
}
Two stops carry a contract. --zx-color-on-accent is white on light surfaces and
near-black on dark ones, and it is never restated per theme — which only works while
600 clears 4.5:1 against white and 400 clears 4.5:1 against the
dark page. Every shipped preset does, and the studio reports the ratio for whatever
you build. Status colours are deliberately not part of a preset: danger, warning, and
success mean the same thing whatever the brand colour is.
Colours are defined once as a tier-1 palette
(--zx-gray-*, --zx-green-*, …) and mapped to
tier-2 semantic tokens that describe a role rather than a hue. Components —
and application CSS — may only use the semantic tier. tests/lint-tokens.js
enforces this, which is what makes retheming a one-file change.
One tier-1 name is meant to be overridden: --zx-accent-*. It is an alias the
accent roles resolve through, so a preset can repoint the brand colour without touching a
single semantic mapping. Everything else in tier 1 stays private.
| Token | Role |
|---|---|
--zx-color-bg-page | Application background |
--zx-color-bg-surface | Cards, panels, headers |
--zx-color-bg-raised | Menus, popovers, toolbars |
--zx-color-bg-muted | Inline code, table headers |
--zx-color-bg-hover | Hover and pressed fills |
--zx-color-bg-selected | Selected rows and items |
--zx-color-glass-chrome | Persistent navigation and application chrome |
--zx-color-glass-surface | Explicitly raised feature content |
--zx-color-overlay-surface | Compact menus, pickers, tooltips, and Toast |
--zx-color-overlay-panel | Launcher, Dialog, Modal, and overlay Sheet |
--zx-color-border | Structural separators |
--zx-color-border-control | Input and button outlines |
--zx-color-text / -muted | Body and secondary text |
--zx-color-accent | Primary actions and selection |
--zx-color-danger / -warning / -success / -info | Status colours, each with a matching -bg |
--zx-control-height / -radius / -pad-x | Control geometry |
--zx-space-unit / --zx-space-1 … -8 | Spacing, as multiples of one unit |
--zx-radius / -sm / -md / -lg / -xl | Corner radius, as fractions of one base |
--zx-focus-ring | The :focus-visible box shadow |
The Design tokens entry renders every one of them live, and the theme studio shows what changing them does to the whole library at once.
Override semantic tokens under a named theme attribute. Nothing else changes — every component picks the new values up.
[data-zx-theme="acme"] {
color-scheme: light;
--zx-color-accent: #2a66ab;
--zx-color-accent-hover: #1d4c80;
--zx-color-on-accent: #ffffff;
--zx-color-bg-page: #f7f8fa;
--zx-radius: 3px;
}
Reach for the least specific override that works. A global semantic token first — it keeps the whole set coherent. Then one of the published component hooks, each of which falls back to the token it replaces, so setting none of them changes nothing:
--zx-table-header-bg, --zx-table-row-hover-bg,
--zx-table-row-selected-bg, --zx-table-border-color--zx-button-radius--zx-panel-header-bg
An internal selector never. Component CSS declares many other
--zx-<component>-* properties — those are internal geometry, not API, and
they change without notice. The list above is deliberately short for the same reason: every
hook published is a promise to keep.
The stock light and dark themes use a restrained neutral ramp, a 4px spacing unit, a 4px base radius, Inter, 30px controls, and a three-pixel translucent focus ring. The accent is the ZeyOS green, tuned across Xenon's emerald ramp so it stays legible on both surfaces.
ZeyOS module colours ship as --zx-module-* tokens.
MasterPanel and NavigationBar accept a module option
(billing, projects, tickets, …) and colour their
header accordingly. Entity and API resource names resolve too —
--zx-module-invoices points at the billing-transaction colour.
The tokens are generated from src/zeyos/modules.js, the configuration file
that also names each module's icon in the ZeyOS Font Awesome kit. See
Icons for the module gallery and the chip renderer.
Business data comes from the dedicated ZeyOS client library,
@zeyos/client: a zero-dependency ESM OpenAPI client covering
the whole ZeyOS surface. Zx does not wrap it, bundle it, or depend on it — you construct it
and hand it in.
import { createZeyosClient } from '@zeyos/client';
const client = createZeyosClient({
platform: 'https://cloud.zeyos.com/your-instance/',
auth: { mode: 'session', session: { enabled: true, credentials: 'include' } }
});
const invoices = await client.api.listTransactions({ limit: 25 });
Note the ZeyOS naming: invoices live under transactions and customers under
accounts. Client instances are frozen — never attach properties to them.
src/zeyos/ is an optional, injection-based layer between Zx and the client. It
reads client.schema at runtime and generates fully typed UI: date fields become
dateboxes, enums become selects, foreign keys become entity pickers.
import { zeyosTable, zeyosForm } from '/assets/zx-zeyos.esm.js';
const list = zeyosTable(client, 'transactions', {
fields: ['transactionnum', 'account', 'date', 'netamount', 'status'],
pageSize: 25,
sort: { id: 'date', dir: 'desc' },
onRowClick: (row) => openEditor(row.ID)
});
await list.load({ search: '', filters: {} });
document.querySelector('#invoices').append(list.table);
The binding ships as a separate dist/zx-zeyos.esm.js bundle and is never
imported by src/index.js, so the core stays dependency-free. Query rules worth
remembering: filters is plural, dates are Unix seconds, and
visibility: 0 is the default scope.
For in-app remotecall-style requests that are not part of the OpenAPI surface,
Zx ships a minimal Http / zeyosService helper. For anything that
maps to a ZeyOS resource, prefer @zeyos/client.
The ZeyOS invoices layout is the reference integration: a
complete invoices screen generated from the real transactions and
accounts schemas, with server-side filtering, sorting, and paging.
Each interactive component is built to a specific APG pattern rather than having ARIA
sprinkled on afterwards: Select is an editable combobox,
MenuButton is a menu button, Tabbox is a tab list,
Dialog is a modal dialog on top of native <dialog>.
The pattern brings its full keyboard map with it.
Components express state through ARIA attributes and data-state — never
through state classes — and style themselves from those same attributes
([aria-expanded="true"], [aria-selected="true"],
[data-state="open"]). What a screen reader announces and what you see are
driven by one source of truth.
Focus styling uses :focus-visible with the --zx-focus-ring token,
so keyboard users get a ring and mouse users do not. Overlays trap focus with
focusTrap() and restore it to the trigger on close. Roving tabindex and
typeahead are available as helpers for custom widgets.
All non-essential animation sits behind
@media (prefers-reduced-motion: no-preference), and the base stylesheet
collapses transitions entirely when a reduced-motion preference is set.
Both stock themes are checked against WCAG AA for body text, muted text, status colours, and
text on accent fills. If you define a product theme, re-check the accent pair —
--zx-color-accent against --zx-color-on-accent — because that is
the one that usually slips.