@astryxdesign/core
Breaking Changes
- DropdownMenu, ContextMenu, MoreMenu: removed the
hasAutoFocusprop. Menus now always focus their first item on open (the correct APG menu-button behavior). PreviouslyhasAutoFocus={false}left the menu keyboard-unreachable and undismissable — the prop existed only as an escape hatch for documentation previews, which no longer need it.
New Features
- Calendar:
weekStartsOnnow also accepts a three-letter day name ('sun'–'sat', case-insensitive) in addition to the numeric0–6, so the starting day is self-documenting at the call site (e.g.weekStartsOn="mon"). Numbers keep working unchanged. Adds an exportedDayOfWeekNametype and anormalizeDayOfWeekhelper (#2843) - CheckboxInput/Switch/Slider/RadioList/CheckboxList/SegmentedControl/Tokenizer/PowerSearch: disabledMessage prop shows a tooltip explaining the disabled state (#3509)
- DateInput can now be used inside InputGroup with shared addon styling and group label/description/status ARIA wiring (#3520).
- DateTimeInput: add a
timePlaceholderprop to customize the time-portion placeholder (previously hardcoded to "Select a time" with no override).placeholdercontinues to control the date portion; the focused typing hint (e.g. "e.g., 2:30 PM") is unchanged (#2729) - Toolbar, TabList, and SegmentedControl now show an ephemeral "← → to navigate" hint on first keyboard focus, teaching sighted users that arrow keys navigate within the group.
Adds a showcase block and Storybook story for useKeyboardHint. - Export
themeProps(and itsThemeProps/ClassProps/ClassValue/ThemeDataAttributestypes) from@astryxdesign/core/utils, so packages building on core can generate the stable astryx class +data-*attribute surface through the public API instead of reaching into core internals. - MultiSelector can now be used inside InputGroup as a decorated single-line control, sharing the group label, description, status, and connected border treatment (#3520).
- Typeahead, DateInput, DateRangeInput, DateTimeInput, and TimeInput:
disabledMessageprop shows a tooltip explaining the disabled state on hover and keyboard focus, keeping the control focusable viaaria-disabledwhile activation stays blocked (#3509) - Popover: expose hasLightDismiss and add hasEscapeDismiss so consumers can opt out of outside-click and Escape dismissal for explicit-dismiss surfaces like onboarding coachmarks. usePopover accepts the same new hasEscapeDismiss option; with it off, no handler is registered on the shared Escape stack so the key falls through untouched (#3287)
- Selector/MultiSelector: disabledMessage prop shows a tooltip explaining the disabled state (#3347)
- Selector can now be used inside InputGroup as a decorated single-line control, sharing the group label, description, status, and connected border treatment (#3520).
- Stack/HStack/VStack: add
padding,paddingInline,paddingBlock(spacing-scale inner padding) andisScrollable(overflow: auto) props; StackItem also gainsisScrollable. These match the existingpadding/isScrollableprops onCard,LayoutContent, andLayoutPanel, so common frame layouts no longer need inlinestyle={{}}for padding or the flex scroll-region pattern. - Standardize layout component sizing props. Add
maxWidth/minHeighttoStack,Grid, andCenter(matchingSection/Card), migrateLayout/LayoutHeader/LayoutFooter/LayoutPanelsizing to the sharedSizeValuetype, and drop redundantxstyle/className/stylere-declarations onStack,StackItem, andLayout. No runtime behavior change. - Add a plugin-contributed right-click context-menu system to
Table. Right-clicking a column header or a row shows a menu of actions aggregated from every enabled plugin (instead of the browser's generic menu). - TabList is now a single tab stop with arrow-key navigation between tabs (roving tabindex) — Arrow keys move focus, Home/End jump to the ends, disabled tabs are skipped, and focus wraps. This does not change the semantic roles (still
<nav>/aria-current); the full tablist/tab/tabpanel conversion is tracked separately in #3335. Reference (#3343). - TextInput/NumberInput/TextArea/FileInput: disabledMessage prop shows a tooltip explaining the disabled state (#3509)
- Add InputGroup compatibility for TimeInput (#3520)
- TreeList now implements the full WAI-ARIA APG Tree View keyboard pattern. Roving tabindex places a single tab stop on the treeitem rows (defaulting to the selected item or the first enabled row), and arrow keys move focus in visible order: ArrowDown/ArrowUp step between visible rows (skipping disabled), ArrowRight expands a collapsed parent then enters its first child, ArrowLeft collapses an expanded parent or moves to the parent row, and Home/End jump to the first/last visible row. Enter and Space activate the row's action (or toggle a parent without its own action), and typeahead moves focus to the next row whose label matches the typed characters. Each treeitem now also exposes
aria-level,aria-posinset, andaria-setsize. Builds on the interim keyboard-expandable toggle in (#3344). Part of the accessibility & keyboard-management program (#3343). - Add InputGroup support for Typeahead (#3520)
- Add
useAnnounce— an accessibility hook that speaks messages to screen readers through persistently-mounted, visually-hidden polite/assertive live regions. Because the regions are created once (not together with their content), announcements are reliable. Wired intoTypeahead/BaseTypeaheadto announce result counts and "no results found" during search, which were previously silent (#3343). - Add
useKeyboardHint— shows an ephemeral "← → to navigate" badge anchored to the focused item when a composite widget first receives keyboard focus. Teaches sighted keyboard users that arrow keys navigate within a roving-tabindex group. Renders in the top layer (popover="manual") with CSS anchor positioning so it is never clipped by overflow containers. Auto-dismisses on first arrow press, timeout, or blur; does not re-show for that instance.aria-hidden(visual-only; screen-reader users already hear the role). - Add
useTypeahead— a first-character (type-to-focus) search hook, and wire it intoDropdownMenu. Typing a letter jumps to the next menu item whose label starts with it; repeated presses of the same letter cycle through matches; the buffer resets after 750ms; disabled items are skipped. The hook is additive and collection-agnostic (composes withuseListFocus/useGridFocusviaonMatch), so menus/listboxes gain APG typeahead which astryx previously lacked (menus-11, infra-14) (#3343). useListFocusgains opt-in roving-tabindex ownership (hasRovingTabIndex), caret-aware arrow handling that leaves keys to nested text inputs and contenteditables (hasCaretGuard), RTL horizontal navigation (isRtl), ahasHomeEndtoggle, andorientation: 'both'for four-arrow navigation.Toolbarnow uses it — it is a single tab stop and no longer steals the caret from a toolbar text input or composer (#3343).- Add
VisuallyHidden— an accessibility primitive that renders content in the accessibility tree while hiding it visually. Use for accessible names on icon-only controls,aria-liveannouncement regions, and supplementary screen-reader context. Renders a<span>by default; useasfor block/live-region elements (#3338).
Fixes
- Announce file selection, page changes, and multi-select count via the live-region hook (#3343)
FileInput now announces successful file selection, Pagination announces page changes, and MultiSelector announces selection-count changes, all through the shared visually-hidden polite live region so these previously-silent surfaces are audible to screen-reader users. - Avatar: an avatar with no
nameoraltis now decorative (role="presentation"+aria-hidden) instead of being announced with the meaningless generic name "Avatar". When named, the inner<img>uses an emptyaltso the accessible name isn't announced twice (once by therole="img"wrapper, once by the image) (#3343). - Breadcrumbs: auto-detected current breadcrumb now places
aria-current="page"on the item's content element (link/button/span), matching the explicitisCurrentpath, instead of on the outer<li>. When the last breadcrumb is a link, the anchor itself now carriesaria-currentso screen readers announce it as the current page (#3343). - Breadcrumbs: BreadcrumbItem now forwards remaining BaseProps (id, aria-, role, event handlers, data-) to the underlying
<li>element. Previously these props were accepted by TypeScript but silently dropped at runtime. - ButtonGroup: remove the invalid
aria-orientationattribute from therole="group"element, which was flagged by axe (aria-allowed-attr). Orientation is still reflected viadata-orientationand drives keyboard navigation and styling, so behavior is unchanged. Also fixes Schedule, which reuses ButtonGroup internally. - Calendar: cross-month keyboard navigation now resolves the focused date from the machine-readable
data-dateattribute instead of parsing the localizedaria-labelwithnew Date(). Previously, month-boundary arrow keys and PageUp/PageDown silently stopped working in non-English locales (e.g. fr-FR, ja-JP) where the label was unparseable (#3343). - Calendar: the month view now uses a valid ARIA grid structure — the weekday names are
columnheadercells inside thegrid, each week is arowwhose direct children aregridcells, and week-number cells arerowheaders. Arrow-key navigation now lands on the correct dates when some days are disabled (viamin/max/dateConstraints): moving up/down keeps the true 7-column geometry and skips disabled days to the same weekday, instead of shifting to the wrong weekday. (#3343) - Carousel, Lightbox: keyboard focus is no longer trapped on invisible or unmounted edge controls. Carousel's scroll left/right buttons, when at an exhausted edge (or when there is no overflow), were hidden with
opacity: 0/pointer-events: nonebut stayed in the tab order, so keyboard users could focus invisible controls (WCAG 2.4.7); they are nowdisabledin that state (still mounted, removed from the tab order and a11y tree). Button-driven scrolling also now respectsprefers-reduced-motion(usesbehavior: 'auto'instead of hardcoded'smooth'). In Lightbox gallery mode, the Prev/Next buttons previously unmounted at the range ends, so advancing onto the first/last item removed the focused control and dropped focus to<body>, dead-ending keyboard navigation; they now stay mounted and becomedisabledat the boundaries so focus stays within the dialog and arrow-key navigation keeps working (#3343) - Chat composer: the message input now uses
role="combobox"when triggers (mentions, slash commands) are configured, and stays a plainrole="textbox"otherwise. Combobox attributes (aria-expanded,aria-haspopup,aria-controls,aria-activedescendant) are only valid on a combobox, so applying them to a textbox was flagged by axe (aria-allowed-attr). (#3343) - ChatMessageList: add an
isStreamingprop that marks therole="log"regionaria-busywhile an assistant message streams in. Previously the polite live region re-announced the accumulating partial text on every token; withisStreamingset for the duration of a stream, screen readers wait and announce the completed message once (#3343). - CheckboxInput: stop setting a redundant
aria-checked="mixed"on the native<input type="checkbox">for the indeterminate state. The nativeindeterminateDOM property (which browsers already map toaria-checked="mixed") is authoritative; the extra attribute could desync from or override the native state (#3343). - CheckboxList: the checkboxes are now wrapped in a
role="group"named by the field label viaaria-labelledby(and associated with the description/error), instead of a flat list with an orphaned labelhtmlFor. Screen-reader users now hear the group's name and context (#3343). - CheckboxListItem: drop the invalid
aria-checkedfrom the list row.aria-checkedis not allowed onrole="listitem"(axe: aria-allowed-attr); checked state is already conveyed by the row's inner checkbox. This also fixes Markdown task lists, which render task items through CheckboxListItem. (#3343) - Citation: linked number-variant badges keep their accent-muted background (previously rendered transparent), and the badge now uses the secondary text color (#3508)
- Citation: only apply
role="doc-noteref"on the linked (anchor) form. On a plain unlinked span the role is not permitted (axe: aria-allowed-role), so it is omitted there while thearia-labelstill names the citation. (#3343) - Selector/MultiSelector/Typeahead: the highlighted option is now scrolled into view during keyboard navigation, so arrow keys no longer move the highlight off-screen in long lists (matches CommandPalette) (#3343).
- CommandPalette: the empty state ("No results") no longer flashes when typing further characters into an already-empty search. The empty state stays mounted for the full duration of the pending search instead of briefly unmounting and re-appearing.
- ContextMenu: Escape now closes the menu even when it was opened without auto-focus (e.g. table row menus), via a document-level Escape listener instead of one that only fires when focus is inside the menu. Focus is also restored to the previously focused element on close, instead of falling to
<body>. Escape during IME composition is ignored (#3343). - ContextMenu: can now be opened on touch (long-press) and by keyboard. A long-press (500ms, cancelled by a 10px finger move) opens the menu at the touch point — previously context menus were unreachable on iOS Safari, which never fires
contextmenuon long-press. A keyboard-initiatedcontextmenu(Shift+F10 / the Menu key), whose coordinates are (0,0), now anchors the menu to the trigger's box instead of the viewport corner (#3343). - DateInput/DateTimeInput: the date field's calendar popover can now be opened from the keyboard with
ArrowDown/Alt+ArrowDown(APG combobox), not just by clicking. DateTimeInput's time input no longer uses a hardcoded Englisharia-label="Time"— it defaults to"{label} time"(tied to the field label and localizable) and accepts an explicittimeLabelprop (#3343). - DateRangeInput: the preset sidebar is now a labeled
role="group"of action buttons instead of arole="listbox"ofrole="option"buttons. The listbox/option roles announced a single-tab-stop listbox that contradicted the actual Tab-between-buttons interaction (no listbox keyboard model existed). The currently-applied preset is marked witharia-currentrather thanaria-selected(#3343). - docs.mjs: resolve the package directory with fileURLToPath so component docs work on Windows, where URL.pathname yields drive-letter paths like /D:/... that made --list silently print nothing and single-component lookup crash with ENOENT (#3331)
- DropdownMenu: pressing Tab in an open menu now closes it (APG menu-button pattern) and returns focus to the trigger, instead of leaking focus into the page while the menu stayed open (#3343).
- Escape now dismisses only the top-most open layer instead of closing every open layer at once — a popover or menu nested inside a Dialog no longer closes both on a single Escape press. Also guards against IME composition: pressing Escape to cancel a CJK/IME composition inside a Dialog or popover no longer closes the overlay (#3343).
- Vertically center the optional/required indicator in
FieldLabel. ThelabelandoptionalRequiredstyles never set an explicitlineHeight, so both fell back toline-height: normal(~1.2), producing mismatched line boxes (~16.8px for the 14px label vs ~14.4px for the 12px "∙ Required" text). WithalignItems: centerthe smaller indicator centered within its shorter box and rendered visually high relative to the label. - FileInput:
aria-describedby,aria-required, andaria-invalidnow sit on the focusablerole="button"control instead of the visually-hiddentabIndex={-1}file input that never receives focus. Screen-reader users now hear the field's help text, required state, and error state. The hidden native input is also markedaria-hiddensince it is not focusable (#3343). - useFocusTrap: the focusable-element detection now includes
contenteditable, media withcontrols,iframe, and an open<details>'s<summary>, and excludes elements hidden viadisplay:none/visibility:hiddenor insideinert/hiddensubtrees. Previously a trapped surface whose only interactive content was (e.g.) a contenteditable composer could let Tab escape (infra-8). - Grid no longer writes
grid-template-columns/grid-auto-rowsas raw inline styles. Track templates now use StyleX dynamic styles (CSS-variable indirection), so consumerxstyleoverrides — including responsive@mediaoverrides — take effect instead of being defeated by inline styles. - NumberInput, DateInput, and DateTimeInput now set
aria-invalid="true"and announce a short message (e.g. "Invalid number" / "Invalid date" / "Invalid time") via a visually-hiddenrole="alert"live region while the currently typed input is unparseable, instead of only dimming the text color and then silently reverting the value on blur. Screen-reader users now get feedback that their entry was rejected rather than silence, and the invalid state is no longer signaled by color alone (WCAG 3.3.1 Error Identification, 1.4.1 Use of Color). The revert-on-blur behavior is unchanged. (#3343) - InputGroup: grouped TextInput and NumberInput controls now include both the group label and their own label in the input name, while preserving the group's description and status associations (#3343).
- InputGroup: the group is now named by the field label via
aria-labelledbyinstead of a duplicatedaria-label, and the label no longer carries an orphanedhtmlFor(itsinputIdwas never handed to a child). Uses theFieldisGroupLabel/labelIDsupport (#3343). - Kbd: the component is no longer entirely
aria-hidden. It now exposes a spoken accessible name (e.g. "Command + K") built from screen-reader-friendly key labels, while the visual glyphs (⌘, ⇧, ↵, …) are hidden from assistive tech. Previously any shortcut communicated only viaKbd— including CommandPalette's footer hints — was invisible to screen-reader users (#3343). - Use Kbd for arrow-key navigation hints and space the hint farther from focused controls so outlines stay visible.
- Layer/popover entry animations now honor
prefers-reduced-motion. The shared layer slide/scale keyframes (used by DropdownMenu, Popover, HoverCard, Tooltip, Selector, and other popover surfaces) and theuseEntryAnimationpresets disable their keyframe animation underprefers-reduced-motion: reduce, so layers appear instantly instead of translating/scaling in (#3343). - useLayer: the popover
toggleevent listener is now removed when the layer element detaches or when the handler identity changes (a newonHide), instead of accumulating stale-closure listeners on the same element. This prevents duplicate/staleonHidefiring over a layer's lifetime (#3343). - Link: hovering now shifts the link color via the hover tint (
color-mixwith--color-tint-hover, matching Slider/Switch/RadioList). This gives always-underlined links (hasUnderline) a visible hover affordance they previously lacked — their underline never changed on hover — without altering the default link's underline-on-hover behavior (#2852) - Link: external links (
isExternalLink) now include visually-hidden "(opens in new tab)" text so screen-reader and cognitive-load users are told about the new-tab context change — previously only a decorativearia-hiddenicon signalled it. The text is overridable via the newnewTabLabelprop for localization (#3343). - DropdownMenu/ContextMenu: the
role="menu"container now has an accessible name — DropdownMenu names it from the trigger's label, and ContextMenu exposes amenuLabelprop (default "Context menu"). ContextMenu also no longer placesaria-haspopup="menu"on its role-less, non-focusable trigger wrapper, where it conveyed nothing useful to assistive tech (menus-13, menus-15). - NavHeadingMenu:
onClick-only items (rendered without anhref) now activate on Enter and Space. Previously theserole="menuitem"elements had no keyboard activation, so keyboard and screen-reader users could focus them but not trigger them (#3333) - Pagination: coerce pageSize to a positive integer so 0/NaN/negative values no longer crash the dots variant (RangeError: Invalid array length) or render Infinity/NaN page counts; the Table pagination plugin applies the same guard since it computes totalPages independently (#3372)
- usePopover: add a
role('dialog' | 'none') andisModaloption so listbox and menu popups no longer announce a false modal dialog. Selector, MultiSelector, BaseTypeahead, PowerSearch, DropdownMenu, TabMenu, and the Chat mention menu now expose their ownlistbox/menurole instead of being wrapped inrole="dialog" aria-modal="true"while focus stays on the trigger. Genuine dialog popovers are unchanged (#3343). - ProgressBar: determinate progress now uses
role="progressbar"instead ofrole="meter".meteris for static gauges (disk usage, battery) that screen readers do not treat as live-updating task indicators; a progress bar conveys task completion and should be announced on update. Indeterminate progress was alreadyprogressbar(#3343). - RadioList: give an unselected radio group a deterministic keyboard tab stop. When focus enters a group with no selected value, the group now normalizes the entry point — first radio when tabbing forward, last radio when tabbing backward — matching the ARIA radio-group pattern. A selected value keeps its native tab stop, and moving between radios inside the group is never redirected. (#3390)
- RadioList: the group is now named via
aria-labelledbypointing at the field label element, and the label no longer carries an orphanedhtmlFor(it pointed at an id no radio used, so clicking it did nothing and the group was double-labeled).Field/FieldLabelgain optionallabelIDandisGroupLabelprops to support grouping controls (#3343). - Table, CodeBlock, and Markdown: the keyboard-focusable scroll containers now use
role="group"instead ofrole="region".regionis a landmark, so multiple same-named scroll regions on one page (e.g. several tables labelled "Table") triggered axelandmark-unique.groupkeeps the label and keyboard focusability without creating duplicate landmarks. (#3343) - CodeBlock/Table/Markdown: overflowing scroll regions are now keyboard-focusable (
tabIndex,role="region") so keyboard users can scroll long code and wide tables. CodeBlock's Copy button no longer collapses the block when clicked, and is no longer nested inside the collapsible header'srole="button"(#3343). - SegmentedControl: a disabled segment (including when the whole control is disabled) is no longer a keyboard tab stop. Previously the selected segment kept
tabIndex={0}while disabled, so it was focusable but silently dead — arrow keys and activation did nothing (#3343). - SegmentedControl: keep the radiogroup reachable by Tab even when the current
valuematches no item (or the selected item is disabled). Previously a stale/unmatched value left every segment attabIndex={-1}, so the whole control dropped out of the tab order. The first enabled segment is now promoted to the tab stop (#3343). - Selector/MultiSelector:
DeleteandBackspacenow clear the value from the focused trigger whenhasClearis set, so clearing a selection is no longer mouse-only. The clear button was already keyboard-reachable; this adds the keyboard shortcut path (#3343). - Selector/MultiSelector (
hasSearch): the popup's search input is now the combobox — it carriesrole="combobox",aria-expanded,aria-autocomplete="list",aria-controls, andaria-activedescendant, so screen readers announce the highlighted option as ArrowUp/Down move it. Previously the search input was a baresearchboxwhilearia-activedescendantstayed on the (now-unfocused) trigger, leaving highlight changes silent. InhasSearchmode the trigger is a plain button that opens the listbox rather than a second combobox (#3343). - Selector, MultiSelector: the combobox trigger is now keyboard-focusable (
tabIndex=0when enabled). Previously it wastabIndex=-1, so keyboard and screen-reader users could not open or operate the control in the default (non-search) mode. The Clear button is now keyboard-reachable too (#3320) - useLayer: guard
showPopover()/hidePopover()behind a feature check so overlays degrade gracefully instead of throwing a TypeError on browsers without the Popover API (Safari <17, Firefox <125) (#3343). - SideNavItem: for split-action items (a collapsible item with its own link/action),
aria-current="page"now sits on the focusable link instead of the non-interactive wrapper<div>, so screen readers announce the current page on the actual navigation element (#3343). - SideNavHeading: label the product icon link with the heading text so it has an accessible name. When superheadingHref, headingHref, and a menu were all set, the icon link to the heading href rendered with no text or aria-label, so axe flagged it under link-name and screen readers announced an unlabeled link. (#3343)
- Skeleton: the loading placeholder is now
aria-hiddenby default (it's decorative — the surrounding region conveys the loading/busy state) and its pulse animation is disabled underprefers-reduced-motion: reduce. Thearia-hiddendefault can be overridden by consumers (#3343). - Spinner: the rotation animation now slows substantially under
prefers-reduced-motion: reduce(matching ProgressBar) instead of spinning unconditionally. Therole="status""Loading" announcement still conveys busy state (#3343). - Table: proportional() and pixel() no longer throw when called from a React Server Component. The Table barrel carried a 'use client' directive that marked the pure column utilities as client functions; the directive now lives only on the component modules, and Table.doc.mjs documents which parts of the data-driven API are server-safe (#3457)
- TabList: stop rendering aria-orientation on the nav element. aria-orientation is not an allowed attribute on the navigation role, so it triggered a critical axe aria-allowed-attr violation (also surfaced via Toolbar and ToolbarEdgeCompensation stories that reuse the same DOM). The orientation prop still drives arrow-key navigation and the keyboard hint. (#3343)
- TimeInput/DateTimeInput: parse dotted meridiems correctly. "2:30 p.m." was silently accepted as 02:30 and "12 a.m." as noon because the meridiem-detection regex did not allow the dots that the AM/PM regexes accept; hasMeridiem is now derived from those regexes so they cannot drift (#3462)
- Toast: keyboard users can now reach and manage notifications. Pressing
F6jumps focus into the toast viewport (the newest toast's first control, or the container). Dismissing a toast that holds focus now hands focus to a remaining toast — or restores the element focused before entering the viewport — instead of dropping to<body>. Auto-hide timers also pause while the window is blurred and resume on focus, so a toast no longer silently expires while you're in another window or tab (#3343) - Toast: remove the invalid
aria-modalattribute from the notifications viewport.aria-modalis only valid onrole="dialog"/alertdialog, so declaring it on therole="region"viewport was flagged by axe (aria-allowed-attr). Because the viewport renders on every page, this surfaced the violation across the whole app (#3343). - Tooltip: satisfy WCAG 1.4.13 (Content on Hover or Focus). Tooltips can now be dismissed with
Escapewhile visible, and stay open when the pointer moves from the trigger onto the tooltip surface (a short hover bridge).useLayercontext render props gainonMouseEnter/onMouseLeaveon the layer container to support hoverable overlays. - TopNavHeading: give the logo an accessible name when it links to a destination. The logo image is decorative, so a logo wrapped in
headingHrefproduced an unnamed link (axe: link-name). It is now labelled fromheading(or a new optionallogoLabelprop for logo-only headings). (#3343) - TreeList: parent rows can now be expanded and collapsed from the keyboard. Any item with children renders a real focusable toggle button with
aria-expanded, so expansion no longer requires a mouse — previously items without anonClick/hrefhad no focusable control at all (#3343). - Typeahead/Tokenizer: close the results dropdown when focus leaves the input (e.g. tabbing away), matching the existing outside-click and Escape dismissal. Previously the menu could stay open after the trigger lost focus.
- ContextMenu and NavMenu heading menus now support first-character typeahead (type a letter to jump to the matching item), via the shared
useTypeaheadhook — matching DropdownMenu. MoreMenu inherits it through DropdownMenu (#3343). - useListFocus (menu/toolbar keyboard navigation): arrow keys, Home, and End now skip disabled items instead of stalling on one whose
.focus()silently no-ops. This unfreezes keyboard navigation in NavHeadingMenu and Toolbar/ButtonGroup when a disabled control is present. The default item selector also now matchesmenuitemradio/menuitemcheckbox(#3343).
Documentation
- Document Carousel's hasEdgeFade and padding props, supported by the component but missing from the props table, docsZh, and docsDense (same omission previously fixed for Text's justify and ToggleButton's isIconOnly). Also correct the Fade edges anatomy row to optional, since the mask is suppressible via hasEdgeFade (#3332)
- Fix stale references in the core README: rename the "XDS CLI" section to "Astryx CLI", correct component names to their current bare exports (Layout, AppShell, TopNav, SideNav), and update remaining XDS brand mentions to Astryx.
- Link: document
type="inherit"for inline links. The value was already supported (forwarded toText, which rendersfont-size/line-height: inherit), but undocumented — clarified thetypeprop JSDoc, added an inline-link example, and added regression tests covering the inherit/default-body behavior (#2927) - MetadataListItem: its docs page preview now renders inside a MetadataList wrapper with realistic defaults (#3318)
Other Changes
- Consolidated 5 inline visually-hidden style blocks into the shared VisuallyHidden primitive (Button, Link, ProgressBar, Switch, TextArea); no behavior change.
- Rename the Field
labelElementIDprop tolabelID, matching the(part)IDnaming convention used by the sibling props (inputID,descriptionID,messageID). The disambiguation between "the id applied to the label element" andinputID("the control the label points at") now lives in the prop JSDoc rather than the name. Also renamed on FieldLabel and updated the RadioList/CheckboxList/InputGroup consumers. No behavior change. (#3343) useGridFocusgainshasRovingTabIndex,handleFocus, andisRtl, matching theuseListFocusAPI. Calendar now usesuseGridFocusto own its roving tab stop, removing the unpublisheduseCalendarRovingTabindexhook.- SegmentedControl now uses the shared useListFocus roving-tabindex primitive instead of its inline keyboard handler and tab-stop repair effect; no behavior change.
The component's ~60-line inline ArrowLeft/Right/Home/End handler and useIsomorphicLayoutEffect tab-stop repair are replaced by useListFocus({hasRovingTabIndex: true, wrap: true, orientation: 'horizontal'}), which owns the single roving tab stop, skips disabled radios, wraps, handles Home/End, and repairs the stop on mount/disable. Selection-follows-focus (APG radiogroup) is preserved via a container onFocus handler that selects the focused radio's value. - Move
@stylexjs/stylexfromdependenciesto a requiredpeerDependency(^0.18.3). A consumer who authors their own StyleX now shares a single runtime with astryx — resolution dedupes to their own install in both browser and Node — instead of silently getting a second copy on version drift. An incompatible StyleX version is now flagged at install (npm errors, pnpm/yarn warn) instead of resolving silently. Consumers who don't author StyleX are unaffected: the runtime is still required to render astryx components and is auto-installed by npm 7+ and pnpm. - New
TableContextActiontype and an optionalcontextMenuActionsfield onHeaderCellRenderProps/BodyCellRenderProps. Plugins append their actions inside the existingtransformHeaderCell/transformBodyCelltransforms; the table concatenates them across plugins (never overridden), the same waystylesare merged. - The cell components (
TableHeaderCell/TableCell) own the menu wrapper and render it around their own content via theContextMenuActionsprop, so the cell controls how the wrapper interacts with padding / content sizing — and row menus work without invalid<tr>nesting. Actions group with dividers and show a checkmark for the active item; when none are contributed, the native browser menu passes through. contextMenuActionsaccepts either an array or a getter (() => TableContextAction[]); the getter is resolved lazily when the menu opens, so plugins with state-derived actions don't build arrays on every render.useTableSortableuses a getter so its checked/clear state always reflects the latest sort.- First contributor:
useTableSortableadds "Sort ascending / Sort descending / Clear sort" on sortable headers. - TabList now uses
useListFocus's built-in roving-tabindex support (hasRovingTabIndex) instead of a hand-rolled tab-stop repair effect. The hook owns the single tab stop — stamping tabindex 0/-1, repairing it on mount and as stops mount/unmount or toggle disabled, and keeping it in sync after clicks/programmatic focus viahandleFocuson the nav. Individual Tabs still rendertabIndex={isSelected ? 0 : -1}as the initial source of truth, which the hook's repair preserves. No behavior change. - useTreeFocus gains
hasRovingTabIndex+handleFocusfor internal tab-stop management; TreeList drops its inlineactiveIdstate and lets the hook own the roving tab stop (#3488).
@astryxdesign/cli
New Features
- Add a hidden
astryx blogcommand that reads the blog over the site's RSS feed and prints a post's plaintext (.txt) variant. The command is not shown in--helpor the manifest and always reads from the canonical site origin. - Component discovery is now package-ownership aware: --package scoping, source resolution for integration components, and package-qualified JSON listings.
- Strict config + integration v1 schema (integrations, issuesUrl, hooks.postCodemod) and new @astryxdesign/cli/integration export.
- File-based codemod API (createCodemod/createConfigCodemod) with the @astryxdesign/cli/codemod export and integration codemod discovery in upgrade.
- component, template, and upgrade now print a one-line non-blocking warning when a configured integration has validation issues, pointing to validate-integration.
- Add a Kanban Board page template: color-coded status columns, draggable task cards with priority tags, and board toolbar.
- Add frame-first layout guidance: new
astryx docs layouttopic (shell choice, region budgets, app archetypes, cards-vs-rows policy, responsive contracts), layout rules in the generated agent cheat sheet, and layout anti-patterns indocs principles. - Add a v0.1.3 config codemod that migrates astryx.config layout.components to experimental.xle.components.
- Add v0.1.0 codemods for migrating
declare module "@xds/core/..."type augmentations and.xds-*/[data-xds-theme]/@layer xds-themeCSS surfaces to their@astryxdesign/astryx-*equivalents. - Introduce the Project configuration API as the single entry point for reading resolved project config, components, templates, codemods, and issue routing, replacing loadConfig. Misconfigured integrations are now skipped with a warning during upgrade instead of hard-failing, and a new --skip-codemod flag lets you re-run past a failed codemod.
- Add a Shell page-template category to the CLI: Top Nav, Side Nav, and Shell Nav app-shell scaffolds (#3245, #3246, #3247)
- Static template authoring API (createPageTemplate/createBlockTemplate) with the @astryxdesign/cli/template export and type-driven, package-scoped template discovery.
- Swizzle can now copy integration-owned components, rewrites escaping imports to the owning package, and routes maintainer feedback through config and integration issue URLs.
astryx theme build --watch: rebuild a theme automatically whenever the source file changes, until interrupted with Ctrl-C. Removes the manual re-run step (and the stale-CSS confusion that comes with forgetting it) from the theme-authoring loop. Each rebuild runs in a child process so a build error is contained and the watcher keeps running. Not supported with--json. (#3375)- Add the validate-integration command and integration issue model for checking an Astryx integration package's manifest and contributions.
- XLE app-component registration moved into validated config under experimental.xle.components (object form), replacing the unvalidated layout.components read.
Fixes
- Align the CLI error-code type declarations with the runtime error codes (add the missing ERR_AMBIGUOUS_TEMPLATE declaration).
- Correct the
doctortheme-wiring hint to reference the realastryx.themeconfig field (wasxds.theme) and update the agent-docs check wording to say "Astryx". - Update the API/CLI parity harness for the package-qualified
component --listshape, and make the component API reject a non-string name with a clean error instead of throwing. - The XDS-prefix drop codemod now runs as a mandatory v0.1.0 upgrade step, so upgrading from 0.0.x rewrites prefixed imports (useXDSTheme, XDSButton, XDSIconRegistry, ...) to their bare names alongside the @xds/_ → @astryxdesign/_ scope rename.
- upgrade now runs core codemods before loading config, so a config codemod can repair an otherwise-invalid config; dry-run reports a fixable config and suggests the command to apply it.
Documentation
- Blockquote: add "With Attribution" and "Testimonials" examples (#3385)
- DateTimeInput and DateRangeInput: add example blocks so their docs pages have populated Examples sections and playground links (#2724)
- Add copyable example blocks to 46 component docs pages that previously showed only a hero visual and an empty Examples section (#3481)
- HoverCard: give the "Link Preview" example an interactive
Linktrigger so there is something to hover over (#2728) - Lightbox: add Gallery, Video, and Zoom examples and fix the playground preview (#3301)
- Remove lingering references to the removed gap-report feature and swizzle gap flags; docs now reflect swizzle's maintainer feedback link.
- Tab: add an interactive example showing
iconandselectedIconon the Tab docs page (#2765) - ToggleButtonGroup: add a vertical example block showing orientation="vertical" with single- and multi-select groups (#2707)
Other Changes
- Integration codemod and template-doc loading now use the shared module-loader util instead of duplicating the jiti/import logic.
- Extract the shared module-loading + conventional-file-discovery helpers used by config and integration loading into one internal util (no behavior change).
- Remove the standalone gap-report command. Swizzle now prints a short maintainer feedback link instead of filing issues.
- Load and validate user-authored config, integration, codemod, and template modules through one shared module loader; create* factories are now type-only and validation happens at load.
- Remove the obsolete xds config-surface migration codemod and unify config codemod execution on the shared (file, api) runner used by integration codemods.
@astryxdesign/build
Fixes
- Use
pnpm buildin theprepackscript so publishing no longer fails thedevEnginespackage-manager check (#3564).
Contributors
Thanks to everyone who contributed to this release:
- @AKnassa
- @arham766
- @athz
- @cixzhang
- @durvesh1992
- @ejhammond
- @ernestt
- @harshavardhan194
- @humbertovirtudes
- @IFAKA
- @imdreamrunner
- @josephfarina
- @kentonquatman
- @mohitWeb-lab
- @pollychen-lab
- @thedjpetersen
Full Changelog: v0.1.2...v0.1.3