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

@jirawatpyk/aura-react

v5.52.0

Published

AURA Design System — React components (Enterprise Standard, Human Creative). Thai-first: Buddhist-era dates, Thai collation.

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-tokens

React 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; container sits flush at the bottom of a card. The status line is a live region. With selected, 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 from lg up 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 add viewport-fit=cover to your viewport meta.
  • DataTable footer: a totals row with the body's widths and alignment (a Totals card when stacked); stickyFooter keeps it in view in a height table. Truncated cells show their full text on hover and keyboard focus.
  • Tabs with href on every item render a nav of links (no panels), the current one aria-current="page".
  • Menu items: href (through linkComponent), tone: 'danger', and type: 'radio' with group / checked (menuitemradio in a labelled group).
  • Time zone: timeZone on AuraProvider, DatePicker, DateRangePicker and Calendar decides "today" (the marker, min/max="today", the first month shown); or pass today as 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 (with loadingRows, default 5): the head as given, then placeholder rows in its columns — and below stackBelow, 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 with touch), its width from skeletonWidth, else the cell's width or style.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) needs label for 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's loading.tsx), from the root and /server: columns ({ label, width, align, numeric, card, skeleton }), rows, and Table's density, stackBelow, stackStyle, bordered, bleed, bleedEnd, rowHeight, align. Build both from one column list and they can't drift.
  • Both are aria-hidden and inert, 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), puts aria-sort on 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, or sort is set) that wins and sortDirection is 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 without minWidth still 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 are aura-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 without wrap, 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__scroll contains 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-height grid scrolls inside first, then the page at its end. Remove any overscroll-behavior-y override 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-width columns and stacked cards ignore it. grow: 0 keeps 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 custom Field too.
<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)

  • invalid on TextField and Textarea (and PasswordField, which is a TextField; Field for 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) and aria-invalid="true" on the control, no error line, and aria-describedby as 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. Point aria-describedby at that explanation, or a screen reader hears "invalid" with no reason. error still 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 inside Field (with invalid on 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, not 480px. In a media query rem is the browser's default font size (Chrome, Edge, Firefox; not iOS Dynamic Type or Safari's text size), not the page's html font 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 the actionsBarBelow actions 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 actionsBarBelow bar 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 on ActionBar position="container" look. The page's end padding and scroll-padding-bottom follow 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 — actionsBarBelow no longer fixes the actions to the bottom: they stay in the header's own row (laid out by actionsFullWidthBelow if set), with no status line, page-end space or scroll padding; and an ActionBar (either position) 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-height is 0 meanwhile, so nothing sticky (a DataTable stickyHeader) 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 with hidesBottomNav still hides the BottomNav on a short screen while it sits at the end of its container. Wherever a BottomNav shows (any height), the page's scroll-padding-bottom now 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 of ActionBar position="viewport". Only the bar's look changed; nothing else about actionsBarBelow.

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; pass size="compact" (Chamber-OS: in its header wrapper) where the boards draw 26 / 32.
  • hero steps to 36px from 1024px (30px below), on a 1.2 line, as hero-lg does — the portal Main and Edit boards. hero-lg is unchanged (26px, then 36px).
  • --aura-page-header-hero-space: the room above a hero or hero-lg title, 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 to 0 below 1024px and 8px from 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" — before meta, in a div (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. Where titleHideBelow hides the title, the addon goes with it.
  • A chip eyebrow: eyebrow that is not text (a StatusPill, a member number, badges) renders as a wrapping row in a div, 8px apart, 4px above the title as before. Text stays the mono p.
  • size: default (28px, 36px from 640px), hero (30px on a 36px line) and hero-lg (26px, 36px from 1024px), the hero sizes with 16px more room above. Per page, --aura-page-header-title-size and --aura-page-header-title-line-height override any of them (staff pages at 26/32: set both). Under lang="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 /server the 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; start with its top — the title, when there is no breadcrumb or eyebrow.
  • Confirmed, no change: the text column's flex: 1 1 320px keeps a long title at least 320px wide; wide actions drop under it (checked at 700px). On a phone actionsFullWidthBelow="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's actions become 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), and actionsStatus is 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-bottom keeps a focused field above it. The main entry's PageHeader measures the bar (--aura-page-actions-height on <html>); the /server one 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 a transform, filter, backdrop-filter, will-change: transform, paint/layout containment or content-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 lg AppShell'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 actionsFullWidthBelow with 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: anywhere with hyphenation, never an ellipsis), growing taller rather than wider, at least 44px with touchHeight; 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's lang (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. Default ghost (no border).

5.43 — Chamber-OS addendum 50 (#148)

  • FilterBar collapseBelow with 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 without min-width: 0. The text box now never asks for more than the room it gets (.aura-input has min-width: 0 and 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 rows wrap (Stack) or fullWidth.

  • FilterBar collapseBelow (#146): 'sm' | 'md' — below 640px or 768px the filters passed as children fold 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 follows touchHeight. The search, the applied chips, Clear all, resultCount and actions stay in the bar; the count and actions wrap under the search row. The number is the count of filters (chips); filterCount overrides 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 in defaultValue. 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 strings filtersApplied(n) and done (th: ตัวกรอง, ใช้อยู่ 2 รายการ · เสร็จสิ้น; sv: Filter, 2 aktiva · Klar), and a sliders-horizontal icon (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-menubutton on any element of the page (as with data-aura-hide-bottomnav) and AppShell's phone bar drops its menu button, giving the title its room (a shell with no header then 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, /server too.
  • Menu links that open a new tab say so (request 21): a Menu or DropdownMenu item with href and target="_blank" is named "Visit site (opens in a new tab)" — the words are visually hidden, after the label, in the locale's language (opensInNewTab in the strings: เปิดในแท็บใหม่, öppnas i en ny flik). A custom strings pack without the key falls back to the built-in words. Other links you give a target (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-haspopup and aria-expanded="true" (DropdownMenu, Popover and Dialog triggers) keeps its hover fill. A disclosure button of your own (aria-expanded without aria-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-size is now max(<px>, <rem>) — for example max(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 with rowHeight="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 Stack without wrap holding buttons or tags can run past a phone's edge at a large default size; give it wrap.

5.39 — Chamber-OS addendum 46

  • Tabs touchHeight (#143): 44px tabs — buttons or links, underline or segmented — with true below 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 with touchHeight already reaches it (checked for Chamber-OS's audit log filter row).

5.38 — Chamber-OS addenda 43–45

  • FilterBar and FilterSelect touchHeight (#140): true makes 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's touchHeight — and 'always' at every width; touchHeight on 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 in actions take their own touchHeight. A SegmentedControl's track is 50px there (44px options plus its edge and padding), as with its own touchHeight. Opt-in rather than a new default, like every other touchHeight, so a narrow desktop window doesn't change size unasked.
  • touchHeight on 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 })} sets data-* attributes and an id on 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, and data-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 Button fullWidth. 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 /server too. 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 — with true below 640px or on a coarse pointer (the same rule as Button's and TextField's touchHeight), 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 other touchHeight props (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 no overflow-clip-margin: there rings in cells are drawn just inside the control, whole. Apps that set overflow: clip on .aura-table__td themselves 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 and rowHeight="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's li. Apps that added their own .aura-crumbs a::after rule 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) in aura-fg-tertiary on the plain surface and border, status word "No data" (th: ยังไม่มีข้อมูล, sv: Ingen data), value in the text colour. An unrecognised status now 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-height is 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-offset drops while it is away, so viewport ActionBars and toasts follow it down. ActionBar hidesBottomNav: 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 with data-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). title wraps 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. value sits top right in tabular figures, 600, in its own tone (valueTone; default the status's: a Down duration is red). meta is 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 in meta) too. href makes 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 an aria-label, so it always matches what is shown (WCAG 2.5.3). statusLabel overrides 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, so innerText and a copy include them; a labelled Sparkline in meta joins the link's name. Light and dark use the Alert tokens.
  • TileGrid: a ul of equal columns 8px apart, each child (and each child of a Fragment) one li; tiles in a row are as tall as the tallest. columns is responsive like Grid; default { base: 2, md: 3 }. Pass aria-label to name the list.
  • Sparkline: data oldest 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) with failures="band" (a light red band behind the point) or "ticks" (a red tick at the foot); default none. tone (neutral, accent, danger, warning, positive), size sm 80×24 (default) or md 120×32, min / max to put several on one scale (values beyond them are drawn at the edge). label makes it role="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 unless statusLabel; 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-hover is 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 keep aura-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, header and 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-height is 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 own div — a nav can't sit in the eyebrow's p (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 headerDivider takes 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 1px aura-border-default line under header, 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. Without header it does nothing. Default false; 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 an h1 (headingLevel 2 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 under lang="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. titleId names the heading for aria-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 /server exports it too.
  • Skeleton variant="pill" and variant="badge" (request 4): a StatusPill's size (20px tall, 22px under lang="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. width sets 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 under lang="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 is aria-hidden and never joins aria-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 own title to 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 })) or data-aura-tip.

5.31 — Chamber-OS addenda 41–42

  • labelAddon on 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__addon with the id {id}-addon, and that id comes first in the input's aria-describedby (then the error or hint, then your own aria-describedby ids), 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 in aria-describedby); ids are never repeated. A focusable control in it comes before the input in tab order; clicking the label still focuses the input. Field takes labelAddon too, for a custom control (add {id}-addon to that control's aria-describedby yourself). 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> and npx aura-theme --brand now emit --aura-control-checked-bg / -fg for 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-bg is now var(--aura-accent-violet) (the same violet-700 by default), so a hand-written :root override 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: clip only 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)

  • touchHeight on 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 inside density="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's touchHeight; 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 uses hitArea. 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-height to --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's touchHeight is 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-bg if 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.css 32 kB, DataTable 21 kB, aura.bundle.js and 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-strong and bg-inverted (tooltips, counts) are off-white zinc-200 instead of pure white, which was the brightest thing on a dark page. Primary hover is zinc-100.
  • Status fills: Blocked is red-800 in light (quieter in a long table, and further from the lime Ready fill); Warning is amber-600 in 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-bg is zinc-200 (was zinc-100 on a zinc-50 page), 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, #111114 in 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 on body should set --font-sans too, 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. A lang="en" part inside keeps the Latin sizes. These follow the lang attribute, not AuraProvider 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 --auto class and minimum height as real rows. A column's new skeletonLines (default 1, at most 10; a pill column keeps one pill) draws that many bars, one per text line — skeletonLines: 2 for a reason and its evidence. On an align: '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 actions cell draws one square and a card: '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 use touchHeight, give the column the new skeletonTouch: 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. Without rowHeight="auto" the grid's skeleton rows keep the fixed row height.

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