@jirawatpyk/aura-react
v5.52.0
Published
AURA Design System — React components (Enterprise Standard, Human Creative). Thai-first: Buddhist-era dates, Thai collation.
Maintainers
Readme
@jirawatpyk/aura-react
AURA Design System components as a real React package — ES modules, TypeScript types, server-rendering safe, Thai-first.
See every component live: Storybook
Try it without a build (CDN)
One HTML file, no install — the packages are on npmjs, so jsDelivr serves them:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jirawatpyk/aura-tokens@5/aura.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jirawatpyk/aura-react@5/dist/styles.css" />
<script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@jirawatpyk/aura-react@5/dist/aura.bundle.js"></script>
<div id="root"></div>
<script>
const h = React.createElement;
ReactDOM.createRoot(document.getElementById('root')).render(
h(
Aura.AuraProvider,
{ locale: 'th' },
h(
Aura.Stack,
{ gap: 4 },
h(Aura.Button, { icon: 'plus' }, 'New order'),
h(Aura.DatePicker, { label: 'Delivery date' }),
),
),
);
</script>Install
npm i @jirawatpyk/aura-react @jirawatpyk/aura-tokens # React 18 or 19; import from "@jirawatpyk/aura-react"
# or under the short names the docs use (imports stay `@aura/...`):
npm i @aura/react@npm:@jirawatpyk/aura-react @aura/tokens@npm:@jirawatpyk/aura-tokensReact 18.3 and 19 are both tested: the dev dependencies pin 18, and CI switches the whole workspace to 19 (node scripts/use-react.mts 19 && npm install) and runs every suite again — contrast, token lint, SSR, types, build, hydration with 0 warnings, layout before hydration, the three pilot pages and Storybook (axe + behaviour).
Public on npmjs; internal projects can use GitHub Packages instead (repository README).
// app root (once)
import '@aura/tokens/aura.css'; // tokens (light + dark)
import '@aura/react/styles.css'; // component styles
// fonts: '@jirawatpyk/aura-tokens/aura-fonts.local.css' (self-hosted, CSP font-src 'self'), next/font, or Google Fonts — see the tokens README
import { AuraProvider, AppShell, SideNav, DataTable, DatePicker } from '@aura/react';
export default function Root() {
return (
<AuraProvider locale="th">
{' '}
{/* Thai built-in labels; dates are พ.ศ. by default */}
<App />
</AuraProvider>
);
}What's inside
| Output | For |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| dist/esm/*.js | Bundlers (Vite, Next.js, webpack). One file per module with 'use client', so Next.js App Router can import it from Server Components. sideEffects is limited to CSS, so unused components are dropped. |
| dist/cjs/index.cjs | require() / Jest. |
| dist/aura.bundle.js | A classic <script> that sets window.Aura (needs window.React / window.ReactDOM). The design-system artifact uses this file. |
| dist/styles.css | Component CSS (no font import). |
| dist/index.d.ts | Types for every component and helper, generated from the TypeScript sources (one file). |
Components (65)
Actions: Button (ghost, size="sm"), IconButton, Menu, DropdownMenu (link, danger and radio items), ActionBar, Tag · Forms: TextField, PasswordField, Textarea, NumberField, Select, FilterSelect, RadioGroup, Checkbox, Switch, SegmentedControl, Combobox (one value, or multiple), FileUpload (+ formatBytes) · Dates & times: DatePicker, DateRangePicker, Calendar, TimePicker (+ useFormatDate, formatDate, parseDate, parseTime) · Feedback: Alert, FormErrorSummary, Toaster/toast() (+ .success/.error/.warning/.info/.loading), Tooltip, StatusPill, Badge, Progress, Skeleton, EmptyState · Overlays: Dialog, Drawer, Popover · Data: DataTable, Table (+ THead, TBody, TFoot, Tr, Th, Td — static tables), FilterBar, FilterDateRange, Stat, StatusTile + TileGrid, Sparkline · Navigation: Command (⌘K palette), BottomNav · Layout: AppShell, PageHeader, Container, Stack, Grid, Separator, Accordion, Pagination, Card, Tabs, Stepper, SideNav, Breadcrumb, Avatar, Surface, Icon · Theme: ColorSchemeToggle, ColorSchemeScript, useColorScheme, ThemeStyle/createTheme · Hooks: useBreakpoint, useResponsive, breakpoints, useAuraLocale, useDensity.
Router links, Swedish, motion
Next.js App Router: next/link is a function, and a Server Component (your layout) can't pass functions to a client component. Put the provider in a small 'use client' file:
// app/providers.tsx
'use client';
import Link from 'next/link';
import { AuraProvider } from '@jirawatpyk/aura-react';
export function Providers({ children, locale }: { children: React.ReactNode; locale: 'th' | 'en' | 'sv' }) {
return (
<AuraProvider locale={locale} linkComponent={Link}>
{children}
</AuraProvider>
);
{
/* th | en | sv */
}
}
// app/layout.tsx (a Server Component): <body><Providers locale="th">{children}</Providers></body>linkComponent is used by Button href, Breadcrumb, Tabs and Menu items with href, BottomNav, Pagination getHref (page numbers and, since 4.17, the previous / next arrows), Stat href, SideNav and DataTable row/pager links. Each of them also takes its own linkComponent, which wins over the provider's. Pass getHref from a client component (it's a function too). The Next.js starter does all of this, and CI clicks every AURA link in it to check none triggers a full page load. Only th shows Buddhist-era years; en and sv are Gregorian (sv weeks start Monday). Without a provider, components are English with Gregorian dates — wrap Thai apps in <AuraProvider locale="th">. For dates in your own components use useFormatDate() — it follows the provider (const fmt = useFormatDate(); fmt(iso, { format: 'long' })). Plain formatDate() has no provider to read: since 5.0 it is English and Gregorian unless you pass locale, the same function from the package root and from @jirawatpyk/aura-react/server. Values are always Gregorian ISO dates.
Compact density
<AuraProvider density="compact">…</AuraProvider> {/* whole app or one section */}
<DataTable density="compact" … /> {/* just one table */}Compact makes fields and buttons 36px (from 44px), table rows 40px (from 48px) and tightens button padding. Dialogs, drawers, popovers and the ⌘K palette opened inside the provider follow it. On touch screens (pointer: coarse) compact keeps 44px targets and 48px rows. Read it with useDensity(). Without CSS-in-JS you can set data-density="compact" on any element — the CSS variables --aura-input-height, --aura-button-height, --aura-table-row-height and --aura-button-padding-x do the rest; virtual DataTables read the row height from CSS.
Motion: skeleton pulses and indeterminate bars honour prefers-reduced-motion: reduce — skeletons stop (a static bar), spinners and progress bars slow to a third.
Collapsible sidebar
const [collapsed, setCollapsed] = useState(initialFromCookie); // keep the choice across reloads
<AppShell nav={<SideNav collapsible collapsed={collapsed} onCollapsedChange={setCollapsed} items={items} />}>
…
</AppShell>;collapsed turns SideNav into a 64px icon-only rail (--aura-sidenav-rail-width). Labels become tooltips and stay the accessible names; counts and badges show as a dot; items without an icon show their first letter; clicking a group widens the rail and opens it. collapsible adds the collapse / expand button at the bottom. Uncontrolled: defaultCollapsed. Pass a short header while collapsed (a logo mark instead of the name). In AppShell's phone drawer the nav is always full width with no toggle.
Toasts, forms, filters, rich text, ⌘K
const id = toast.loading('Saving'); // spinner, stays
toast.success('Saved', { id }); // same id: replaced in place, never stacked
toast.error('Could not save'); // danger = role="alert"; the rest role="status"
<FormErrorSummary errors={formState.errors} focusKey={formState.submitCount} onSelect={setFocus} />
<PasswordField label="Password" {...register('password')} />
<FilterBar search={q} onSearchChange={setQ} filters={chips} onClearAll={clear} resultCount={total}>…</FilterBar>
<div className="aura-prose" dangerouslySetInnerHTML={{ __html: sanitised }} /> {/* p, h1–h4, lists, blockquote, hr, a, strong, em, u */}
<Command open={open} onOpenChange={setOpen} items={commands} /> {/* ⌘K / Ctrl+K toggles it */}Server search in the palette (4.17): control the query and hand over what the server found. The active item is kept by id while results change, and loading announces "Searching…" and then the count.
<Command
open={open}
onOpenChange={setOpen}
query={q}
onQueryChange={setQ} // fetch results for q (debounced)
items={results}
filter={false}
loading={isFetching}
empty={
<Button size="sm" icon="plus">
Create member
</Button>
}
/>DataTable with sort and page in the URL (4.17): onStateChange={({ sort, page }) => router.push(…)} reports a sort click once, as { sort, page: 1 }, instead of onSortChange and then onPageChange(1). That's one history entry and one server render.
Phone bars, totals, link tabs, time zones (4.19)
<AppShell nav={<SideNav … />} bottomNav={<BottomNav value={tab} items={[{ id: 'home', label: 'Home', icon: 'house', href: '/' }, …]} />}>
<form>
…fields…
<ActionBar status={dirty ? 'Unsaved changes' : 'Total 107,000.00 THB · due Oct 22, 2026'}> {/* last child of the form */}
<Button>Save</Button>
</ActionBar>
</form>
</AppShell>
<ActionBar selected={ids.length} onClearSelection={clear}><Button size="sm">Send reminder</Button></ActionBar>
<DataTable … footer={{ id: 'Total', vat: '9,779.00', total: '149,479.00' }} stickyFooter height={480} />
<Tabs label="Renewals" value="review" tabs={[{ id: 'pipeline', label: 'Pipeline', href: '/renewals' }, …]} />
<DropdownMenu items={[{ label: 'Open', href: '/m/1' }, { label: 'Grid', type: 'radio', group: 'View', checked }, { label: 'Void', tone: 'danger' }]} … />
<DatePicker label="Payment date" max="today" /> {/* with <AuraProvider timeZone="Asia/Bangkok"> */}- ActionBar is
position: sticky, so it stays in the flow and never covers the last field; it sticks while its parent (the form or card) is on screen.viewport(default) floats above the home indicator and above a BottomNav;containersits flush at the bottom of a card. The status line is a live region. Withselected, it reads "N selected", adds Clear, and hides at 0 (focus goes back to where it came from). - BottomNav: up to five icon + label tabs, 44px+ targets at 320px,
aria-current="page", counts and dots. Hidden fromlgup in CSS (hideFrom), with a spacer in the flow, so the server's HTML is right and nothing shifts on hydration. For the notch and home indicator addviewport-fit=coverto your viewport meta. - DataTable
footer: a totals row with the body's widths and alignment (a Totals card when stacked);stickyFooterkeeps it in view in aheighttable. Truncated cells show their full text on hover and keyboard focus. - Tabs with
hrefon every item render anavof links (no panels), the current onearia-current="page". - Menu items:
href(throughlinkComponent),tone: 'danger', andtype: 'radio'withgroup/checked(menuitemradio in a labelled group). - Time zone:
timeZoneon AuraProvider, DatePicker, DateRangePicker and Calendar decides "today" (the marker,min/max="today", the first month shown); or passtodayas an ISO date.todayIn('Asia/Bangkok')is exported from the root and/server. - Toasts queue past three instead of dropping: six in a row all show, in order, three at a time.
5.52 — Chamber-OS addendum 65 (#164)
Table loading(withloadingRows, default 5): the head as given, then placeholder rows in its columns — and belowstackBelow, the same cards (title and action slots kept). Each column follows its head cell:skeleton={{ lines: 1 | 2, kind: 'text' | 'pill' | 'action', touch }}(two lines for a value over its caption, a status pill, or a block as tall as a small Button — 44px on a phone card withtouch), its width fromskeletonWidth, else the cell'swidthorstyle.width, else an even share of what is left. The THead must be a direct child (call your head as a function, not your own component) — a dev notice says so — and its first row must be the column row (with grouped headers, columns come from that first row). A head cell whose content isn't plain text (an icon, a sort button) needslabelfor the cards' field labels. TBody and TFoot aren't rendered while loading, so a totals row appears with the data.TableSkeleton— the same thing for Server Components (a route'sloading.tsx), from the root and/server:columns({ label, width, align, numeric, card, skeleton }),rows, and Table'sdensity,stackBelow,stackStyle,bordered,bleed,bleedEnd,rowHeight,align. Build both from one column list and they can't drift.- Both are
aria-hiddenandinert, with nothing focusable: announce the load in the page's own status line. A loading table has no content to size from, so its columns are fixed to those widths; give the loaded table the same shares (adding up to 100%) and every column's left edge matches.
<Table stackBelow="sm" loading={isPending} loadingRows={8}>
{head()} {/* <THead><Tr><Th style={{ width: '26%' }} card="title" skeleton={{ lines: 2 }}>Member</Th>… */}
<TBody>…</TBody>
</Table>
// app/members/loading.tsx (a Server Component)
import { TableSkeleton } from '@jirawatpyk/aura-react/server';
export default () => <TableSkeleton stackBelow="sm" rows={8} columns={MEMBER_COLUMNS} />;5.51 — Chamber-OS addenda 58–64 (#157–163)
- DataTable column
sortDirection(#157):'ascending' | 'descending'— for a list the page orders (from the URL, a line under the table), putsaria-sorton that column's header with no sort button, icon or click, in the server's HTML too. Give it to one column (two warn in development); move it when the order changes. Once the table has its own sort (a sortable header was used, orsortis set) that wins andsortDirectionis ignored, so one header at most claims the order. - DataTable
selectAllRef(#157): a ref to the select-all checkbox, e.g. to return focus there after a bulk bar's Clear. - The row minimum counts each flexible column's
minWidth(#159), the value its cells already used, instead of 160px for every one — eight columns with minimums of 80–132px now fit a 959px box without a sideways scroll. Columns withoutminWidthstill count 160. StatusPill wrap(#158): in a box narrower than the words they wrap inside the pill (the icon beside the first line, the pill's own line height, Thai's too), and its corners areaura-radius-lg(16px) instead of full (addendum 61): on one line that still gives fully round ends, so it looks exactly as before; wrapped it is a rounded rectangle rather than a tall lozenge instead of running out of it — for a narrow DataTable column or a stacked card. In a wide box, or withoutwrap, a pill is one line as before.- Toggle Tags among FilterBar's controls show their whole label (#160): from 640px a selectable Tag inside FilterBar isn't cut at 24ch, only when wider than the bar, and in the Filters sheet it never is; below 640px in the bar itself, and for a Tag outside FilterBar, the cap and its tip stay. Applied-filter chips were already uncapped (5.28).
- The wheel over a DataTable scrolls the page again (#163):
.aura-table__scrollcontains only the sideways axis (overscroll-behavior-x: contain), so a vertical wheel over a grid that scrolls sideways scrolls the page, a sideways swipe stays in the grid (no back navigation), and a fixed-heightgrid scrolls inside first, then the page at its end. Remove anyoverscroll-behavior-yoverride you added for it. - DataTable column
grow(#162): a flexible column's share of the spare width (its cells' flex-grow; default 1), e.g.{ key: 'actions', minWidth: 128, grow: 3 }puts Approve and Reject side by side once the table is wide. At the row minimum nothing changes, the row minimum ignores it, and fixed-widthcolumns and stacked cards ignore it.grow: 0keeps a column at its minimum; leave at least one flexible column above 0 or the rows won't fill a wide table. - DataTable
keyboard="tab"(#161): every checkbox, sort and column button, row link and row action is an ordinary Tab stop in reading order (Shift+Tab walks back), each with its focus ring, in the grid and in stacked cards — for lists whose rows carry actions people expect to Tab to. The cells stop being Tab stops; the arrows still move between them once focus is in one (click a cell, or Escape from a control), and the grid's row and column semantics stay. Without it ('grid', the default) the table is one Tab stop as before. Keyboard resizing and reordering of columns (Alt/Ctrl+Shift with the arrows on a header cell) need focus on that header cell, so they stay with the grid keyboard. - 5.50 follow-up: a disabled box whose control has
aria-invalid="true"keeps its disabled edge inside an enabled customFieldtoo.
<DataTable columns={[{ key: 'timeInStage', label: 'Time in stage', minWidth: 132, sortDirection: 'descending' }, …]} selectAllRef={selectAll} … />
<StatusPill tone="progress" wrap>Awaiting marketing review</StatusPill>5.50 — Chamber-OS addendum 57 (#156)
invalidon TextField and Textarea (and PasswordField, which is a TextField;Fieldfor a custom control): marks the value invalid without AURA's message — the danger edge (--aura-border-danger, light and dark, also on hover and focus) andaria-invalid="true"on the control, no error line, andaria-describedbyas it would be without it (the addon, the hint, then your own ids). For a page that explains the problem itself — a live contrast readout, a reason line a disabled Save points at. Pointaria-describedbyat that explanation, or a screen reader hears "invalid" with no reason.errorstill wins: its message replaces the hint and its id the hint's. A disabled field keeps its disabled edge.- Your own
aria-invalid="true"is enough too: a field whose input or textarea carries it gets the same edge, so a custom control insideField(withinvalidon the Field for the class) or a form library that sets it needs nothing else.
<TextField label="Primary colour" hint="A hex value." value={colour} onChange={…} invalid={!meetsAa} aria-describedby="contrast-readout" />5.49 — Chamber-OS addendum 56 (#155)
- The short-screen threshold follows the browser's text size: the height at which bottom bars stop pinning (5.48, #154) is
30rem, not480px. In a media queryremis the browser's default font size (Chrome, Edge, Firefox; not iOS Dynamic Type or Safari's text size), not the page'shtmlfont size: 480px at the default 16px (nothing changes), 720px at 24px (an iPhone SE counts as short, an 844px phone keeps the bar), 960px at 32px (an 844px phone counts as short — it holds what a 422px screen holds at 16px). There theactionsBarBelowactions stay in the header, an ActionBar sits at the end of its container and AppShell's top bar scrolls away, as at 400% zoom. Changing the text size on an open page re-checks it (the bar-height tokens follow). The width breakpoints stay in px.
5.48 — Chamber-OS addendum 55 (#153–154)
- The
actionsBarBelowbar is now ActionBar's edge strip (#153): the full width of the screen, on its bottom edge (or on a BottomNav's top edge), a top border only — no radius, no shadow — with ActionBar's inner padding (8px block, 16px start, 12px end) and the home-indicator inset inside it, as the save and bulk bars of an app built onActionBar position="container"look. The page's end padding andscroll-padding-bottomfollow the new height. The layout inside is unchanged. - Bottom bars on a short screen (#154): on a screen 30rem tall or less — 480px at the browser's default 16px text, and since 5.49 (#155) it follows the default font size set in Chrome, Edge or Firefox (960px at 32px, 720px at 24px), so large-text users get the same room (iOS Dynamic Type and Safari's text-size controls don't change it); 400% zoom on a desktop (320×256 CSS px), a phone held sideways —
actionsBarBelowno longer fixes the actions to the bottom: they stay in the header's own row (laid out byactionsFullWidthBelowif set), with no status line, page-end space or scroll padding; and anActionBar(eitherposition) stops sticking and sits at the end of its container, in the flow. AppShell's top bar scrolls away with the page there too, so a field reached backwards (Shift+Tab) isn't left under it, and--aura-shell-bar-heightis 0 meanwhile, so nothing sticky (a DataTablestickyHeader) leaves a gap for it. A focused control is never under a bar there (WCAG 2.4.11, 1.4.10); taller than 30rem everything is as before. The width breakpoints stay in px. An ActionBar withhidesBottomNavstill hides the BottomNav on a short screen while it sits at the end of its container. Wherever a BottomNav shows (any height), the page'sscroll-padding-bottomnow reserves its height, so a field tabbed into view stops above it. actionsBarLook="floating"keeps 5.45's card (inset 16px, lifted 12px, bordered all round, rounded, with the overlay shadow) — the look ofActionBar position="viewport". Only the bar's look changed; nothing else aboutactionsBarBelow.
5.47 — Chamber-OS addendum 54 (#152)
- PageHeader
size="compact": 26px below 640px and 32px from 640px, on a 1.2 line — the staff and list boards' title, so a company name such as "Siam Nordic Trading Co., Ltd." takes two lines on a 390px phone instead of three. The default stays 28px / 36px (1.15), so DxT Monitor and other apps don't change; passsize="compact"(Chamber-OS: in its header wrapper) where the boards draw 26 / 32. herosteps to 36px from 1024px (30px below), on a 1.2 line, ashero-lgdoes — the portal Main and Edit boards.hero-lgis unchanged (26px, then 36px).--aura-page-header-hero-space: the room above aheroorhero-lgtitle, default 16px, on top of the page's own padding (which belongs to the app). For the boards' 24px on a phone and 40px from 1024px inside AppShell's 24px / 32px content padding, set it to0below 1024px and8pxfrom 1024px — or whatever your page padding needs.- Under
lang="th"titles keep their 1.4 line at every size. Checked at 390 and 1280px in English, Thai and Swedish and at a 32px default font size: nothing scrolls sideways.
5.46 — Chamber-OS addendum 53 (#151)
What Chamber-OS's own page header did that PageHeader didn't, so it can be rebuilt on PageHeader with its props kept:
subtitle: a sentence under the title at body size (14px/1.7, the secondary colour) — "Members, plans and renewals for SweCham" — beforemeta, in adiv(a loading skeleton's blocks are valid inside).titleAddon: a chip or badge on the title's line (a role, an invoice's status, "Archived"). Title and addon share a wrapping row, 12px apart (8px between lines); on a narrow screen the addon wraps under the title. It sits beside the heading, not in it, so the heading's name stays the title. WheretitleHideBelowhides the title, the addon goes with it.- A chip eyebrow:
eyebrowthat is not text (a StatusPill, a member number, badges) renders as a wrapping row in adiv, 8px apart, 4px above the title as before. Text stays the monop. size:default(28px, 36px from 640px),hero(30px on a 36px line) andhero-lg(26px, 36px from 1024px), the hero sizes with 16px more room above. Per page,--aura-page-header-title-sizeand--aura-page-header-title-line-heightoverride any of them (staff pages at 26/32: set both). Underlang="th"the title keeps its 1.4 line, whatever the size or variable. The hero's 16px adds to the padding around it (AppShell's content, a Card), by design.focusTitleOnMount: the heading takes focus when the header mounts (tabIndex={-1},preventScroll, no visible ring) — a page reached by a redirect, so a screen reader starts at its heading (WCAG 2.4.3). The main entry's PageHeader does it once, when it mounts; from/serverthe heading is focusable but not focused. Use it on pages reached by client-side navigation or a redirect, not on every page: on a first load it moves focus past a skip link, and a header that stays mounted while its title changes doesn't refocus — key it by the page (key={pathname}) if it must.actionsAlign:end(default) lines the actions up with the bottom of the text, as before;startwith its top — the title, when there is no breadcrumb or eyebrow.- Confirmed, no change: the text column's
flex: 1 1 320pxkeeps a long title at least 320px wide; wide actions drop under it (checked at 700px). On a phoneactionsFullWidthBelow="sm"shares the row between actions. - Checked at 390 and 1280px in English, Thai and Swedish, and at a 32px default font size: nothing scrolls sideways.
5.45 — Chamber-OS addendum 52 (#150)
- PageHeader
actionsBarBelow(#150):'sm' | 'md'— below 640px or 768px the header'sactionsbecome a bar fixed to the bottom of the screen (from 5.48 ActionBar's edge strip,actionsBarLook="floating"for this card; above a BottomNav, clear of the home indicator), andactionsStatusis its status line ("Total 10,700.00 THB incl. VAT · due 22 Oct 2026", a polite live region shown only in the bar).<PageHeader title="Invoice SC-2026-900001" actionsFullWidthBelow="sm" actionsBarBelow="sm" actionsStatus="Total 10,700.00 THB incl. VAT · due 22 Oct 2026" actions={<> <Button variant="secondary" touchHeight>Issue credit note…</Button> <Button variant="secondary" touchHeight>Issue refund…</Button> <DropdownMenu trigger={<IconButton icon="ellipsis" label="More actions" variant="outline" touchHeight />} items={…} /> </>} />- Rendered once: it is the same element moved by CSS, not a copy or a portal — a dialog trigger, its state and its test id exist once at every width, and in the tab order the actions stay after the title.
- The page scrolls clear of it: AppShell's content (or the body outside AppShell) gains the bar's height at its end, and
scroll-padding-bottomkeeps a focused field above it. The main entry's PageHeader measures the bar (--aura-page-actions-heighton<html>); the/serverone renders the same markup and the CSS assumes 7rem until a client PageHeader takes over. - Layout: the status line takes the first row. With an IconButton among the actions they share the next row as #149 — text buttons wrap their labels inside, the icon keeps 44px at the end, vertically centred. With only icon buttons (a read-only role's ⋯) the status and the icons share one row (62px against 93px stacked). Text actions alone wrap as ActionBar's do, the main one last.
- From the breakpoint up nothing changes. One per page: the bar is fixed, so two would overlap (a development notice says so). Not beside an ActionBar either: use one bottom bar. With no
actions(a role that may do nothing) there is no bar and no space reserved. - The bar is
position: fixed, so no ancestor of the header may have atransform,filter,backdrop-filter,will-change: transform, paint/layout containment orcontent-visibility: auto(some page-transition and frosted-glass wrappers add one); it would be placed against that ancestor, off screen. The main entry's PageHeader warns in development. - Reading and Tab order stay the header's: the actions come after the title and before the page content, as they do from the breakpoint up. Below
lgAppShell's sidebar is a drawer, so the bar spans the screen with 16px margins.
- The ⋯ is centred (addendum 52): beside two-line buttons the IconButton in
actionsFullWidthBelow(#149) now sits at the row's vertical centre, not its top.
5.44 — Chamber-OS addendum 51 (#149)
- PageHeader
actionsFullWidthBelowwith an icon button keeps one row (#149): when the actions include an IconButton (a ⋯ DropdownMenu trigger, bare or wrapped), below the breakpoint they are one bar. The row no longer wraps; the text buttons share what the icon leaves and wrap their labels inside the button (two or three lines, centred,overflow-wrap: anywherewith hyphenation, never an ellipsis), growing taller rather than wider, at least 44px withtouchHeight; a pill that grows past one line gets 22px corners instead of becoming an oval. Hyphens appear only where the browser has a dictionary for the page'slang(Safari, Android); elsewhere a long word breaks where it must, so keep phone labels short. The icon keeps its size at the end of the row. Measured on an invoice page at 393 and 320px in English, Thai and Swedish: nothing clipped, no sideways scroll. Actions without an icon button wrap onto rows as in 5.37 (DxT Monitor's two-plus-one); a header that mixes text and icon buttons (e.g. Mute, Edit, ⋯, Open site) is now one row at 320px where it was two. Needs:has(); older browsers keep the 5.37 layout. - IconButton
variant="outline": a secondary Button's 1px border, so a lone ⋯ reads as a button beside outlined actions. Defaultghost(no border).
5.43 — Chamber-OS addendum 50 (#148)
- FilterBar
collapseBelowwith no search (#148): below the breakpoint the Filters button fills the row it would otherwise sit at the end of, its label centred (an activity timeline with filters and no search). With a search it stays beside it, as in 5.42. From the breakpoint up nothing changes. Remove a stand-in such as[&_.aura-filterbar__more]:flex-1.
5.42 — Chamber-OS addendum 49 (#146–147)
Fields no longer widen a phone at a large default font (#147, a 5.40 regression): an
<input>'s 20-character size grew with the browser's font size, so at a 32px default FilterBar's search measured ~450px and a 390px page scrolled sideways; so did a TextField, Combobox, DatePicker or NumberField in a grid cell or flex row withoutmin-width: 0. The text box now never asks for more than the room it gets (.aura-inputhasmin-width: 0and its control a percentage width, so its smallest size is zero) while its natural width is unchanged. At the 16px default a page lays out as in 5.41 unless a field was already wider than its room: we compared every Storybook story at 390 and 1280px pixel for pixel, and the only differences are fields that used to stick out of their grid cell or row, which now fit (a touch-height Combobox in a 390px FilterBar, a time-zone field in a two-column grid). A row of nowrap Buttons or a SegmentedControl can still be wider than a phone at 32px; give such rowswrap(Stack) orfullWidth.FilterBar
collapseBelow(#146):'sm' | 'md'— below 640px or 768px the filters passed aschildrenfold into one Filters button at the end of the search's row ("Filters (2)", read "Filters, 2 applied";aria-haspopup="dialog",aria-expanded,aria-controls), as tall as the search, so it followstouchHeight. The search, the applied chips, Clear all,resultCountandactionsstay in the bar; the count and actions wrap under the search row. The number is the count offilters(chips);filterCountoverrides it.<FilterBar collapseBelow="sm" touchHeight search={q} onSearchChange={setQ} filters={chips} onClearAll={clear} resultCount={total}> <FilterSelect label="Status" … /> <FilterDateRange label="Submitted" … /> </FilterBar>The button opens a Filters sheet (AURA's Dialog, a bottom sheet on phones) with the same children one per row at full width, 44px, labels showing: a FilterSelect is a labelled Select there (its "all" choice in full words) and a FilterDateRange shows its name above a full-width face. A toggle Tag, a Button or a SegmentedControl keeps its own width. Every change applies at once, as in the bar — there is no Apply step. The footer has Clear all (while chips exist) and Done; Done, Escape and the scrim close it and focus returns to the button (in Safari too). Widening past the breakpoint with the sheet open closes it. The children render in one place at a time — the bar (hidden below the breakpoint) or, while it is open, the sheet — so a controlled filter is never doubled; they mount again when they move, so keep their state with the page (
value+onChange, the URL), not indefaultValue. A bar with no children (search only) shows no button; without the prop nothing changes. Server HTML is the bar with the button closed. New stringsfiltersApplied(n)anddone(th: ตัวกรอง, ใช้อยู่ 2 รายการ · เสร็จสิ้น; sv: Filter, 2 aktiva · Klar), and asliders-horizontalicon (71 in all).
5.41 — DxT Monitor requests 19–22
- No menu button on a page with a back arrow (request 19): put
data-aura-hide-menubuttonon any element of the page (as withdata-aura-hide-bottomnav) and AppShell's phone bar drops its menu button, giving the title its room (a shell with noheaderthen has no bar on phones). The drawer stays reachable from pages without the mark. A browser without:has()keeps the button. - PageHeader
titleHideBelow(request 20):'sm' | 'md' | 'lg'— below that width the title is visually hidden but stays the page's heading for screen readers, and the breadcrumb and eyebrow are hidden; the meta line then starts the header, or the actions when there is no meta (with neither, the header takes no room). For phone layouts that show the title in the top bar. Stateless,/servertoo. - Menu links that open a new tab say so (request 21): a Menu or DropdownMenu item with
hrefandtarget="_blank"is named "Visit site (opens in a new tab)" — the words are visually hidden, after the label, in the locale's language (opensInNewTabin the strings: เปิดในแท็บใหม่, öppnas i en ny flik). A customstringspack without the key falls back to the built-in words. Other links you give atarget(Breadcrumb, Tabs, DataTable rows) don't add it yet; give their label the words yourself. - Menu triggers look open while their menu shows (request 22): an IconButton, secondary or ghost Button with
aria-haspopupandaria-expanded="true"(DropdownMenu, Popover and Dialog triggers) keeps its hover fill. A disclosure button of your own (aria-expandedwithoutaria-haspopup) keeps its plain look.
5.40 — Chamber-OS addendum 48
- Type follows the browser's default font size (#145, WCAG 1.4.4). Every font size and px line height in
styles.css, the.aura-text-*classes and--aura-input-font-sizeis nowmax(<px>, <rem>)— for examplemax(13px, .8125rem). At the default 16px root nothing changes. When someone raises the browser's default font size (Settings → Font size, not page zoom), AURA's text grows with the app's own: at 32px a card's description, a Switch's description and a table cell are twice the size. Spacing, borders and control heights stay in px; boxes that hold text grow where they already could (fields, cards, rows withrowHeight="auto"), and one-line controls keep their height with larger text inside. - An app that sets a smaller root (
html { font-size: 62.5% }) keeps AURA's px sizes as a floor, so nothing gets smaller than before. An app with a fixed larger root (html { font-size: 18px }) will see AURA's text grow with it. - Boxes with text that had a fixed height now have a minimum instead, so they grow with the text: Badge, the small SegmentedControl option, the calendar's month title (it wraps under the arrows rather than overlapping the weekdays), the DataTable footer (which also wraps the count and the pager) and BottomNav's count. Avatar initials stay in px, sized to their circle; the name is the accessible name.
- A smaller browser default never makes AURA's text smaller than its px size.
- A row
Stackwithoutwrapholding buttons or tags can run past a phone's edge at a large default size; give itwrap.
5.39 — Chamber-OS addendum 46
- Tabs
touchHeight(#143): 44px tabs — buttons or links, underline or segmented — withtruebelow 640px or on a coarse pointer (Button's rule), or'always'at every width. The count badge and icon stay centred on the label (above the 2px underline). The height is set on the tabs themselves, so fields in the panels keep their own size. Without it nothing changes. - Link tabs are as tall as button tabs. A link tab (
href) was 2px taller than a button tab — its 2px underline sat outside the height (38px instead of 36px in compact density, 46px instead of 44px on touch screens). Tests that measure link tabs will see 2px less. - FilterDateRange
touchHeight: a 44px face on its own, as FilterSelect's. A FilterBar withtouchHeightalready reaches it (checked for Chamber-OS's audit log filter row).
5.38 — Chamber-OS addenda 43–45
- FilterBar and FilterSelect
touchHeight(#140):truemakes the search (and its clear button) and every field-height control in the bar (FilterSelect, Select, Combobox, DatePicker and TimePicker with their buttons, SegmentedControl's options) 44px below 640px or on a coarse pointer — the same rule as Button's and TextField'stouchHeight— and'always'at every width;touchHeighton one FilterSelect does the same for it alone. Without it nothing changes: the density's input height with a mouse, 44px on a coarse pointer. Buttons inactionstake their owntouchHeight. A SegmentedControl's track is 50px there (44px options plus its edge and padding), as with its owntouchHeight. Opt-in rather than a new default, like every othertouchHeight, so a narrow desktop window doesn't change size unasked. touchHeighton Combobox, DatePicker, DateRangePicker, TimePicker and NumberField (#141): the same values and rule as TextField's. The box is 44px and the buttons inside it (clear, chevron, calendar, steppers) take their touch-screen size — 44px wide, side by side, 40px tall — so none is a 32px target in a 44px box. A multiple Combobox now reserves room for both 44px buttons on touch screens too (chips used to run up to 24px under the clear button).- DataTable
rowProps(#142):rowProps={(row) => ({ 'data-user-id': row.id, id: 'user-' + row.id })}setsdata-*attributes and anidon each row's element, in the grid and as a stacked card — for end-to-end hooks. Anything else it returns is ignored (a development warning names it) —aria-*,role,className, handlers, anddata-rc(the table's own cell address) — so a row keeps its role, name, classes, selection and keyboard behaviour.
5.37 — DxT Monitor requests 17–18
- PageHeader
actionsFullWidthBelow(request 17):'sm','md'or'lg'— below 640px, 768px or 1024px the actions take the whole row under the text, 12px apart, and the buttons share each line in equal widths (unless a label needs more), so they reach the page's edge: three buttons on a phone fill two rows (two halves, then one across) when their labels don't fit on one, and a single button spans the row. A button inside a DropdownMenu, Tooltip or Popover fills its share too; an IconButton (bare, or as a menu's trigger) keeps its square size — so a line with only IconButtons on it doesn't reach the edge (one that wraps alone sits at the start); put them on the first line. Labels don't wrap: a label wider than the row overflows, as before — keep phone labels short, or give that one ButtonfullWidth. For a fixed arrangement, pass your own grid as the one child — it spans the row too. Wider than the breakpoint, the actions hug their content at the end, 8px apart, as before. A class on the root, so it works from/servertoo. Apps that stretched the actions with their own selector (e.g.max-md:[&>:last-child]:w-full) can drop it. - SegmentedControl
touchHeight(request 18): 44px options —size="sm"too, icon-only options 44px square — withtruebelow 640px or on a coarse pointer (the same rule as Button's and TextField'stouchHeight), or'always'at every width. A phone-sized viewport with a mouse (or an emulated phone in end-to-end tests) now gets the touch size too. Without it nothing changes: options are as tall as a field beside them with a mouse, and 44px on a coarse pointer. DxT asked for "below md"; it follows AURA's othertouchHeightprops (below 640px) so a form's fields, buttons and choices change size together — at 390px it is the same.
5.36 — DxT Monitor requests 15–16
- Focus rings in table cells (request 15): a DataTable cell clips its text (ellipsis) but no longer the focus ring of a link or button in it. The clip has a 4px margin (
overflow: clip+overflow-clip-margin), so content wider than its cell (a pill, a button) now shows up to 4px past its edge. In the first data column a focused link or button sits just above the row's sticky start gutter or pinned checkbox column (which painted over the ring's start side) — not while the table is scrolled sideways, and never above the sticky header, totals or pinned columns — and a plain<a>or<button>of yours in a cell gets AURA's ring instead of the browser's (zero specificity, so your own focus style wins). Safari has nooverflow-clip-margin: there rings in cells are drawn just inside the control, whole. Apps that setoverflow: clipon.aura-table__tdthemselves can drop it. - Right-aligned DataTable columns (request 16): already there —
align: 'end'on a column right-aligns its header (label and sort arrow) and every cell, with tabular figures, in default androwHeight="auto"rows, totals and skeleton rows. Stacked cards keep each value under its label, start-aligned, by design. Now covered by tests and documented here.
5.35 — DxT Monitor requests 11–14
- Breadcrumb touch targets (request 11): on a touch screen (
pointer: coarse) every crumb link or button (and "…") gets a hit area 44px tall and as wide as its text plus 24px; the text doesn't move. It reaches 12px into the gutter on each side, so short neighbours never share a spot, and only 4px below the text, so it stops short of a PageHeader title right underneath — the rest of the height is above, so keep about 20px clear above a breadcrumb (AppShell's content padding already is). When the crumbs wrap, the rows sit 24px apart on touch screens so one row never covers the next. The current page (not a link) gets none; a link component must render its<a>directly in the crumb'sli. Apps that added their own.aura-crumbs a::afterrule should drop it. - StatusTile
status="unknown"(request 12): for a thing with no data yet (a site before its first check) — a dashed circle (circle-dot-dashed) inaura-fg-tertiaryon the plain surface and border, status word "No data" (th: ยังไม่มีข้อมูล, sv: Ingen data), value in the text colour. An unrecognisedstatusnow shows as unknown rather than OK, with a dev warning. - AppShell
headerHideOnScroll(request 13): below 1024px the top bar slides up out of view while the page scrolls down (after it has moved past the bar's height and kept going 8px) and comes back after 8px up, near the top, and whenever focus enters it.--aura-shell-bar-heightis 0 while it is away, so a sticky table header pinned under the bar moves up with it. No slide with reduced motion. The page must scroll the window (AppShell's does). - BottomNav
hideOnScroll(request 14): the same for the tab bar, sliding down. Its spacer stays (nothing shifts) and--aura-bottomnav-offsetdrops while it is away, so viewport ActionBars and toasts follow it down. ActionBarhidesBottomNav: while that bar is on the page the BottomNav and its spacer are gone and the bar sits on the bottom edge — for a page whose own actions (Acknowledge, Visit site) replace the tabs. Any element can do the same withdata-aura-hide-bottomnav.
5.34 — StatusTile, TileGrid, Sparkline (DxT Monitor Overview S06)
- StatusTile: one monitored thing.
status(down/problem/maintenance/ok) sets an icon and a tint and border — red, amber, violet or the plain surface — so colour is never the only sign (circle-alert, triangle-alert, wrench, circle-check).titlewraps to two lines at most, then ends with "…" (never one cut line, so similar names stay apart); a cut title shows its full text in AURA's tip on hover.valuesits top right in tabular figures, 600, in its own tone (valueTone; default the status's: a Down duration is red).metais a short second line;markers([{ icon, label, tone? }]) are small icons at its end, each named and showing its label as a tip on hover. A tip needs a mouse, so on a phone a marker is a hint only: say what it means on the detail page (or inmeta) too.hrefmakes the whole tile one link with a focus ring; its accessible name is everything the tile says, in reading order — "api.example.co.th, Down, 12m, since 09:41, SSL expires in 5 days" — taken from the content (screen-reader-only status word and commas) rather than anaria-label, so it always matches what is shown (WCAG 2.5.3).statusLabeloverrides the status word (th: ล่ม / มีปัญหา / ปิดปรับปรุง / ปกติ). In a TileGrid cell narrower than 226px (two columns on a phone) the value moves under the title in every tile alike; a tile elsewhere (a flex row, a card) keeps the value top right. The hidden status word and commas are text, soinnerTextand a copy include them; a labelled Sparkline inmetajoins the link's name. Light and dark use the Alert tokens. - TileGrid: a
ulof equal columns 8px apart, each child (and each child of a Fragment) oneli; tiles in a row are as tall as the tallest.columnsis responsive like Grid; default{ base: 2, md: 3 }. Passaria-labelto name the list. - Sparkline:
dataoldest first;null(or anything not a finite number) is no point and breaks the line, and a point with no neighbour is a dot.failed(booleans by index) withfailures="band"(a light red band behind the point) or"ticks"(a red tick at the foot); defaultnone.tone(neutral, accent, danger, warning, positive),sizesm80×24 (default) ormd120×32,min/maxto put several on one scale (values beyond them are drawn at the edge).labelmakes itrole="img"with that name — summarise what the line shows, e.g. "Response time, last 24 h: 180–420 ms, 2 failed checks"; without one it is decoration, hidden from screen readers. - All three are stateless and also exported from
/server(the tile's status word is English there unlessstatusLabel; marker tips need the client entry on the page).
5.33 — DxT Monitor requests 5–8
- Hover shows on the canvas (request 5): in light,
aura-bg-surface-hoveris zinc-50 — the canvas itself — so SideNav's rows, an IconButton or ghost Button in its header or footer (Sign out), and the same on the page's canvas (PageHeader actions, Pagination, a selectable Tag) showed no hover. A new token,aura-bg-canvas-hover(zinc-100 light, zinc-800 dark), is the hover inside AppShell and SideNav; cards, tables, inputs, the top bar, dialogs, drawers and popovers inside them keepaura-bg-surface-hover. Hover fills now read--aura-hover, falling back to the surface hover: outside AppShell, set--aura-hover: var(--aura-bg-canvas-hover)on your own canvas-coloured area. - AppShell
headerHideFrom="lg"(request 6): the top bar,headerand all, shows only below 1024px — where the menu button is — as a header-less bar does: from lg up it is hidden and--aura-shell-bar-heightis 0. For apps whose phone pages draw a top bar (brand, avatar) and whose desktop pages don't. Default: the bar shows at every width. - PageHeader
breadcrumb(request 7): a<Breadcrumb>above the title, 4px from it like the eyebrow, in its owndiv— anavcan't sit in the eyebrow'sp(invalid HTML, a hydration error) — with Breadcrumb's own 13px sans. With an eyebrow too, the breadcrumb comes first. - Phone drawer header = bar height (request 8): in AppShell's phone drawer, a SideNav with
headerDividertakes the top bar's height (56px, or more when the bar's header wraps), content centred, so its line meets the bar's line beside it; the close button is centred on it. The fixed desktop sidebar keeps 72px. - Also: the nav drawer's close button is a 44px target on phones and touch screens (the header leaves it 60px); the divided header's layout no longer needs
:has()(older Safari and Firefox centre the close button too); a DatePicker's or read-only Select's toggle shows its hover on the grey read-only ground.
5.32 — Icon tips, PageHeader, SideNav header divider, pill skeletons (DxT Monitor requests 1–4)
DxT Monitor requests 1, 3, 4
- SideNav
headerDivider(request 1): a full-width 1pxaura-border-defaultline underheader, with the board's metrics — the header at least 72px tall, its content 20px from the top and start, the line at its foot, 16px before the first item. The collapsed rail keeps its 8px sides. Withoutheaderit does nothing. Defaultfalse; the plain header is unchanged. - PageHeader (request 3):
<PageHeader eyebrow="Monitor / Sites" title="Sites" meta="12 sites · updated 2 min ago" actions={…} />— eyebrow in mono 12/1.7, the title anh1(headingLevel2 or 3 when the page already has one) in the display face at 36/1.15, weight 600, -0.01em (28px below 640px; a 1.4 line underlang="th"), meta 13/1.5, 4px between them, actions at the end on the text's last line, at least 24px away, wrapping under the text on a phone.titleIdnames the heading foraria-labelledby. It has no outer padding: inside AppShell, the content padding is the page margin (16/24px on phones, 24px from 640px, 32px from 1024px) — don't add your own. Stateless, so/serverexports it too. - Skeleton
variant="pill"andvariant="badge"(request 4): a StatusPill's size (20px tall, 22px underlang="th", 72px wide) and a Badge's (20px in every language, 56px wide), fully rounded, so a loading list's grey pills don't move the row when the real ones arrive.widthsets the width.
Icon tips (request 2)
- IconButton, Pagination's arrows, an icon-only SegmentedControl option and a cut Tag show their name in AURA's own tip instead of the browser's
title: themed (light and dark), Thai-sized underlang="th", shown on hover after 400ms (at once while another tip is up) and on keyboard focus (the native title never showed for keyboard or touch users), hoverable (WCAG 1.4.13: it stays while the pointer is over it, yet lets clicks and hovers through, so it never covers the ⋯ in the row above), hidden by Escape (inside a Dialog or Drawer, Escape closes that, and the tip with it), a press, blur or leaving. A tip opened by keyboard focus stays until focus moves, whatever the mouse does. It flips below at the top of the screen, stays on screen, and goes when its element leaves the page. - One tip and one set of document listeners serve the whole page — no state per button — so a DataTable with a ⋯ in every row costs nothing. The elements carry
data-aura-tip; the tip isaria-hiddenand never joinsaria-describedby, because it repeats the accessible name (a screen reader hears it once). - No tip when the button is already wrapped in
<Tooltip>(that one speaks), when it is a menu trigger with its menu open, or when you pass your owntitleto IconButton (kept as a native title). - Tests that looked up the
title(getByTitle,toHaveAttribute('title', …)) on these elements should use the accessible name (getByRole('button', { name })) ordata-aura-tip.
5.31 — Chamber-OS addenda 41–42
labelAddonon TextField, Textarea and PasswordField (item 138): a node between the label and the box, in the field's 6px gap — e.g. the phrase to type on a typed confirmation, as a chip with a copy button, so it is read before the box and can be copied. The hint and error stay below the box. The addon's box is.aura-field__addonwith the id{id}-addon, and that id comes first in the input'saria-describedby(then the error or hint, then your ownaria-describedbyids), so a screen reader reads it after the label; keep any button names in it short ("Copy").labelAddonDescribes={false}keeps it out — for an addon that isn't a description (a "Forgot password?" link), or to name only part of it (give that part an id and pass it inaria-describedby); ids are never repeated. A focusable control in it comes before the input in tab order; clicking the label still focuses the input.FieldtakeslabelAddontoo, for a custom control (add{id}-addonto that control'saria-describedbyyourself). The other fields (Select, Combobox, NumberField, the pickers, FileUpload) don't take it yet. Unset, nothing changes.- "Chosen" follows the brand (item 139):
createTheme,<ThemeStyle>andnpx aura-theme --brandnow emit--aura-control-checked-bg/-fgfor light and dark, so a themed product's checked box, radio dot, switch, active tab underline, current page, selected day and year, completed step and checked menu item match its links and focus instead of staying violet. Light is the theme's accent itself; dark is the brand's 300 step, lightened to at least 3:1 on the dark surface and canvas (as AURA's own violet-300). The mark on it is white or ink, whichever reads better; both are in the theme's contrast checks (fill 3:1 on surface and canvas, mark 4.5:1 on the fill). In aura.css the light--aura-control-checked-bgis nowvar(--aura-accent-violet)(the same violet-700 by default), so a hand-written:rootoverride of the accent carries it too; a scoped override needs the generator, or both tokens. Unthemed, nothing changes. Remove any hand-written--aura-control-checked-*stand-in after regenerating the theme.
5.30 — Visual polish (audit, Oct 2026) and Chamber-OS addenda 38–40
One release: everything below ships together in 5.30.0.
Menus stay open on scroll; StatusPill weight (addenda 39–40)
- An open menu follows its trigger (item 136): DropdownMenu and every menu built on it (DataTable's row ⋯ and column menus) now move with the trigger on scroll and resize — a slight finger drag during a tap, the phone's address bar or keyboard no longer close it, as Popover already behaved. It closes once the trigger is wholly out of view (the page's, or a box around it that clips it — a fixed bar inside a scrolling wrapper isn't clipped by it, and
overflow-x: cliponly clips sideways), without taking focus back so the page doesn't jump; inside a Dialog or Drawer focus goes back to the trigger without scrolling, so it stays in the dialog. Escape, a pointerdown outside and choosing an item close it as before, with focus back on the trigger. No new prop. - StatusPill is 500 (medium) (item 137) wherever it sits — before, it took its parent's weight (regular beside a heading, bold inside bold text). Badge stays 600, so the two stay distinct.
44px fields and choice rows (addendum 38)
touchHeighton TextField, Textarea, Select, RadioGroup and Checkbox (item 135):touchHeight="always"gives a 44px input box and 44px choice rows at every width — also with a mouse and insidedensity="compact"— for a form whose spec asks for 44px targets everywhere (Chamber-OS's issue dialog: zero-rate radios, certificate number, typed phrase).touchHeight(true) does it only below 640px or on a coarse pointer, as Button'stouchHeight; touch screens already had 44px fields and rows. A one-line option's label is centred in its row (a Checkbox row is 45px: its label sits on a 21px line); a row with a description grows. On a Checkbox it applies to a labelled box; a bare box useshitArea. Textarea accepts it too, but is always at least 96px tall, so nothing changes there. In a horizontal RadioGroup each option keeps its own height (one-line options stay centred beside a taller one with a description).- How: the field's root gets
is-touch-always/is-touch, which sets its own--aura-input-heightto--aura-touch-target(44px) — any control inside that reads the input height follows, whatever density is around it. Label, hint and error ids are unchanged; Button'stouchHeightis unchanged.
Visual polish, part 2
- "Chosen" is violet everywhere (audit item 4): a checked Checkbox or Radio, a Switch that is on, the active Tab's underline, the current Pagination page, the selected Calendar day and year, a completed Stepper step and a checked menu item use the new
--aura-control-checked-bg/-fg(violet-700 with a white mark in light, violet-300 with a zinc-900 mark in dark). Buttons stay ink / off-white. A disabled Switch that is on keeps its whole violet track (faded, it fell under 3:1 from a disabled one that is off) and fades its thumb instead. - Tables say when they scroll (item 9): a Table wider than its box shows a soft shadow at the edge with more to scroll (CSS only,
--aura-scroll-shadow; body rows show it, the header band and totals rows cover it; set--aura-tbl-bgif you give the box another ground); a DataTable with columns past its end edge shows the same shadow over the grid's end edge, inside its scrollbars (has-more-x), until scrolled to the end. Stacking stays opt-in (stackBelow) — no layout changes by default. - Table columns are never squeezed to one letter (item 8): from 640px a cell is at least 10ch wide; a longer word (an email, a URL) still breaks to fit rather than widening the table; numbers already stay on one line. Phones are unchanged (a table still fits its box): give a table that is too wide for a phone
stackBelow. - Totals (item 20): a 2px rule in the hairline colour over the totals (DataTable and Table), not the strong border that glared in dark; with several totals rows the last is 14px bold; a pinned DataTable totals row casts a soft shadow over the rows under it.
- SegmentedControl (items 11–12) is as tall as a field with a mouse (
--aura-input-height: 44px, 36px in compact;size="sm"unchanged; icon-only options stay square; on touch screens each option is 44px, as before), and the selected option is 600 weight on its own thumb colour (--aura-segmented-thumb-bg), which is lighter than the track in dark instead of darker. - FilterSelect (item 11): from 640px the face has a field's inset and 14px text, like the search box beside it; phones keep the tight face so three filters and a chip share a row.
- Smaller fixes: the show-password button is fg-secondary like a field's other icons (item 14); a removable Tag's × is 14px (item 17); a plain link (no class) inside a Card, Stat, Alert, EmptyState, field, tab panel, AppShell main, table or Dialog takes the accent colour, at zero specificity so your own link rules win (item 18).
- Not changed, on purpose: a ghost button that starts an ActionBar keeps its box at the bar's edge (Chamber-OS 116 relies on it); the Progress track keeps its 3:1 edge (WCAG 1.4.11); DatePicker's calendar button and TimePicker's clock stay where they are (one is a button, one a label icon).
- Size budgets:
styles.css32 kB, DataTable 21 kB,aura.bundle.jsand the whole package 66 kB.
Visual polish
A visual audit of every Storybook story in light, dark and at 390px. This release takes the system-wide fixes; colour hues and layout defaults are unchanged.
- Dark mode: the primary button, checked box, selected day, completed step,
bg-surface-strongandbg-inverted(tooltips, counts) are off-whitezinc-200instead of pure white, which was the brightest thing on a dark page. Primary hover iszinc-100. - Status fills: Blocked is
red-800in light (quieter in a long table, and further from the lime Ready fill); Warning isamber-600in dark too (amber-500 glowed). Ready, Warning and Blocked still differ by lightness, not only hue. - Neutral fills show on the light canvas:
--aura-status-neutral-bgiszinc-200(waszinc-100on azinc-50page), so the neutral StatusPill, tab counts, Avatar and SideNav initials are one step darker in light. The neutral Badge and the EmptyState icon disc use it too. - DataTable header band has its own token,
--aura-table-head-bg: the canvas in light,#111114in dark — between the page and the card, so it no longer reads as a hole. Table's header and totals rows (also the totals card) and DataTable's totals row use it. - Disabled filled buttons (primary, danger, creative) use new tokens
--aura-button-disabled-bg/-fg: a grey fill with muted text at full opacity, not a faded copy (faded red read as pink, faded ink as a separate grey button); the creative shadow goes. Secondary, ghost and danger-secondary keep the fade. In forced colours every disabled button shows GrayText and the fade. - Buttons are always the AURA sans (
font-family: var(--font-sans)), also outside a root that sets the font. An app that sets its own font onbodyshould set--font-sanstoo, or its buttons stay Inter. - Table caption has 12px below it, so it no longer touches the header.
- DataTable skeleton action bars take the button's own shape (
--aura-button-radius). - Thai: where the language is Thai (
lang="th"on the page or a part), StatusPill, Badge and tab counts are 12px (the loading pill matches), BottomNav labels 12px, Stepper descriptions 13px with a 1.6 line, SideNav labels on a 22px line, field labels, hints and errors on a 1.6 line, Radio and Checkbox descriptions 13px on a 1.6 line, and a stacked card's field labels 12px. Alang="en"part inside keeps the Latin sizes. These follow thelangattribute, notAuraProvider locale: set<html lang="th">(Next.js: the root layout's<html lang>), so portals (Dialog, Menu, Popover) get it too.
5.29 — Chamber-OS addendum 37
- DataTable loading rows match the rows they stand in for (item 134), so the layout doesn't jump when the rows arrive:
- Height: with
rowHeight="auto"skeleton rows get the same--autoclass and minimum height as real rows. A column's newskeletonLines(default 1, at most 10; apillcolumn keeps one pill) draws that many bars, one per text line —skeletonLines: 2for a reason and its evidence. On analign: 'end'column they line up at the end. Each bar sits on a 20px body line in the grid (a caption line is 16–18px, so a row can come out a few px taller than the real one) and on the 15px value line in a card. - Stacked cards: skeleton cards keep each field's label (
data-label) above its bar, as a real card does. - Row actions: an
actionscell draws one square and acard: 'footer'cell a bar across the cell (in a card, the full row, as a Button there fills it), at the small button's height (32px), in the grid as well as in cards. When the column's buttons usetouchHeight, give the column the newskeletonTouch: its bars are then 44px below 640px and on a coarse pointer, as those buttons are. A footer that holds only an IconButton still gets the full-width bar; the heights match. - Unchanged: skeleton rows stay
aria-hidden, the pulse stops under reduced motion,skeletonRows,bleed/bleedEnd. WithoutrowHeight="auto"the grid's skeleton rows keep the fixed row height.
- Height: with
5.28 — Chamber-OS addenda 35–36
- Applied-filter chips show their whole text (item 132): in
FilterBar's chips row a chip takes its text's own width ("Submitted: 28 Aug – 3 Sept 2026", "Member: Midsommar Hospitality Co., Ltd."); only a chip wider than the row is cut, with an ellipsis, and a long chip never widens the FilterBar or its parents (the chips row takes its width from the bar; a bar holding only chips needs a width from its parent, as a block has). A Tag outside FilterBar keeps its 24ch cap. Whenever a Tag's text is cut — the 24ch cap or the row — the full text shows on
