@gluonjs/atoms
Focused Gluon UI primitives plus the shared UI installation boundary, tokens, and themes.
Foundation atoms include AspectRatio, Avatar, FileInput, ScrollArea, and Separator. Accessibility utilities provide token-backed visually-hidden and :focus-visible classes without taking ownership of navigation or focus state. Each owns a separately tree-shakable stylesheet and exposes only component-scoped --gluon-* properties. They do not fetch data, load account records, upload images, virtualize content, or own product state.
@gluonjs/atoms at a glance
Runtime: browser · Release: 1.13.0
Documentation guide · npm · Source
Public API: @gluonjs/atoms
Install
npm install @gluonjs/atomsQuick start
import { AspectRatio, Avatar, ScrollArea, Separator, installUi } from '@gluonjs/atoms';
const ui = installUi(document, { theme: 'light' });
const media = AspectRatio({ ratio: 4 / 3, children: 'Product media' });
const avatar = Avatar({ alt: 'Ada Lovelace', fallback: 'AL', status: 'error' });
const history = ScrollArea({ label: 'Order history', children: 'Order rows' });
const rule = Separator({ decorative: true });
console.log(media, avatar, history, rule, ui.theme);Choose this package when
- Focused UI primitives and shared theming boundaries.
- Accessible form controls, image/fallback presentation, bounded native scrolling, separators, and theme installation.
- Shared styles for browser-facing Gluon interfaces.
Choose another boundary when:
- Does not own larger compositions or app shell layout.
- Depends on Quarks for native primitives and Core for runtime ownership.
Related documentation
import {
AspectRatio,
Avatar,
ScrollArea,
Separator,
installUi,
} from '@gluonjs/atoms';
const ui = installUi(document, { theme: 'light' });
const media = AspectRatio({ ratio: 4 / 3, children: productImage });
const avatar = Avatar({ src: profileImage, alt: 'Ada Lovelace', status: 'loaded' });
const history = ScrollArea({ label: 'Order history', children: orderRows });
const rule = Separator({ decorative: true });
ui.setTheme('dark');
ui.dispose();installUi() is the one public call for the shared cascade-layer order, Core foundation, UI tokens, active theme, and target-scoped styleOwner. It accepts a Document or ShadowRoot, exposes the current typed theme, changes the target-local theme without replacing its active stylesheet object, and disposes idempotently. Owners on one target are reference-counted. Existing adopted sheets retain their relative order, pre-adopted shared sheets are never removed, and the last owner restores a theme attribute only when Gluon still owns it.
createUiStyleSelection(theme) returns the same four named sheets for SSR. installUi(target, { theme, hydrate: true }) validates and consumes the matching gluon-ui carriers. Missing, duplicate, reordered, and content/digest-mismatched carriers throw UiHydrationError before target mutation. Importing the package never changes a document or shadow root, and no browser <style> fallback is provided.
Tenant-scoped CSS-variable themes
Applications that render more than one brand or tenant in the same document can install an isolated owner for each tenant scope:
import { installUi } from '@gluonjs/atoms';
const tenantRoot = document.querySelector<HTMLElement>('[data-tenant="acme"]')!;
const tenantUi = installUi(document, {
tenant: {
id: 'acme',
scope: tenantRoot,
theme: 'light',
tokens: {
'--gluon-color-action': '#2457d6',
'--gluon-radius-control': '0.25rem',
},
},
});
tenantUi.setTokens({ '--gluon-color-action': '#163aa0' });
tenantUi.setTheme('dark');
tenantUi.dispose();Tenant overrides are inherited CSS custom properties on the supplied scope; they never replace selectors, mutate another tenant, or require a Tailwind rebuild. Names must use the public --gluon-* namespace. A scope can have multiple reference-counted owners only when their tenant id, theme, and token configuration match. Serialize the tenant id, theme, and token contract with the application state before hydration and install the matching owner before the tenant subtree is hydrated.
AspectRatio, Avatar, Badge, Button, Checkbox, DateInput, FileInput, Heading, Icon, Image, Input, Label, Link, Meter, NumberInput, Progress, Radio, ScrollArea, Select, Separator, Skeleton, Slider, Spinner, StatusBadge, Switch, Text, Textarea, TimeInput, and ToggleButton expose immutable Component.styles metadata and have separately tree-shakable sheets. The renderer adopts only the sheets reachable from its active value tree and releases them with the render owner. Nested composition stays on that same path, so a public Molecule that calls Radio() directly still contributes the exact radioStyles sheet before the first measurable render and through hydration. atomStyles is deprecated; adopting it with exact rendering throws GLUON_LEGACY_COMPONENT_STYLE_CONFLICT rather than applying duplicate rules. installUiTheme() is deprecated in favor of installUi().
File and accessibility utilities
FileInput renders only the native file picker boundary. It exposes accept, multiple, capture, name, required, invalid, and disabled, while the application owns file reading, validation policy, upload transport, progress, and persistence. It never writes a file value programmatically.
accessibilityStyles is an opt-in constructable stylesheet. Apply visuallyHiddenAttributes() to preserve semantic content for assistive technology while revealing it when focused, and apply focusRingAttributes() to use the token-backed :focus-visible outline. Both utilities support --gluon-focus-width, --gluon-focus-offset, and --gluon-color-focus, plus forced-colors mode.
Concise app Atoms
Use defineUiAtom() for small presentational wrappers that would otherwise repeat prop partitioning, native-tag branching, and stylesheet metadata:
import { defineUiAtom } from '@gluonjs/atoms';
import { css } from '@gluonjs/core';
interface TextLinkProps {
readonly href?: string;
readonly children?: string;
}
export const TextLink = defineUiAtom<TextLinkProps, 'a' | 'span'>({
displayName: 'TextLink',
tag: ({ href }) => href ? 'a' : 'span',
style: {
id: 'shop-text-link',
sheet: css`:where(.shop-text-link) { text-underline-offset: 0.2em; }`,
},
nativeProps: ({ href, children }, tag) => ({
class: 'shop-text-link',
children,
...(tag === 'a' ? { href } : {}),
}),
});The component still returns an ordinary Gluon TemplateResult; the selected tag is rendered by quark(), and the optional sheet becomes ordinary immutable Atom style metadata. For a line-neutral native wrapper, omit nativeProps and all caller props are forwarded in one object.
During an incremental migration, { loose: true } additionally accepts legacy slot.content; normal children wins if both are supplied. Strict mode rejects slot.content instead of forwarding it as an accidental DOM attribute. defineUiAtom() is for stateless presentational Atoms only. Use defineAtom() and q.*() when a component needs several native nodes or precise prop partitioning, defineMolecule()/defineOrganism() for larger composition, and defineGluonElement() for state or lifecycle ownership.
create-gluon --ui is the maintained application-owner example for this contract. It retains the UiOwner for the application lifetime, keeps its --starter-* tokens in a separate application sheet, maps only .starter-action to the public Button override properties, and relies on Button.styles for exact usage-driven adoption. It does not add a blanket native button rule or adopt atomStyles.
Accessibility contracts
AspectRatiorenders a nativedivand adds no role.ratiomust be finite and greater than zero; invalid geometry throws before rendering. Caller div attributes, classes, and styles remain native and are merged.Avatarrequires non-emptyalt.loadedwith asrcrenders exactly one nativeimgcarrying that alt.loading,error, and every no-srccase render one named fallback image whose visible initials are hidden from the accessibility tree to avoid duplicate announcements. The caller ownssrc, status changes, fallback text, retries, and image policy;attributestarget only the loaded native image.Buttonrenders a nativetype="button", preserves disabled semantics, has a 44px minimum target, and receives a visible:focus-visibleoutline.Checkboxpreserves native Space-key, label, form, reset, required, disabled, checked, and indeterminate behavior. Indeterminate remains a DOM presentation state and does not create a third submitted form value.Iconisaria-hiddenwithout a label. With a label it exposesrole="img"and the supplied accessible name.Inputrenders a native input and supportsaria-invalid; useLabel,Field, orFormFieldto provide its accessible name.Labelis visible label text.FormFieldplaces it inside a native label; standalone callers must compose it with a native labeling relationship.Progresspreserves native determinatevalue/maxsemantics and omits thevalueattribute for indeterminate work. Give every instance an accessible name throughattributesor a native labeling relationship.Radiopreserves native same-name grouping, Arrow/Space keyboard, label, form, required, checked, and disabled behavior. Place related controls in a labeledfieldsetor compose them withChoiceGroup.Selectpreserves native option, keyboard, disabled, and required semantics; compose it withLabelor another native labeling relationship. Its public sizes aresmall,medium, andlarge, andfullWidthis opt-in.Sliderpreserves native range keyboard, pointer, form, and hydration semantics. It is single-value only; usemin,max,step,orientation,valueText, and caller-ownedonInput/onChange.readonlyis an ARIA/read-only interaction contract because native range has no readonly attribute.ScrollArearenders a named nativesectionregion, uses platform overflow, and defaults totabIndex=0; a caller-suppliedtabIndexwins.orientationselects vertical, horizontal, or two-axis overflow. The Atom does not own wheel/key handlers, scroll position, custom scrollbars, or virtualization. Reduced-motion mode forcesscroll-behavior: auto, including when an app set the public scroll-behavior property tosmooth.Separatorrenders a nativehr: horizontal instances keep the platform's implicit separator orientation, vertical instances add onlyaria-orientation="vertical", and decorative instances use presentation plusaria-hidden. It owns no labeling or layout-container behavior.StatusBadgeis a presentational span. It owns only neutral, info, success, warning, or danger tone styling; applications own domain mapping, translated status copy, and whether a surrounding surface is a live region. Its default presentation stays on one line and bounds pathological tokens with an ellipsis instead of breaking short labels mid-word. When a product genuinely needs a multiline badge, apply that behavior in the consuming surface rather than by changing the atom contract.Switchis a native checkbox withrole="switch"for binary on/off settings. Keep its caller-owned accessible label stable when the checked state changes.Textareapreserves native multiline editing, selection, resize, form, disabled, readonly, and required semantics. Associate it with visible label text and useinvalidonly with useful validation copy.ToggleButtonis a native Button with a required caller-controlled booleanpressedvalue reflected asaria-pressed. Use it for an independent pressed choice, not for an on/off setting (Switch) or an ordinary action (Button).
Slider state and normalization
value makes the Slider controlled: the caller handles native events, updates application state, and supplies the next value. A rerender always restores that normalized value. Without value, the Slider is uncontrolled; defaultValue sets its initial/reset value while native edits survive rerenders. The two props are mutually exclusive in SliderProps. Gluon forwards each native input and change once and never synthesizes an extra event.
const controlled = Slider({
min: 0,
max: 1,
step: 0.1,
value: volume,
valueText: `${Math.round(volume * 100)} percent`,
onInput: (event) => setVolume((event.currentTarget as HTMLInputElement).valueAsNumber),
attributes: { name: 'volume', 'aria-label': 'Volume' },
});
const uncontrolled = Slider({
min: 1.5,
max: 2.5,
step: 1,
defaultValue: 1.5,
attributes: { name: 'cable', 'aria-label': 'Cable length' },
});The public normalizeSliderRange() and normalizeSliderValue() helpers expose the same deterministic contract used by the Atom. Non-finite min/max become 0/100; max < min collapses to min; non-finite or non-positive step becomes 1. Values are clamped and aligned to the nearest representable decimal step rooted at min; non-finite values use the supplied finite fallback. If the finite range and step cannot produce a finite representable grid index, min is the deterministic result.
Native range inputs have no HTML readonly state. readonly therefore exposes aria-readonly="true", blocks value-changing keyboard and pointer defaults, restores the last confirmed controlled or uncontrolled value if an input or change is dispatched, and does not invoke application callbacks for rejected interactions. disabled remains the native non-focusable form exclusion state. Slider defines no transition or animation, so reduced-motion mode requires no override. The public --gluon-slider-accent property customizes the native accent while forced-colors mode retains platform rendering.
Every compatible Atom uses the named attributes extension contract. Use defineButtonPreset() for app-owned brand/danger classes and analytics/ref/data bindings while ButtonVariant and ButtonSize remain closed. Use defineIcon() plus Icon({ icon }) for app-owned SVG geometry; Icon continues to own decorative/informative ARIA semantics. defineIcon() rejects empty metadata and bodies not created by Core's svg template tag. Official .gluon-* classes are implementation details. The public Button override properties are --gluon-button-background, --gluon-button-color, and --gluon-button-border-color. Select exposes --gluon-select-background, --gluon-select-color, and --gluon-select-border-color. Textarea exposes --gluon-textarea-background, --gluon-textarea-color, --gluon-textarea-border-color, --gluon-textarea-readonly-background, and --gluon-textarea-resize. Checkbox exposes --gluon-checkbox-accent; Radio exposes --gluon-radio-accent; Slider exposes --gluon-slider-accent. Switch exposes --gluon-switch-track, --gluon-switch-on, --gluon-switch-thumb, and --gluon-switch-border-color. ToggleButton exposes --gluon-toggle-button-pressed-background, --gluon-toggle-button-pressed-color, and --gluon-toggle-button-pressed-border-color. Progress exposes --gluon-progress-track, --gluon-progress-value, --gluon-progress-track-border, --gluon-progress-width, and --gluon-progress-height. StatusBadge exposes --gluon-status-badge-background, --gluon-status-badge-color, and --gluon-status-badge-border. AspectRatio maps its validated ratio prop to --gluon-aspect-ratio; callers may deliberately override that property through attributes.style. Avatar exposes --gluon-avatar-size, --gluon-avatar-border, --gluon-avatar-radius, --gluon-avatar-background, --gluon-avatar-color, and --gluon-avatar-object-fit. ScrollArea exposes --gluon-scroll-area-max-inline-size, --gluon-scroll-area-max-block-size, and --gluon-scroll-area-scroll-behavior. Separator exposes --gluon-separator-color, --gluon-separator-length, and --gluon-separator-thickness. Shared public tokens retain their documented --gluon-* names. See the extension matrix.
Logical CSS properties support both text directions, and the maintained themes define light/dark contrast and focus tokens. atomManifest is the stable machine-readable inventory. All components appear in the compiled UI example and the browser/visual evidence named by that manifest.
Atoms contain no translated interface copy; labels and visible strings remain application inputs so localization stays with the consuming product.
GLUON GOODS is the production dogfood surface: its public Button presets cover global navigation, dialogs, product add/retry, and bag quantity/remove actions; catalog search uses Input, catalog sorting uses the native Select, and checkout delivery instructions use Textarea; checkout consent uses Checkbox; product configuration uses native Radio groups; async inventory feedback uses indeterminate Progress. The application supplies only documented public tokens/classes; completed inventory feedback uses StatusBadge with application-owned availability-to-tone mapping. The app owns the shared and exact sheets through one UiOwner lifecycle.