npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gnome-ui/web-components

v1.13.0

Published

Framework-agnostic GNOME UI Web Components built with native Custom Elements

Downloads

464

Readme

@gnome-ui/web-components

Framework-agnostic GNOME UI widgets implemented with native Custom Elements, light DOM, and the design tokens from @gnome-ui/core.

The package currently contains forty-eight framework-agnostic components:

  • <gnome-action-row> — settings row with title/subtitle/prefix/suffix slots; interactive composes a real <button data-slot="row-surface"> around everything except row-suffix, emitting gnome-activate.
  • <gnome-avatar> — circular image or name-derived initials fallback, driven by the composed <img>'s own error event.
  • <gnome-badge> — pure CSS counter/status indicator, optionally anchored over another element.
  • <gnome-banner> — persistent top-of-view message strip with data-action/data-dismiss descendants and gnome-action/gnome-dismiss events.
  • <gnome-bin> — single-child container with no visual styling of its own; the only CSS rule it ships is display: block, matching a plain <div>.
  • <gnome-boxed-list> — rounded bordered list; gives every direct child role="listitem" and merges their borders with pure CSS dividers.
  • <gnome-button> — styled native buttons with GNOME variants, sizing, loading state, and preserved form behavior.
  • <gnome-callout> — inline, dismissible admonition box; data-dismiss emits gnome-dismiss but the host never hides itself, unlike gnome-banner.
  • <gnome-card> — elevated content container; interactive composes a real <button data-slot="card-surface"> around its children.
  • <gnome-checkbox> — styled native multi-selection checkbox with imperative indeterminate support.
  • <gnome-choice-card-group> — role="radiogroup" of large selectable cards; combines gnome-field-group's fieldset/legend/hint wrapping with gnome-view-switcher's roving-tabindex keyboard navigation.
  • <gnome-combo-row> — settings row with a real, consumer-authored <gnome-dropdown> in data-slot="row-suffix"; genuinely composes gnome-action-row's layout with gnome-dropdown's behavior, no combobox logic duplicated.
  • <gnome-copy-button> — icon button that copies value to the clipboard, swapping to a checkmark confirmation; fully host-generated, composing an internal gnome-icon-button.
  • <gnome-dialog> — modal focus management, Escape/backdrop dismissal, and focus restoration.
  • <gnome-divider> — horizontal rule with an optional centered label, fully host-derived from the label attribute; distinct from gnome-separator, which has no label but supports a vertical orientation.
  • <gnome-dropdown> — combo-box option list; role="combobox" trigger + role="listbox" content, active option tracked via aria-activedescendant instead of moving DOM focus.
  • <gnome-expander> — standalone disclosure triangle + collapsible content; a bare, unstyled counterpart to gnome-expander-row for use outside a settings-row context.
  • <gnome-expander-row> — collapsible gnome-action-row; remaining light-DOM children are moved into a generated, height-animated role="region" panel; dividers between them are pure CSS.
  • <gnome-field-group> — wraps a real <fieldset>/<legend>; original light-DOM children (the fields) are moved into a generated content div inside it, so disabled disables every descendant form control for free.
  • <gnome-file-drop-zone> — drag-and-drop upload target with a click-to-browse fallback; fully host-generated, entirely event-driven (gnome-files-selected/gnome-error) since File objects can't round-trip through attributes.
  • <gnome-file-type-icon> — role="img" icon (or thumbnail) resolved from a file's MIME type or name extension; icon glyphs come from @gnome-ui/icons, this package's only non-@gnome-ui/core dependency.
  • <gnome-header-bar> — title bar with start/title/end regions placed in explicit CSS grid columns so the title stays centered without either side slot.
  • <gnome-highlight> — wraps every occurrence of query in <mark>; query splits on whitespace into individual terms. Fully host-derived from text/query/case-sensitive.
  • <gnome-icon-button> — icon-only action button, always circular; the host requires a label and syncs it onto the control's aria-label.
  • <gnome-kbd> — single key-cap for inline instructional text; the key name is authored as plain light-DOM text, common names normalise to a Unicode symbol unless raw is set, and the original name moves to aria-label.
  • <gnome-level-bar> — role="meter" gauge with colour-coded low/high offset zones; continuous fill via a CSS custom property, or a row of host-derived blocks in discrete mode.
  • <gnome-linked-group> — merges children into a single connected unit with no gap and merged borders; vertical is a plain attribute read directly by CSS, same rationale as gnome-badge.
  • <gnome-menu> — action menus with arrow-key navigation, typeahead, and cancelable selection events.
  • <gnome-otp-input> — segmented PIN/verification-code input; fully host-generated cells with auto-advance, backspace, and paste support.
  • <gnome-radio-group> — shared naming and group-level disabling around native radio inputs, with a normalized value/gnome-change API.
  • <gnome-rating-stars> — fully host-generated star rating; interactive roving-tabindex role="radiogroup" by default, or a static role="img" when readonly/disabled.
  • <gnome-separator> — dividing line; the host manages role="separator"/aria-orientation since a custom element can't switch between <hr> and a <div> per orientation like the React version.
  • <gnome-skeleton> — presentational loading placeholder with rect, circle, and text (row-count) variants.
  • <gnome-slider> — styled native <input type="range"> with a CSS-driven accent fill.
  • <gnome-spin-button> — styled native <input type="number"> with decrement/increment buttons wired to stepDown()/stepUp().
  • <gnome-spinner> — presentational indeterminate loading indicator with role="status".
  • <gnome-progress-bar> — presentational determinate/indeterminate progress indicator with role="progressbar".
  • <gnome-step-indicator> — numbered "Step X of Y" progress indicator; steps accepts a plain count (unlabelled) or a comma-separated label list, fully host-derived.
  • <gnome-switch> — styled native on/off toggle with preserved form behavior and native change/input events.
  • <gnome-switch-row> — the entire row is the switch (role="switch" on a generated <button data-slot="row-surface">); clicking anywhere toggles it, genuinely different from gnome-action-row + a nested gnome-switch.
  • <gnome-tab-bar> — role="tablist" roving-tabindex keyboard navigation; the host does not create tabs or manage selection, only focus movement.
  • <gnome-text-field> — styled native text input/textarea with label and helper/error text slots wired via for/id and aria-describedby.
  • <gnome-text-truncate> — ResizeObserver-driven overflow detection; only when actually clipped does it move its content into a real gnome-tooltip revealing the full text.
  • <gnome-toast> — live-region announcements, timed dismissal, and pause-on-hover/focus.
  • <gnome-toolbar> — horizontal .toolbar-style action bar; pure CSS host with no attributes, providing the standard 6px padding/gap for rows of buttons, gnome-linked-group clusters, dropdowns, and dividers.
  • <gnome-popover> — trigger relationships, adaptive positioning, outside dismissal, and focus restoration.
  • <gnome-tooltip> — hover/focus-triggered informational bubble, sharing gnome-popover's floating-position helper; no focus trap.
  • <gnome-view-switcher> — segmented control (role="radiogroup"); all four arrow keys cycle and moving focus also activates the target item (automatic activation).

The package does not depend on React, Angular, Lit, or htmx.

Installation

npm install @gnome-ui/core @gnome-ui/web-components

Import tokens and component styles once, then import the package to register all elements:

import '@gnome-ui/core/styles';
import '@gnome-ui/web-components/styles';
import '@gnome-ui/web-components';

Granular entry points register only one element:

import '@gnome-ui/web-components/avatar';
import '@gnome-ui/web-components/badge';
import '@gnome-ui/web-components/button';
import '@gnome-ui/web-components/callout';
import '@gnome-ui/web-components/checkbox';
import '@gnome-ui/web-components/choice-card-group';
import '@gnome-ui/web-components/combo-row';
import '@gnome-ui/web-components/copy-button';
import '@gnome-ui/web-components/dialog';
import '@gnome-ui/web-components/divider';
import '@gnome-ui/web-components/dropdown';
import '@gnome-ui/web-components/expander';
import '@gnome-ui/web-components/expander-row';
import '@gnome-ui/web-components/field-group';
import '@gnome-ui/web-components/file-drop-zone';
import '@gnome-ui/web-components/file-type-icon';
import '@gnome-ui/web-components/highlight';
import '@gnome-ui/web-components/icon-button';
import '@gnome-ui/web-components/kbd';
import '@gnome-ui/web-components/level-bar';
import '@gnome-ui/web-components/menu';
import '@gnome-ui/web-components/otp-input';
import '@gnome-ui/web-components/popover';
import '@gnome-ui/web-components/progress-bar';
import '@gnome-ui/web-components/radio-group';
import '@gnome-ui/web-components/rating-stars';
import '@gnome-ui/web-components/slider';
import '@gnome-ui/web-components/spin-button';
import '@gnome-ui/web-components/spinner';
import '@gnome-ui/web-components/step-indicator';
import '@gnome-ui/web-components/switch';
import '@gnome-ui/web-components/switch-row';
import '@gnome-ui/web-components/tab-bar';
import '@gnome-ui/web-components/text-field';
import '@gnome-ui/web-components/text-truncate';
import '@gnome-ui/web-components/toast';
import '@gnome-ui/web-components/view-switcher';

Every registration function is idempotent. Importing these modules during SSR is safe; registration occurs only when the Custom Elements registry exists.

Avatar

<gnome-avatar name="Ada Lovelace" size="lg">
  <img data-slot="avatar-image" src="/ada.jpg" />
</gnome-avatar>

<!-- No image (or a broken one): shows initials derived from name -->
<gnome-avatar name="Grace Hopper"></gnome-avatar>

gnome-avatar composes a real light-DOM <img data-slot="avatar-image"> when the consumer wants a photo — its own native error/load events drive the fallback, matching ordinary browser image-loading behavior. Unlike every other component in this package, initials are always derived from name (up to two characters, via the same algorithm as @gnome-ui/react), so there is nothing for a consumer to author: the host manages a [data-slot="avatar-initials"] element itself, adopting one already present in light DOM (e.g. from server-rendered markup) or creating one.

The host defaults alt on the image from name (without overwriting an explicit alt), and always keeps its own aria-label in sync from the image's alt or name. color ("blue" | "green" | "yellow" | "orange" | "red" | "purple" | "brown" | "teal" | "slate") overrides the deterministic name-derived color; size accepts "sm" | "md" | "lg" | "xl" (default "md"). getInitials and hashNameToColor are exported for consumers who need the same derivation outside the component.

Badge

<gnome-badge>3</gnome-badge>
<gnome-badge variant="success">12</gnome-badge>
<gnome-badge dot variant="error"></gnome-badge>

<!-- Anchored over another element: the consumer's own wrapper must be
     position: relative (or similar) — the badge only knows about itself. -->
<span style="position: relative; display: inline-flex;">
  <gnome-icon-button aria-label="Notifications">🔔</gnome-icon-button>
  <gnome-badge anchored variant="error">3</gnome-badge>
</span>

gnome-badge is a pure CSS host — no lifecycle logic, no light-DOM management. variant ("accent" | "success" | "warning" | "error" | "neutral", default "accent"), dot, and anchored are plain attributes read directly by CSS. dot renders a small dot and visually collapses any light-DOM text content (font-size: 0) rather than removing it — keep dot badges empty. anchored only switches the badge to position: absolute; top; right;; positioning it correctly over a sibling still requires the consumer's own wrapper to establish the positioning context.

Button

<gnome-button variant="suggested" size="md">
  <button
    type="submit"
    name="intent"
    value="save"
    data-slot="button-control"
    hx-post="/settings"
  >
    Save changes
  </button>
</gnome-button>

gnome-button deliberately composes a native light-DOM <button> instead of reimplementing button semantics on the custom element. Native form submission, keyboard activation, accessible naming, name/value, and htmx attributes therefore continue to work normally.

The host accepts:

  • variant="default|suggested|destructive|flat|raised"
  • size="sm|md|lg"
  • shape="default|pill|circular"
  • Boolean disabled, loading, and osd attributes

loading sets aria-busy="true" and temporarily disables the native control. Consumer-owned disabled and aria-busy values are restored when the state ends or htmx replaces the control. The host's focus() and click() methods delegate to the native button.

Icon Button

<gnome-icon-button variant="flat" label="Close panel">
  <button type="button" data-slot="icon-button-control">
    <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
      <path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="1.5" fill="none" />
    </svg>
  </button>
</gnome-icon-button>

gnome-icon-button composes a native light-DOM <button>, same as gnome-button, but is always circular — an icon-only control has no text that would make a rectangular shape meaningful, so shape isn't exposed. Any SVG or <img> child of the control is sized automatically from size.

The host accepts:

  • variant="default|suggested|destructive|flat|raised"
  • size="sm|md|lg"
  • label — required accessible name, synced onto the control's aria-label (restored to any consumer-authored value once label is removed or the control is swapped, e.g. by htmx)
  • Boolean disabled, loading, and osd attributes

loading sets aria-busy="true", disables the native control, and hides the icon in favor of a centered spinner. The host's focus() and click() methods delegate to the native button.

Checkbox

<label>
  <gnome-checkbox>
    <input type="checkbox" data-slot="checkbox-control" />
  </gnome-checkbox>
  Select item
</label>

<script type="module">
  const checkbox = document.querySelector('gnome-checkbox');
  const control = checkbox.querySelector('input');

  control.addEventListener('change', () => {
    console.log('Selected:', control.checked);
  });

  // "Select all" pattern: apply indeterminate on the host, not the input.
  checkbox.indeterminate = true;
</script>

gnome-checkbox composes a native light-DOM <input type="checkbox">. Native form participation, the checked property, and change/input events continue to work normally — listen on the input directly or on the host.

indeterminate has no HTML attribute equivalent, so the host applies it to the control imperatively whenever the indeterminate attribute or property is set on <gnome-checkbox>. As with native checkboxes, user interaction (a click or Space) resolves the visual indeterminate state on the control; update the host's indeterminate property again if your "select all" logic still considers the group mixed.

The host accepts a boolean disabled attribute, which disables the native control while preserving any disabled state the consumer set directly on it once the host's disabled attribute is removed. focus() and click() delegate to the native control.

Choice Card Group

<gnome-choice-card-group label="Account type" helper-text="You can change this later.">
  <button type="button" role="radio" aria-checked="true" data-value="personal">
    <span data-slot="choice-card-title">Personal</span>
    <span data-slot="choice-card-description">For individual use</span>
  </button>
  <button type="button" role="radio" aria-checked="false" data-value="team">
    <span data-slot="choice-card-title">Team</span>
    <span data-slot="choice-card-description">For small groups</span>
  </button>
</gnome-choice-card-group>

<script type="module">
  const group = document.querySelector('gnome-choice-card-group');

  group.addEventListener('click', (event) => {
    const card = event.target.closest('[role="radio"]');
    if (!card || card.disabled) return;

    for (const sibling of group.querySelectorAll('[role="radio"]')) {
      sibling.setAttribute('aria-checked', String(sibling === card));
    }
  });
</script>

gnome-choice-card-group is a card-based single-choice selector (role="radiogroup") — large selectable cards instead of radio buttons, for account-type/template pickers in welcome/setup flows. It combines two behaviors this package already has separately: wraps a real <fieldset>/<legend> plus a label/helper-text/error hint, same technique as gnome-field-group (original light-DOM children — the card <button>s — are moved, once, into a generated <div role="radiogroup" data-slot="choice-card-group-grid"> inside the fieldset); and roving-tabindex keyboard navigation with automatic activation, same algorithm as gnome-view-switcher (all four arrow keys cycle, Home/End jump to the ends, moving focus also clicks the target card).

Same division of responsibility as gnome-tab-bar/gnome-view-switcher: requires descendants marked role="radio" (real <button>s recommended) — the host does not create cards or manage aria-checked itself, only the surrounding fieldset chrome and keyboard navigation. Style hooks on each card use [role="radio"] directly (not a redundant data-slot), matching gnome-tab-bar's [role="tab"] convention. A decorative [data-slot="choice-card-dot"] is injected into each card automatically if missing, same pattern as gnome-expander-row's auto-injected chevron. Nest a [data-slot="choice-card-icon"] element for an optional leading icon — the slot only sets color, so any inline SVG or gnome-file-type-icon works.

Switch

<label>
  Wi-Fi
  <gnome-switch>
    <input type="checkbox" role="switch" data-slot="switch-control" />
  </gnome-switch>
</label>

<script type="module">
  const control = document.querySelector('gnome-switch input');

  control.addEventListener('change', () => {
    console.log('Wi-Fi is now', control.checked ? 'on' : 'off');
  });
</script>

gnome-switch composes a native light-DOM <input type="checkbox"> instead of reimplementing toggle semantics on the custom element. The consumer marks it role="switch" and data-slot="switch-control"; native form participation, the checked property, and change/input events continue to work normally — listen on the input directly or on the host, since light DOM lets the events bubble through either.

The host accepts a boolean disabled attribute, which disables the native control while preserving any disabled state the consumer set directly on it once the host's disabled attribute is removed. gnomeSwitch.checked proxies to the native control's checked property; focus() and click() delegate to it as well.

Tab Bar

<gnome-tab-bar aria-label="Settings sections">
  <button role="tab" aria-selected="true">General</button>
  <button role="tab" aria-selected="false">Notifications</button>
  <button role="tab" aria-selected="false">Privacy</button>
</gnome-tab-bar>

<script type="module">
  const tabBar = document.querySelector('gnome-tab-bar');

  tabBar.addEventListener('click', (event) => {
    const tab = event.target.closest('[role="tab"]');
    if (!tab) return;

    for (const other of tabBar.querySelectorAll('[role="tab"]')) {
      other.setAttribute('aria-selected', String(other === tab));
    }
  });
</script>

gnome-tab-bar defaults to role="tablist" and only manages roving-tabindex keyboard navigation — Left/Right/Home/End move focus between descendants marked role="tab" (real <button>s recommended, so disabled is free). Same division of responsibility as @gnome-ui/react's TabBar: the host does not create tabs, change aria-selected, or show/hide panels — the consumer owns selection state and panel visibility, same as clicking any other button.

The host mirrors aria-selected onto tabIndex the same way gnome-radio-group mirrors native checked: whichever tab has aria-selected="true" becomes the roving-tabindex stop (falling back to the first enabled tab if none is selected), kept in sync via a MutationObserver so it also works after an htmx/Turbo swap. The boolean inline attribute removes the header-bar background for use inside a card or content area.

When there are more tabs than fit, the host generates two position: sticky scroll buttons of its own (prepended/appended once on connect, alongside the consumer's own [role="tab"] children, which are never moved or wrapped) — each fades in only while there's genuinely more to reveal in that direction, so a mouse/trackpad user (or anyone who hasn't discovered the native horizontal swipe/scroll — the bar's own scrollbar is hidden via CSS) still has an explicit way to reach the rest. RTL-aware: which physical edge each button sticks to, which way a click scrolls, and which way its chevron points all follow the bar's resolved direction, not a hardcoded left/right.

View Switcher

<gnome-view-switcher aria-label="View switcher">
  <button role="radio" aria-checked="true">List</button>
  <button role="radio" aria-checked="false">Grid</button>
  <button role="radio" aria-checked="false">Timeline</button>
</gnome-view-switcher>

<script type="module">
  const viewSwitcher = document.querySelector('gnome-view-switcher');

  viewSwitcher.addEventListener('click', (event) => {
    const item = event.target.closest('[role="radio"]');
    if (!item) return;

    for (const other of viewSwitcher.querySelectorAll('[role="radio"]')) {
      other.setAttribute('aria-checked', String(other === item));
    }
  });
</script>

gnome-view-switcher defaults to role="radiogroup" — same division of responsibility as gnome-tab-bar (the host doesn't create items or manage aria-checked, and mirrors it onto roving tabIndex the same way gnome-radio-group mirrors native checked), but with two real differences: all four arrow keys cycle (Left/Up move back, Right/Down move forward) instead of a tablist's horizontal-only nav, and — since a radiogroup uses "automatic activation" — moving focus with an arrow key also clicks the target item, mirroring @gnome-ui/react's ViewSwitcher.

Radio Group

<gnome-radio-group name="view-mode">
  <label><input type="radio" data-slot="radio-control" value="list" checked /> List</label>
  <label><input type="radio" data-slot="radio-control" value="grid" /> Grid</label>
  <label><input type="radio" data-slot="radio-control" value="compact" /> Compact</label>
</gnome-radio-group>

<script type="module">
  const group = document.querySelector('gnome-radio-group');

  group.addEventListener('gnome-change', (event) => {
    console.log('View mode:', event.detail.value);
  });
</script>

gnome-radio-group composes native light-DOM <input type="radio"> controls marked data-slot="radio-control". Same-name native radios already provide mutual exclusivity, arrow-key cycling, and Space/click selection in every browser, so the host does not reimplement that — it only:

  • Assigns a shared name to every control: the host's own name attribute if set, otherwise an auto-generated one, so groups work correctly even without an explicit name.
  • Mirrors a group-level disabled attribute onto every control, preserving any disabled state a consumer set directly on one of them.
  • Normalizes selection into a value property (get the checked control's value, or set it to check the matching control) and a gnome-change event with { value }, dispatched whenever any control's native change fires.

Text Field

<gnome-text-field>
  <label data-slot="text-field-label">Username</label>
  <input type="text" data-slot="text-field-control" placeholder="octocat" />
  <span data-slot="text-field-hint">Choose a unique handle.</span>
</gnome-text-field>

gnome-text-field composes a native light-DOM <input> or <textarea> marked data-slot="text-field-control", with optional <label data-slot="text-field-label"> and data-slot="text-field-hint" slots. Consumers own all label and hint text content; the host only wires ARIA relationships and state:

  • Links the label to the control via for/id (auto-generated if the control has none).
  • Links the hint to the control via aria-describedby (auto-generated), removing it if the hint is removed.
  • Mirrors a group-level disabled attribute onto the control, dimming the whole field, while preserving any disabled state set directly on the control.
  • Mirrors an invalid boolean attribute onto the control's aria-invalid and applies the error visual treatment to the control and hint — consumers still choose what message the hint displays.

value proxies to the native control's value; focus(), checkValidity(), reportValidity(), setCustomValidity(), and validity delegate to it as well, so standard HTML5 form validation keeps working.

Slider

<gnome-slider>
  <input
    type="range"
    data-slot="slider-control"
    aria-label="Brightness"
    min="0"
    max="100"
    value="50"
  />
</gnome-slider>

Unlike the React Slider (a synthetic role="slider" widget built on pointer-event handlers), gnome-slider composes a real <input type="range"> marked data-slot="slider-control" — native pointer/touch dragging, ArrowLeft/Right/Up/Down, Home/End, Page Up/Down, and form participation all keep working without any JS. The host only recomputes a --gnome-slider-fill custom property on every input event so CSS can paint the accent-colored fill up to the thumb on WebKit/Blink (Firefox paints it natively via ::-moz-range-progress).

value proxies to the control's valueAsNumber; focus() delegates to it as well.

Spin Button

<gnome-spin-button>
  <button type="button" data-slot="spin-button-decrement" aria-hidden="true" tabindex="-1">
    −
  </button>
  <input
    type="number"
    data-slot="spin-button-control"
    aria-label="Volume"
    min="0"
    max="10"
    value="5"
  />
  <button type="button" data-slot="spin-button-increment" aria-hidden="true" tabindex="-1">
    +
  </button>
</gnome-spin-button>

Unlike the React SpinButton (a synthetic role="spinbutton" widget), gnome-spin-button composes a real <input type="number"> marked data-slot="spin-button-control" — native min/max/step constraint validation, typing, and ArrowUp/ArrowDown stepping all keep working. The decrement/increment buttons (data-slot="spin-button-decrement" / data-slot="spin-button-increment") are marked aria-hidden="true" and tabindex="-1" since the native control is already the single keyboard- and screen-reader-facing target; the host wires their clicks to the control's stepDown()/stepUp(), dispatches input/change on the control afterward, and keeps each button's disabled state in sync with the control's bounds (and with each other, and the control itself, when the host's own disabled attribute is set).

value proxies to the control's valueAsNumber; focus() delegates to it as well.

Spinner

<gnome-spinner size="lg"></gnome-spinner>
<gnome-spinner label="Fetching results…"></gnome-spinner>
<gnome-spinner label=""></gnome-spinner>
<!-- silenced: a sibling already announces the loading state -->

gnome-spinner is purely presentational — no light-DOM children, no interaction. The host itself is the spinning ring, and manages its own ARIA: role="status" (unless the consumer already set a role) and aria-label from the label attribute, defaulting to "Loading…". Set label="" to silence it (aria-hidden="true") when a sibling element already announces the loading state. size accepts "sm" | "md" | "lg" (default "md"). The animation respects prefers-reduced-motion.

Progress Bar

<gnome-progress-bar aria-label="Download progress" value="0.6"></gnome-progress-bar>
<gnome-progress-bar aria-label="Download progress" variant="success"></gnome-progress-bar>
<!-- indeterminate: omit value -->
<gnome-progress-bar aria-label="Download progress"></gnome-progress-bar>

gnome-progress-bar is purely presentational — no light-DOM children. Native <progress> has no reliably cross-browser-stylable way to paint a custom-colored indeterminate pulse (there's no value to hook a percentage-based pseudo-element onto), so — like gnome-spinner — the host manages role="progressbar" and aria-valuenow/aria-valuemin/ aria-valuemax itself and paints the fill through CSS driven by a --gnome-progress-value custom property.

Set value between 0 and 1 for the determinate state (out-of-range values are clamped); omit it (or remove the attribute) for the indeterminate pulsing state. variant accepts "accent" | "success" | "warning" | "error" (default "accent"). The transition and pulse animation both respect prefers-reduced-motion.

Level Bar

<gnome-level-bar
  aria-label="Disk usage"
  value="0.85"
  low="0.2"
  high="0.8"
></gnome-level-bar>

<!-- discrete mode: signal-strength style blocks -->
<gnome-level-bar
  aria-label="Signal strength"
  discrete
  num-blocks="5"
  value="0.6"
></gnome-level-bar>

gnome-level-bar is role="meter" — the WAI-ARIA role for a scalar measurement within a known range, distinct from gnome-progress-bar's role="progressbar". Use it for a gauge (disk usage, battery, signal strength), not task progress.

The host accepts:

  • value, min (default 0), max (default 1)
  • low / low-variant (default "warning") — colour used at or below the low threshold
  • high / high-variant (default "error") — colour used at or above the high threshold
  • variant (default "accent") — colour between the two thresholds; accepts "accent" | "success" | "warning" | "error"
  • Boolean discrete + num-blocks (default 10)

In continuous mode the fill is painted through CSS driven by a --gnome-level-value custom property, same technique as gnome-progress-bar. In discrete mode there is nothing for a consumer to author — like gnome-skeleton's text variant rows, the host derives num-blocks [data-slot="level-block"] elements itself and toggles data-filled up to the current fraction.

Linked Group

<gnome-linked-group>
  <gnome-button>
    <button type="button" data-slot="button-control">Cut</button>
  </gnome-button>
  <gnome-button>
    <button type="button" data-slot="button-control">Copy</button>
  </gnome-button>
  <gnome-button>
    <button type="button" data-slot="button-control">Paste</button>
  </gnome-button>
</gnome-linked-group>

<!-- Stack vertically -->
<gnome-linked-group vertical>…</gnome-linked-group>

Renders children as a single visually-connected unit with no gap and merged borders — the canonical GNOME pattern for button groups and segmented inputs, mirroring the libadwaita .linked style class. Pure CSS host: vertical is a plain attribute read directly by CSS, same rationale as gnome-badge.

Unlike the React version, whose children ARE the real bordered DOM elements, gnome-button is a two-level host (its border/radius lives on the inner [data-slot="button-control"], not the host itself) — the border-merge CSS matches that control slot explicitly, alongside any bare native <button>, <input>, <select>, <textarea>, or <a> passed directly as a child. Works with any gnome-button variant, and mixes cleanly with other widget types (e.g. a native <input> beside a gnome-button).

Skeleton

<gnome-skeleton width="240" height="20"></gnome-skeleton>
<gnome-skeleton variant="circle" size="48"></gnome-skeleton>
<gnome-skeleton variant="text" lines="3"></gnome-skeleton>

gnome-skeleton is a loading placeholder — purely presentational, always aria-hidden. Like gnome-avatar's initials, the text variant's row elements ([data-slot="skeleton-line"]) are entirely host-derived from lines; there is nothing for a consumer to author, so — unlike gnome-avatar — no MutationObserver is needed, since nothing external ever swaps this content.

variant accepts "rect" | "circle" | "text" (default "rect"). width (default "100%") and height (default 16) apply to the rect variant; size (default 40) sets the diameter for circle; lines (default 3, minimum 1) sets the row count for text, whose last row renders at 60% width. Bare numeric values for width/height/size are treated as pixels; any other CSS length ("12rem", "2em") is used as-is. animated defaults to true — set animated="false" to disable the shimmer regardless of prefers-reduced-motion (which also disables it automatically).

Separator

<gnome-separator></gnome-separator>

<!-- Inside a flex row: stretches via align-self: stretch -->
<div style="display: flex; align-items: center; gap: 12px; height: 32px;">
  <span>Files</span>
  <gnome-separator orientation="vertical"></gnome-separator>
  <span>Music</span>
</div>

Unlike the React Separator — which renders a semantic <hr> for the horizontal case and a <div role="separator"> for the vertical one — a custom element is always a single fixed tag, so gnome-separator manages role="separator" and aria-orientation itself for both orientations (guarded so it never overwrites a consumer-authored role). orientation accepts "horizontal" | "vertical" (default "horizontal"; aria-orientation is only set for "vertical", matching the ARIA default). Color comes entirely from design tokens and adapts to dark mode automatically.

Divider

<gnome-divider label="OR"></gnome-divider>

<!-- Bare, no label -->
<gnome-divider></gnome-divider>

gnome-divider is purely presentational — no light-DOM children for the consumer to author. The label attribute drives an aria-label and a host-derived centered [data-slot="divider-label"] span between two [data-slot="divider-line"] segments; the whole structure is fully rebuilt from label alone, same rationale as gnome-level-bar's discrete blocks. Distinct from gnome-separator, which is a bare dividing line with no label but supports a vertical orientation — this component doesn't.

Highlight

<gnome-highlight text="The quick brown fox jumps over the lazy dog." query="quick fox">
</gnome-highlight>

gnome-highlight wraps every occurrence of query within text in a <mark> element — pairs with gnome-dropdown's option list or any filterable list to show users which part of a result matched what they typed. Purely presentational — no light-DOM children for the consumer to author; the whole structure (plain text interleaved with <mark> matches) is fully rebuilt from text/query/case-sensitive, same rationale as gnome-divider.

query splits on whitespace into individual terms, mirroring @gnome-ui/react's Highlight, whose most common multi-term use case is highlighting each word of a multi-word search query. case-sensitive (boolean, default false) matches case-sensitively when set.

Step Indicator

<!-- Unlabelled: shows a "Step X of Y" caption -->
<gnome-step-indicator steps="4" current="1"></gnome-step-indicator>

<!-- Labelled: shows each step's label instead of the caption -->
<gnome-step-indicator steps="Account, Profile, Confirm" current="0" clickable>
</gnome-step-indicator>

<script type="module">
  document.querySelector('gnome-step-indicator[clickable]').addEventListener(
    'gnome-select',
    (event) => {
      // event.detail.step — jump back to a completed step.
    },
  );
</script>

gnome-step-indicator is a numbered "Step X of Y" progress indicator for onboarding/wizard flows — steps are numbered circles connected by a progress line, with the completed portion tinted in the accent color. Purely attribute-driven and fully host-derived — nothing for the consumer to author, same rationale as gnome-divider/gnome-highlight.

steps accepts either a plain count (steps="4", unlabelled — only the caption is shown, mirroring @gnome-ui/react's StepIndicator accepting a plain number) or a comma-separated label list (mirroring its string[] case). current is the zero-based active step index (clamped to the valid range). Completed steps are only clickable when clickable is set (mirrors react's onStepClick being optional to make steps non-interactive) — clicking one emits gnome-select with { step }; the current and upcoming steps are never clickable. orientation accepts "horizontal" | "vertical" (default "horizontal").

Kbd

Press <gnome-kbd>Enter</gnome-kbd> to confirm, or <gnome-kbd>Esc</gnome-kbd>
to cancel.

<!-- Unknown key names render as-is -->
<gnome-kbd>F5</gnome-kbd>

<!-- Raw text instead of the normalised symbol -->
<gnome-kbd raw>Enter</gnome-kbd>

gnome-kbd is a standalone single key-cap for inline instructional text — the key name is authored as plain light-DOM text, same as the native <kbd> element, and captured once on first connect. Common key names (Ctrl, Shift, Alt, Enter, Esc, arrow keys, and more) normalise to their Unicode symbol unless the raw boolean attribute is set; when a symbol is substituted, the original name moves to aria-label so assistive tech still announces "Enter" rather than "↵". Set the key property (not .textContent) for programmatic updates after the initial render, since the component re-renders from the captured raw name rather than re-reading its own generated content.

Banner

<gnome-banner variant="warning">
  <span data-slot="banner-message">Your session will expire in 5 minutes.</span>
  <span data-slot="banner-actions">
    <button type="button" data-action="extend">Extend session</button>
    <button type="button" data-dismiss aria-label="Dismiss">×</button>
  </span>
</gnome-banner>

<script type="module">
  document.querySelector('gnome-banner').addEventListener('gnome-action', (event) => {
    if (event.detail.action === 'extend') {
      // Extend the session.
    }
  });
</script>

gnome-banner is a persistent message strip, meant to sit at the top of a view until the user acts or dismisses it. variant ("info" | "warning" | "error" | "success", default "info") is a plain attribute read directly by CSS, same as gnome-badge/gnome-toast — there is no JS state to keep in sync. The host sets role="status"/aria-live="polite" itself (guarded, like every other auto-managed ARIA attribute in this package).

Mark a descendant data-action to emit gnome-action with { action: string } (the button's own data-action value, or "default" if empty) — unlike gnome-toast, clicking one does not dismiss the banner, since a banner persists until its underlying condition is resolved, not just until the next action. Mark a descendant data-dismiss to call dismiss(), which fires a cancelable gnome-before-dismiss followed by gnome-dismiss and hides the banner (hidden) rather than removing it from the DOM — remove it yourself in a gnome-dismiss listener if that's what you want.

Callout

<gnome-callout variant="warning">
  <svg data-slot="callout-icon" width="16" height="16" aria-hidden="true">…</svg>
  <span>Double-check this value before continuing.</span>
  <button type="button" data-dismiss aria-label="Dismiss">×</button>
</gnome-callout>

<script type="module">
  document.querySelector('gnome-callout').addEventListener('gnome-dismiss', () => {
    // You decide what happens next — hide it, remove it, etc.
  });
</script>

gnome-callout is an inline, dismissible admonition box for contextual help text within forms and cards — distinct from gnome-banner (a persistent, edge-to-edge strip at the top of a view) and gnome-toast (a temporary notification). variant ("info" | "warning" | "tip", default "info") is a plain attribute read directly by CSS, same as gnome-banner/gnome-badge.

Mark a descendant data-dismiss to emit gnome-dismiss on click. Unlike gnome-banner, the host never hides itself — there's no internal open/closed state to manage, matching @gnome-ui/react's Callout, whose onDismiss is a plain notification callback that leaves visibility entirely to the consumer. An optional icon goes in data-slot="callout-icon"; there is no default/built-in icon.

Dialog

<button id="delete-trigger" type="button">Delete</button>

<gnome-dialog id="delete-dialog" close-on-backdrop>
  <section data-slot="dialog-surface">
    <header data-slot="dialog-header">
      <h2 data-slot="dialog-title">Delete item?</h2>
      <p data-slot="dialog-description">This action cannot be undone.</p>
    </header>

    <div data-slot="dialog-actions">
      <button type="button" autofocus data-cancel>Cancel</button>
      <button type="button" data-confirm>Delete</button>
    </div>
  </section>
</gnome-dialog>

<script type="module">
  const dialog = document.querySelector('#delete-dialog');

  document.querySelector('#delete-trigger').addEventListener('click', () => {
    dialog.showModal();
  });

  dialog.querySelector('[data-cancel]').addEventListener('click', () => {
    dialog.close();
  });
</script>

close-on-backdrop is opt-in. Escape and backdrop requests emit a cancelable gnome-cancel event before closing. Dialogs can be stacked; only the most recently opened dialog remains interactive, and closing it restores the previous modal and its focus.

Dropdown

<gnome-dropdown placeholder="Select a theme">
  <button type="button" data-slot="dropdown-trigger"></button>
  <ul data-slot="dropdown-content">
    <li data-option data-value="light">Light</li>
    <li data-option data-value="dark">Dark</li>
    <li data-option data-value="hc" aria-disabled="true">High contrast</li>
  </ul>
</gnome-dropdown>

<script type="module">
  document.querySelector('gnome-dropdown').addEventListener('gnome-change', (event) => {
    console.log('Selected:', event.detail.value);
  });
</script>

gnome-dropdown combines gnome-menu's internals — light-DOM trigger/ content, floating position with flip, outside-pointer/Escape/Tab dismissal, geometry tracking — with a trigger styled as a <select>. Two real differences from gnome-menu: focus never leaves the trigger (the standard role="combobox" pattern tracks the active role="option" through aria-activedescendant instead of moving DOM focus per item), and selection is single-value, tracked via the value attribute and mirrored onto each option's aria-selected the same way gnome-radio-group mirrors native checked.

The host manages the trigger's visible text itself, via a [data-slot="dropdown-value"] span it adopts or creates — nothing for the consumer to author there, since it's fully derived from value/ placeholder. Options only need data-option + data-value; wrap the label in [data-slot="option-label"] if you also want a secondary [data-slot="option-description"] line. disabled/aria-disabled="true" options are skipped during keyboard navigation and ignored on click.

Fires a non-cancelable gnome-change ({ value }) on selection and gnome-open-change/gnome-close ({ reason }) on open-state transitions, same shape as gnome-menu's events.

Menu

<gnome-menu placement="bottom">
  <button type="button" data-slot="menu-trigger">Project options</button>

  <section data-slot="menu-content">
    <span data-slot="menu-label">Project</span>
    <button type="button" data-menu-item data-value="rename">Rename</button>
    <button type="button" data-menu-item disabled>Duplicate</button>
    <button type="button" data-menu-item data-value="archive">
      Archive
      <span data-slot="menu-shortcut" aria-hidden="true">⇧⌘A</span>
    </button>
    <hr data-slot="menu-separator" />
    <a href="/settings" data-menu-item data-value="settings">
      Project settings
    </a>
  </section>
</gnome-menu>

<script type="module">
  document.querySelector('gnome-menu').addEventListener('gnome-select', (event) => {
    if (event.detail.value === 'archive' && !window.confirm('Archive project?')) {
      event.preventDefault();
    }
  });
</script>

Use semantic buttons and links for items and mark each with data-menu-item. The component adds the menu roles and trigger relationships, skips disabled items, supports Arrow keys, Home, End, and typeahead, and restores trigger focus after Escape or selection. Add data-keep-open when an item should not close the menu.

Otp Input

<gnome-otp-input label="Verification code" helper-text="We sent a 6-digit code to your email.">
</gnome-otp-input>

<script type="module">
  const otp = document.querySelector('gnome-otp-input');

  otp.addEventListener('gnome-change', (event) => {
    console.log('Value:', event.detail.value);
  });

  otp.addEventListener('gnome-complete', (event) => {
    console.log('Complete:', event.detail.value);
  });

  otp.length = 6; // default
  otp.value = '123'; // pre-fill programmatically
</script>

gnome-otp-input is a segmented PIN/verification-code input — one cell per digit, with auto-advance on typing, backspace-to-previous-cell, and paste support (pasting a full code distributes it across the remaining cells). Fully host-generated, same rationale as gnome-field-group: nothing for the consumer to author — the fieldset/legend/hint chrome and every [data-slot="otp-input-cell"] are built from attributes alone.

value is a JS-only property, not an attribute — mirrors gnome-radio-group's value, since it changes on every keystroke and reflecting it as an attribute would be wasted churn. Fires gnome-change ({ value }) on every user-driven edit and gnome-complete ({ value }) once the value reaches length digits (default 6) — the complete check compares against the previous value, so it won't re-fire on an unchanged complete value but fires again if the value leaves and returns to a (possibly different) complete value, matching @gnome-ui/react's OtpInput effect semantics exactly. masked renders type="password" cells; disabled disables every cell (and the wrapping <fieldset>).

Toast

<gnome-toast id="saved-toast" duration="5000" variant="success">
  <span data-slot="toast-title">Changes saved</span>
  <span data-slot="toast-actions">
    <button type="button" data-action="undo">Undo</button>
    <button type="button" data-dismiss aria-label="Dismiss notification">×</button>
  </span>
</gnome-toast>

<script type="module">
  const toast = document.querySelector('#saved-toast');
  toast.show();

  toast.addEventListener('gnome-action', (event) => {
    if (event.detail.action === 'undo') {
      // Undo the operation.
    }
  });
</script>

Set duration="0" for a persistent toast. gnome-before-dismiss is cancelable; gnome-dismiss reports the final dismissal reason.

Toolbar

<gnome-toolbar>
  <gnome-button variant="flat">
    <button type="button" data-slot="button-control">←</button>
  </gnome-button>
  <gnome-linked-group>
    <gnome-button variant="flat">
      <button type="button" data-slot="button-control">Bold</button>
    </gnome-button>
    <gnome-button variant="flat">
      <button type="button" data-slot="button-control">Italic</button>
    </gnome-button>
  </gnome-linked-group>
  <gnome-separator orientation="vertical" style="height: 24px"></gnome-separator>
  <gnome-dropdown>…</gnome-dropdown>
  <div style="flex: 1"></div>
  <gnome-button variant="suggested">
    <button type="button" data-slot="button-control">Save</button>
  </gnome-button>
</gnome-toolbar>

Horizontal action bar following the libadwaita .toolbar style class — pure CSS host with no attributes, no lifecycle logic. Provides the standard 6px padding/gap for rows of flat or raised buttons, gnome-linked-group clusters, dropdowns, and dividers; consumer-authored children render directly, in DOM order, same as the React version, which never sets a role. Use a plain flex: 1 filler <div> between leading and trailing groups to push trailing items to the end (mirrors the React version's <Spacer />, which this package does not port as a separate element since a bare <div> already does the job with no JS behind it).

Bin

<gnome-bin style="max-width: 480px; padding: 12px">
  <p>Some content constrained to a neutral wrapper</p>
</gnome-bin>

Single-child container with no visual styling of its own — mirrors AdwBin. Pure CSS host, no attributes, no lifecycle logic; the only rule it ships is display: block, since a custom element defaults to display: inline and this needs to match a plain <div>'s default layout behavior. Useful as a neutral base for applying layout or size constraints without introducing any chrome — every visual detail in the example above comes entirely from the style attribute, not from gnome-bin itself.

Popover

<gnome-popover placement="bottom">
  <button type="button" data-slot="popover-trigger">Options</button>

  <section data-slot="popover-content">
    <a href="/profile">Profile</a>
    <button type="button">Sign out</button>
  </section>
</gnome-popover>

The trigger receives aria-haspopup, aria-expanded, and aria-controls. The popover flips and clamps itself to remain inside the viewport.

Tooltip

<gnome-tooltip placement="top" delay="500">
  <button type="button" data-slot="tooltip-trigger" aria-label="Save">💾</button>
  <span data-slot="tooltip-content">Save file (Ctrl+S)</span>
</gnome-tooltip>

gnome-tooltip reuses the same computeFloatingPosition helper (and its flip/clamp/arrow-offset logic) as gnome-popover — set via internal/floating.ts, shared between the two. Shows on the trigger's mouseenter/focus after delay milliseconds (default 500; delay="0" for instant), hides on mouseleave/blur/Escape. placement accepts "top" | "bottom" | "left" | "right" (default "top"). The trigger's aria-describedby is wired to the content automatically (guarded — a consumer-authored aria-describedby is left alone).

Unlike gnome-popover, the content is never hidden — it stays laid out at all times with opacity: 0, fading in via [data-state="open"]. A display: none element can't transition, so this is what makes the fade/scale animation possible; as a side effect it also keeps the description available to aria-describedby readers even while the visual bubble is hidden, which the visibility: hidden fallback in the React version does not (that removes it from the accessibility tree between hovers). There is no focus trap and no dismiss/action events — a tooltip is purely informational.

Card

<gnome-card padding="md">
  <strong>Card title</strong>
  <p>Static content grouped on an elevated surface.</p>
</gnome-card>

<!-- Interactive: composes a real <button data-slot="card-surface"> -->
<gnome-card interactive aria-label="Open settings">
  <strong>Settings</strong>
  <p>Manage your account and preferences.</p>
</gnome-card>

A custom element is always one fixed tag, so unlike the React version — which renders <button> (or whatever as specifies) when interactive — gnome-card composes a real <button type="button" data-slot="card-surface"> around its existing children for native keyboard/click activation, moving them inside without cloning (so event listeners survive). Author your own data-slot="card-surface" (e.g. an <a> for a card that navigates) to use a different element; the host adopts it instead of generating one. Toggling interactive off unwraps the surface, moving children back onto the host directly. An aria-label set on the host is copied onto a generated surface (not onto an adopted one you authored yourself).

padding accepts "none" | "sm" | "md" | "lg" (default "md") and is a plain attribute read directly by CSS — no JS state to keep in sync.

Action Row

<gnome-action-row>
  <span data-slot="row-prefix">🔔</span>
  <span data-slot="row-title">Notifications</span>
  <span data-slot="row-subtitle">Manage app alerts</span>
  <span data-slot="row-suffix">
    <input type="checkbox" role="switch" aria-label="Notifications" checked />
  </span>
</gnome-action-row>

<!-- Interactive: composes a real <button data-slot="row-surface"> -->
<gnome-action-row interactive aria-label="Open Wi-Fi settings">
  <span data-slot="row-title">Wi-Fi</span>
  <span data-slot="row-subtitle">Home Network</span>
</gnome-action-row>

<script type="module">
  document.querySelector('gnome-action-row[interactive]').addEventListener('gnome-activate', () => {
    // Navigate or open a dialog.
  });
</script>

gnome-action-row groups data-slot="row-title" and data-slot="row-subtitle" into a generated (or adopted) data-slot="row-content" wrapper so they stack correctly next to row-prefix/row-suffix — this happens regardless of interactive.

Unlike the React version — which renders <button> around everything, including trailing, when interactive — gnome-action-row only composes a real <button data-slot="row-surface"> around row-prefix and row-content; row-suffix stays outside it. The React docs warn that a trailing Switch/Button needs manual stopPropagation() to avoid double-nesting inside the row's own <button> (invalid, inaccessible HTML) — here that problem doesn't exist structurally, since suffix is never inside the surface. The hover/active tint still spans the full row (:hover/:active on the host itself, which CSS already matches while the pointer/press is on any descendant, row-suffix included) so it looks the same as the React version; only the keyboard focus ring stays scoped to row-surface, since it must point at the exact element that has focus — that's the one place a row-suffix control (e.g. a Switch) visibly diverges, with its own focus ring instead of the row's.

Clicking (or keyboard-activating) the surface emits gnome-activate — already pre-filtered to real row activation, since clicks on a row-suffix control's own elements never reach the surface. Author your own data-slot="row-surface" (e.g. an <a> for a row that navigates) to use a different element; the host adopts it instead of generating one. variant="property" (default "default") flips the visual hierarchy — row-title shrinks to a dim caption label and row-subtitle becomes the prominent value — and, like padding on gnome-card, is a plain attribute read directly by CSS.

Combo Row

<gnome-combo-row>
  <span data-slot="row-title">Theme</span>
  <span data-slot="row-subtitle">Choose your preferred color scheme</span>

  <gnome-dropdown data-slot="row-suffix" value="dark">
    <button type="button" data-slot="dropdown-trigger"></button>
    <ul data-slot="dropdown-content">
      <li data-option data-value="light">Light</li>
      <li data-option data-value="dark">Dark</li>
    </ul>
  </gnome-dropdown>
</gnome-combo-row>

gnome-combo-row genuinely composes gnome-action-row's layout conventions (row-prefix/row-title/row-subtitle/row-suffix) with a real <gnome-dropdown> the consumer authors directly in data-slot="row-suffix" — no combobox logic is duplicated; the nested dropdown registers and behaves entirely on its own once connected. Unlike gnome-action-row, this row is never itself interactive (there's no row-level activation, only the nested dropdown trigger), so title/subtitle stacking is handled entirely by CSS grid areas — no JS-generated row-content wrapper is needed here.

The host's disabled attribute dims the row and forwards disabled to whichever element is marked data-slot="row-suffix", if it exposes a disabled property — works for gnome-dropdown, gnome-button, or a native control marked directly with that slot.

Switch Row

<gnome-switch-row>
  <span data-slot="row-prefix">🔔</span>
  <span data-slot="row-title">Notifications</span>
  <span data-slot="row-subtitle">Get notified about updates</span>
</gnome-switch-row>

<script type="module">
  document.querySelector('gnome-switch-row').addEventListener('gnome-change', (event) => {
    console.log('Checked:', event.detail.checked);
  });
</script>

gnome-switch-row is genuinely different from gnome-action-row composed with a nested gnome-switch: the entire row is the switch — clicking anywhere toggles it — matching @gnome-ui/react's SwitchRow, whose own docs call out that this differs from ActionRow + trailing Switch because the row itself is the interactive element. Composing a real <gnome-switch> (which wraps its own native <input>) inside the row's own <button> would nest two interactive controls — invalid, inaccessible HTML — so the host's generated <button data-slot="row-surface"> itself carries role="switch"/aria-checked, and the switch track/thumb is a small host-generated aria-hidden visual: nothing for the consumer to author, same rationale as gnome-avatar's initials.

Only row-title, row-subtitle, and row-prefix are consumer-authored (same slot names as gnome-action-row). Author your own data-slot="row-surface" (e.g. an <a> for a row that navigates) to use a different element — the host adopts it instead of generating one, and falls back to aria-disabled/tabindex="-1" for disabled when the adopted element has no native disabled property. Fires a non-cancelable gnome-change ({ checked }) on every toggle.

Expander

<gnome-expander label="Show advanced options">
  <p>These settings are rarely needed — change them only if you know what you're doing.</p>
</gnome-expander>

<script type="module">
  document.querySelector('gnome-expander').addEventListener('gnome-open-change', (event) => {
    console.log('Expanded:', event.detail.open);
  });
</script>

gnome-expander is a standalone disclosure triangle + collapsible content — mirrors GtkExpander. A bare, unstyled counterpart to gnome-expander-row, which is scoped to gnome-action-row's header-slot layout; use gnome-expander outside a settings-row context (e.g. "Show advanced options" in a form or dialog).

The header (a real <button data-slot="expander-header"> with a decorative chevron) is fully host-generated from the label attribute — nothing for the consumer to author there, same rationale as gnome-divider. All original light-DOM children are moved, once, into a generated <div data-slot="expander-panel" role="region">, height-animated with the same CSS grid 0fr/1fr transition as gnome-expander-row's panel. Fires gnome-open-change ({ open }) on toggle — same event name and detail shape as gnome-dialog/gnome-dropdown/gnome-menu/gnome-popover/ gnome-expander-row.

Expander Row

<gnome-expander-row>
  <span data-slot="row-title">Advanced settings</span>
  <span data-slot="row-subtitle">Configure additional options</span>
  <span data-slot="row-suffix">Beta</span>

  <gnome-action-row>
    <span data-slot="row-title">Enable telemetry</span>
  </gnome-action-row>
  <gnome-action-row>
    <span data-slot="row-title">Hardware acceleration</span>
  </gnome-action-row>
</gnome-expander-row>

<script type="module">
  document.querySelector('gnome-expander-row').addEventListener('gnome-open-change', (event) => {
    console.log('Expanded:', event.detail.open);
  });
</script>

gnome-expander-row is a collapsible gnome-action-row — same header slots (row-prefix/row-title/row-subtitle/row-suffix, grouped into a generated row-content), wrapped in a generated <button data-slot="row-surface"> plus a host-generated, aria-hidden row-chevron indicator. Any remaining light-DOM children (nested rows, e.g. <gnome-action-row>) are moved into a generated <div data-slot="row-panel" role="region">, height-animated with a CSS grid 0fr/1fr transition; dividers between child rows are pure CSS (border-top on every child but the first), same technique as gnome-boxed-list — no JS bookkeeping as rows are added or removed. If there's nothing to reveal, no panel is generated at all.

The expanded attribute uses AdwExpanderRow's own terminology, but fires gnome-open-change ({ open }) — the same event name and detail shape gnome-dialog/gnome-dropdown/gnome-menu/gnome-popover use for their own open-state transitions. Author your own data-slot="row-surface" to use a different element, same adoption pattern as gnome-action-row/gnome-switch-row.

Field Group

<gnome-field-group label="Notification method" helper-text="Choose how you want to be notified.">
  <label><input type="radio" name="notif" value="email" checked /> Email</label>
  <label><input type="radio" name="notif" value="sms" /> SMS</label>
  <label><input type="radio" name="notif" value="push" /> Push</label>
</gnome-field-group>

<!-- Error state: replaces helper-text, announced via role="alert" -->
<gnome-field-group label="Notification method" error="Select at least one notification method.">
  ...
</gnome-field-group>

gnome-field-group is a generic form-field grouping with a shared label, help text, and error message, for arbitrary fields outside a gnome-boxed-list — distinct from gnome-boxed-list/gnome-action-row, which are scoped to settings-page rows. It wraps a real <fieldset data-slot="field-group-fieldset">/<legend data-slot="field-group-legend"> — same rationale as gnome-highlight wrapping real <mark> and gnome-kbd wrapping real <kbd> — so disabled disables every descendant form control for free via native fieldset behavior, no need to thread it through each field manually.

All original light-DOM children (the fields themselves) are moved, once, into a generated <div data-slot="field-group-content"> inside the fieldset — same adopt-existing-children technique as gnome-expander-row's panel. error takes priority over helper-text when both are set, and the visible hint gets role="alert" only in the error case; the fieldset's aria-describedby points at whichever is shown.

File Type Icon

<!-- Resolved from the name extension -->
<gnome-file-type-icon name="report.pdf"></gnome-file-type-icon>

<!-- Resolved from a MIME type — takes precedence over name when both are set -->
<gnome-file-type-icon mime-type="image/png"></gnome-file-type-icon>

<!-- Folder, regardless of name/mime-type -->
<gnome-file-type-icon is-folder></gnome-file-type-icon>

<!-- Thumbnail instead of the resolved icon -->
<gnome-file-type-icon name="sunset.jpg" thumbnail="/preview.jpg" size="lg"></gnome-file-type-icon>

gnome-file-type-icon is a small role="img" icon — optionally a thumbnail — resolved from a file's MIME type or name extension, useful for file-manager-style listings. Falls back to the generic file icon (mirrors freedesktop's text-x-generic) when the type can't be resolved. Purely presentational and fully host-derived, same rationale as gnome-divider/gnome-highlight — nothing for the consumer to author.

Icon glyphs come from @gnome-ui/icons — framework-agnostic path data, this package's only runtime dependency besides @gnome-ui/core — and are built into an inline <svg data-slot="file-type-icon-svg"> the same way gnome-callout's story icons are: via DOMParser, not document.createElement('svg'), to avoid the HTML-namespace self-closing-tag parsing bug. label overrides the generated accessible name (e.g. "PDF document"); size accepts "sm" | "md" | "lg" (default "md") and sizes the host itself so swapping between the resolved icon and a thumbnail never shifts layout.

Boxed List

<gnome-boxed-list>
  <gnome-action-row>
    <span data-slot="row-title">Wi-Fi</span>
    <span data-slot="row-subtitle">Home Network</span>
    <span data-slot="row-suffix">
      <input type="checkbox" role="switch" aria-label="Wi-Fi" checked />
    </span>
  </gnome-action-row>
  <gnome-action-row>
    <span data-slot="row-title">Bluetooth</span>
    <span data-slot="row-subtitle">Off</span>
    <span data-slot="row-suffix">
      <input type="checkbox" role="switch" aria-label="Bluetooth" />
    </span>
  </gnome-action-row>
</gnome-boxed-list>

Rounded bordered list grouping row-shaped children (e.g. gnome-action-row, or any element) with merged borders and a single rounded outline. The host sets role="list" (guarded) and gives every direct child role="listitem" (also guarded, and re-applied to children added later — e.g. an htmx append — via a lightweight childList `Mutation