More controls
Each is one custom element from @xoong/controls (also its own entry point, for example @xoong/controls/date). The form controls are XoControls: they have a name and a value, take part in validation, reset with the form and tell you when they are dirty. They look like the rest of the theme because the markup they draw uses the theme's classes. Every one has a keyboard path and an accessible name.
Choose: switch, segmented, split button
<div class="stack">
<xo-switch name="notify" checked>Email me about new comments</xo-switch>
<xo-switch name="menu" size="sm" label="Show in the menu"></xo-switch>
<xo-segmented name="period" label="Billing period" value="month"><button value="month">Monthly</button><button value="year">Yearly</button></xo-segmented>
<xo-split-button more="More publish options"><button class="primary">Publish</button><button>Schedule</button><button>Save as draft</button></xo-split-button>
</div>Text inside <xo-switch> is its visible label; without text, give it label. <xo-segmented> takes <button value> children; with multiple each segment toggles. The first child of <xo-split-button> is the main action; the others are the items of its menu.
Numbers: slider, range slider, stepper
<div class="stack">
<xo-slider name="quality" label="Image quality" min="0" max="100" step="5" value="80"></xo-slider>
<xo-slider name="blur" label="Blur" min="0" max="2" step="1" value="1" labels="Low,Medium,High"></xo-slider>
<xo-range-slider name="price" label="Price" min="0" max="100" value="20,70" min-label="Minimum price" max-label="Maximum price"></xo-range-slider>
<xo-stepper name="perPage" label="Items per page" min="1" max="100" step="5" value="25"></xo-stepper>
</div>labels gives screen readers a word for each stop. The stepper clamps to min and max, goes by step with the arrow keys and by ten steps with Page Up and Page Down. Without min or max there is no limit.
Date and time
<div class="stack">
<xo-date name="publishedAt" label="Publish on" value="2026-11-25"></xo-date>
<xo-date name="stay" label="Stay" range></xo-date>
<xo-time name="at" label="Time" step="15" value="09:30"></xo-time>
</div>The value is ISO YYYY-MM-DD (a range: { start, end }) and HH:mm; the field shows the date in the visitor's locale and also accepts typing. In the calendar: arrows move by day and week, Page Up and Page Down by month, Home and End to the start and end of the week, Escape closes.
Text: password, copy, textarea with a counter
<div class="stack">
<xo-password name="password" label="Password" autocomplete="new-password"></xo-password>
<xo-copy label="API key" value="xo_live_4f9a7f3a"></xo-copy>
<xo-textarea name="excerpt" label="Excerpt" maxlength="160" rows="3"></xo-textarea>
</div>The eye button of <xo-password> is aria-pressed. <xo-copy> says "Copied" for a moment and fires copied. <xo-textarea> keeps text pasted over the limit and marks it invalid instead of cutting it.
File
<xo-file name="logo" label="Logo" accept="image/*" max-size="2MB" hint="PNG or SVG, up to 2 MB"></xo-file>Drag a file onto the zone or press Enter to browse. The value is a File (an array with multiple); a refused file shows why and fires xo-file-reject. Images get a thumbnail; set el.progress = 0.4 to show an upload.
Structure: accordion, popover, chart, stat
Does it need a build?
Can I use my own styles?
Nothing new.
<div class="stack">
<xo-accordion single>
<details><summary>Does it need a build?</summary><div class="xo-accordion__body">No. The browser loads the files you installed.</div></details>
<details><summary>Can I use my own styles?</summary><div class="xo-accordion__body">Yes: tokens and plain CSS, in layers you control.</div></details>
</xo-accordion>
<div class="xo-notify"><button class="icon-only ghost" aria-label="Notifications"><span class="ic" data-ic="bell" aria-hidden="true"></span></button>
<xo-popover label="Notifications"><p>Nothing new.</p><button class="ghost sm">Mark all read</button></xo-popover></div>
<xo-stat label="Visits" value="48,210" delta="12.4" delta-label="vs last week" series="20,22,18,26,24,32,29"></xo-stat>
<xo-chart type="bar" label="Visits per day" unit="k" data='[{"label":"Mon","value":12},{"label":"Tue","value":18},{"label":"Wed","value":9},{"label":"Thu","value":22},{"label":"Fri","value":16}]'></xo-chart>
</div><xo-chart> draws bars or a line from data and always offers the same numbers as a table ("View as table"); the bars can be focused. <xo-popover> opens from the button before it and closes with Escape or a click outside; it stays inside the viewport.
Feedback: progress, spinner, steps, avatars, one-time code, rename in place
<div class="stack">
<xo-progress value="62" label="Upload"></xo-progress>
<xo-progress label="Working"></xo-progress>
<div class="row-inline"><xo-spinner label="Loading"></xo-spinner><xo-spinner label="Loading" size="lg"></xo-spinner></div>
<xo-steps current="1" label="Checkout"><span>Account</span><span>Plan</span><span>Payment</span></xo-steps>
<div class="row-inline"><xo-avatar name="Mai Trần"></xo-avatar><xo-avatar name="Linh Phạm" status="ok"></xo-avatar>
<xo-avatars max="2" label="Reviewers"><xo-avatar name="An Lê"></xo-avatar><xo-avatar name="Binh Vo"></xo-avatar><xo-avatar name="Chi Do"></xo-avatar></xo-avatars></div>
<xo-otp name="code" length="6" label="Verification code"></xo-otp>
<xo-inline-edit name="title" value="Greece Golden Visa" label="Title"></xo-inline-edit>
</div>Without a value, <xo-progress> is indeterminate. <xo-steps> marks the current step with aria-current="step" and shows a check on done steps, so the state is not only a colour. In <xo-otp> typing moves forward, Backspace back and a paste is split across the boxes; complete fires when every box is filled. In <xo-inline-edit> Enter saves and Escape cancels.
Page: image, theme toggle, command palette, cookie consent
<div class="stack">
<xo-image src="img/does-not-exist.png" alt="A picture that is not there" ratio="16/9" fallback="img/also-missing.png"></xo-image>
<xo-theme-toggle></xo-theme-toggle>
</div><xo-image> loads lazily, keeps its space and shows the alt text when the picture fails (here it does, on purpose). <xo-theme-toggle> sets data-theme on the page and remembers the choice; it is the same button as in this site's header.
<xo-command shortcut="k" label="Search commands">
<a href="/articles" data-group="Go to">Articles</a>
<button data-group="Actions" data-value="new-article">New article</button>
</xo-command>
<xo-consent categories="analytics,marketing">
<b>We use two cookies.</b> One keeps you signed in, one measures anonymous visits. You choose.
</xo-consent>
<button xo-consent-open>Cookie settings</button><xo-command> opens with Ctrl or Cmd plus K; its children are the commands, filtered as you type. <xo-consent> shows a bar until the visitor chooses, saves the choice and fires xo-consent. They are not run on this page because they cover it.
Navigation and overlays: breadcrumb, tree, drawer
Choose what to show.
<div class="stack">
<xo-breadcrumb max="3" items='[{"label":"Home","href":"#overlays"},{"label":"Docs","href":"#overlays"},{"label":"Controls","href":"#overlays"},{"label":"Tree"}]'></xo-breadcrumb>
<xo-tree label="Regions" multiple expanded="asia" data='[{"id":"asia","label":"Asia","children":[{"id":"vn","label":"Vietnam"},{"id":"jp","label":"Japan"}]},{"id":"eu","label":"Europe","children":[{"id":"fr","label":"France"}]}]'></xo-tree>
<button xo-open="#filters-drawer">Open the drawer</button>
<xo-drawer id="filters-drawer" heading="Filters" side="end" dismiss>
<p>Choose what to show.</p>
<button slot="footer" type="button" xo-close>Close</button>
</xo-drawer>
</div><xo-breadcrumb> marks the last item aria-current="page" and folds the middle into an "..." menu when max is set or the space is short. <xo-tree> follows the WAI-ARIA tree pattern: arrows, Home, End, * and type-ahead; with multiple a parent is checked, unchecked or mixed from its children. <xo-drawer> is a modal side sheet opened with xo-open like <xo-dialog>; Escape closes it and the focus returns to the button.
Messages: alert, skeleton
<div class="stack">
<xo-alert kind="info" heading="Heads up">Changes are saved as you type.</xo-alert>
<xo-alert kind="danger" heading="Payment failed" dismissible>Check your card and try again.</xo-alert>
<xo-skeleton lines="3"></xo-skeleton>
<xo-skeleton shape="circle" width="48" label=""></xo-skeleton>
</div><xo-alert> is role="status", or role="alert" for warning and danger (live overrides). <xo-skeleton> hides its shapes from screen readers and adds one visually hidden "Loading"; the shimmer stops under reduced motion.
Pick: color, rating
<div class="stack">
<xo-color name="brand" label="Brand color" value="#0b7285" swatches="#0b7285,#c92a2a,#2b8a3e,#f08c00"></xo-color>
<xo-rating name="score" label="Your rating" value="3.5" half clearable></xo-rating>
</div><xo-color> submits #rrggbb (#rrggbbaa with alpha); the hex field says what is wrong. <xo-rating> is a radio group with one native radio per star, so the arrow keys work.
Reference
Every attribute, property and event is in docs/controls.md and in the editor hints of xoong.html-data.json.