Astryx v0.7.0: better mobile experiences, clearer inputs, and stronger foundations
What shipped after v0.6.0: easier forms, stronger mobile behavior, composable Markdown, better data display, clearer CLI integrations, and guided migrations.
Astryx v0.7.0 brings together everything shipped from v0.6.1 through v0.6.8 and the last updates for this release. The release improves the experiences people touch most—forms, mobile layouts, menus, Markdown, and tables—while making theme and CLI behavior more predictable.
Forms and inputs
Styling, IDs, and handlers you pass now land on the control people use, and searchable pickers can create a missing choice.
diff<TextInputlabel="Project name"value={name}onChange={setName}- xstyle={styles.projectName}+ controlXstyle={styles.projectName}/><Typeaheadlabel="Assignee"searchSource={peopleSource}value={assignee}onChange={setAssignee}- xstyle={styles.assigneeField}- emptySearchResultsText="No people found"+ containerXstyle={styles.assigneeField}+ emptySearchText="No people found"/>
Create a missing choice while searching

With
hasSearch and hasCreate, a query that matches no option offers a Create row. onChange receives the new value with a {type: 'create', query} descriptor so your app can add the option.tsxconst [reviewerOptions, setReviewerOptions] = useState(initialReviewerOptions);const [reviewers, setReviewers] = useState(['platform']);<MultiSelectorlabel="Reviewers"options={reviewerOptions}value={reviewers}onChange={(next, change) => {if (change?.type === 'create') {setReviewerOptions(current => [...current,{value: change.query, label: change.query},]);}setReviewers(next);}}hasSearchhasCreatetriggerDisplay="badges"/>;
Menus and overlays
Menus hold real controls beside their rows and drill into submenus on touch; sheets, dialogs, and toasts stack, dismiss, and pad their content the way people expect.
Put controls beside menu rows

Header and footer controls stay outside the menu itself, so screen readers hear a normal menu and Tab reaches each region in order.
tsximport {PlusIcon} from '@heroicons/react/24/outline';<DropdownMenubutton={{label: 'Assign reviewer'}}header={<TextInputlabel="Filter reviewers"isLabelHiddenplaceholder="Filter reviewers"value={query}onChange={setQuery}/>}footer={<Buttonlabel="Create team"variant="secondary"icon={<Icon icon={PlusIcon} />}onClick={createTeam}/>}items={reviewerActions}/>;
Drill into submenus on a phone

Opened by touch, a sub-menu drills into the current rows, with a Back row named for the menu it returns to. With a mouse it still opens as a flyout; set
presentation on DropdownMenuSubMenu to force either mode.tsx<DropdownMenu button={{label: 'Actions'}}><DropdownMenuItem label="Assign to me" onClick={assign} /><DropdownMenuItem label="Snooze" onClick={snooze} /><DropdownMenuSubMenu label="Move to"><DropdownMenuItem label="Backlog" onClick={moveToBacklog} /><DropdownMenuItem label="Next release" onClick={moveToNext} /></DropdownMenuSubMenu></DropdownMenu>
Keep nested layers in order
- Layers and dialogs: Escape closes the top layer first for menus, popovers, and dialogs inside a sheet. Nested layers stay inside their nearest dialog.
- Toasts: They stay visible and clickable above open dialogs.
Give BottomSheet content room

BottomSheet now adds a comfortable content inset by default;
padding={0} keeps the old flush box.diff<BottomSheetisOpen={isOpen}onOpenChange={setIsOpen}label="Filters"+ padding={0}><CustomEdgeToEdgeFilters /></BottomSheet>
Mobile and touch
Phones get 16px body text, swipe actions on rows, and a choice of date and time pickers.
Reveal row actions with a swipe

Sideways drags reveal touch actions without hiding them from keyboard and assistive-technology users. Rows without actions do not load the gesture code, saving about 2.7 KB (gzip) on first load.
tsx<ListItemlabel={row.label}onClick={openRow}swipeActions={{trailing: [{label: 'Archive',onActivate: () => archive(row.id),variant: 'neutral',hasRemoval: true,},{label: 'Delete',onActivate: () => remove(row.id),variant: 'destructive',hasRemoval: true,},],}}/>
Choose the date and time picker
Date and time fields can explicitly keep Astryx's picker on touch, use a popover, or hand off to the operating system.
tsx<DateInputlabel="Release date"value={date}onChange={setDate}presentation="adaptive-bottom-sheet"/><TimeInputlabel="Release time"value={time}onChange={setTime}presentation="adaptive-bottom-sheet"/>
Read comfortably on a phone

On touch devices narrower than the
lg breakpoint, Neutral, Chocolate, Butter, and Stone floor their type base to 16px and re-derive the ratio so Display 1 keeps its desktop size. Desktop sizes are unchanged.To keep Neutral's previous 14px phone scale, append a later rule for the same condition:
tsximport {defineTheme} from '@astryxdesign/core/theme';import {neutralTheme} from '@astryxdesign/theme-neutral';const appTheme = defineTheme({name: 'app',extends: neutralTheme,adaptations: {rules: [{when: {pointer: 'coarse', width: {below: 'lg'}},value: {typography: {scale: {base: 14, ratio: 1.2}}},},],},});
If your theme extends one of these and sets its own type scale, the phone rule replaces it on touch screens; append your own rule for the same condition with the values you want. For Butter and Stone, use
ratio: 1.25 and also restore tokens: {'--text-supporting-size': '12px'} in the rule's value.Data display
Tables keep expanded rows aligned and make column resizing easy to discover.
Expand a row without disturbing the table

Expanded details align with the first data column, the chevron stays centered in its column, row striping stays stable, and buttons or links inside a row never toggle it.
tsxconst expansion = useTableRowExpansion<Order>({expandedKeys,onToggle: key => setExpandedKeys(previous => toggle(previous, key)),getRowKey: order => order.id,renderExpanded: order => <OrderItems order={order} />,hasRowClickExpansion: true,});<Tabledata={orders}columns={columns}idKey="id"isStripedhasHoverplugins={{expansion}}/>;
Resize columns without losing context

Column boundaries reveal on header hover, and resized widths stay in place when the drag ends.
tsxconst columnResize = useTableColumnResize<Order>({columnWidths,columns,onColumnResizeEnd: updates =>setColumnWidths(previous => ({...previous, ...updates})),});<Table data={orders} columns={columns} idKey="id" plugins={{columnResize}} />;
Markdown and content
Markdown can now turn your own syntax into real components instead of stopping at formatted text. The examples below use the same source before and after, so each feature's effect is visible on its own.
Keep authored line breaks

Single line endings become line breaks; code blocks and inline code stay unchanged.
tsximport {Markdown} from '@astryxdesign/core/Markdown';import {markdownSoftBreaksPlugin} from '@astryxdesign/core/Markdown/plugins';<Markdown plugins={[markdownSoftBreaksPlugin]}>{source}</Markdown>;
Read frontmatter as data

Your app gets the frontmatter as typed data while it stays out of the rendered document.
tsximport {Markdown} from '@astryxdesign/core/Markdown';import {createMarkdownFrontmatter} from '@astryxdesign/core/Markdown/plugins';const frontmatter = createMarkdownFrontmatter({name: 'release-notes',parse: fields => ({title: typeof fields.title === 'string' ? fields.title : 'Untitled',status: typeof fields.status === 'string' ? fields.status : 'draft',}),});const result = frontmatter.parse(source);<Markdown plugins={[frontmatter.plugin]}>{source}</Markdown>;
Turn product syntax into links

Text transforms can link product identifiers in prose without touching code spans, existing links, images, or math.
tsximport {Markdown} from '@astryxdesign/core/Markdown';import {createMarkdownPlugin,createMarkdownTextTransform,} from '@astryxdesign/core/Markdown/plugins';const specLinks = createMarkdownPlugin({name: 'spec-links',apiVersion: 1,transform: createMarkdownTextTransform({pattern: /\bAST-(\d{3})\b/g,requiredSubstrings: ['AST-'],replace: match => ({type: 'link',url: `/specs/${match[0]}`,children: [{type: 'text', value: match[0]}],}),}),});<Markdown plugins={[specLinks]}>{source}</Markdown>;
Turn custom fences into components

A declared fence language can render as a product component while every other fence keeps its normal code treatment.
tsximport {Markdown} from '@astryxdesign/core/Markdown';import {Banner} from '@astryxdesign/core/Banner';import {createMarkdownFenceTransform,createMarkdownPlugin,type MarkdownExtensionNode,} from '@astryxdesign/core/Markdown/plugins';type CalloutNode = MarkdownExtensionNode<'callouts','callout',{readonly title: string; readonly body: string},'block'>;const callouts = createMarkdownPlugin<'callouts', CalloutNode>({name: 'callouts',apiVersion: 1,transform: createMarkdownFenceTransform({languages: ['callout'],createNode: ({code}) => {const [title, ...body] = code.split('\n');return {type: 'extension',plugin: 'callouts',name: 'callout',display: 'block',data: {title, body: body.join(' ')},};},}),renderers: {callout: {render: ({node}) => (<Bannerstatus="warning"title={node.data.title}description={node.data.body}/>),toText: node => `${node.data.title}\n${node.data.body}`,},},});<Markdown plugins={[callouts]}>{source}</Markdown>;
Copy links to headings

Heading links reveal on hover or keyboard focus, copy without navigating, and briefly confirm success.
tsximport {Markdown} from '@astryxdesign/core/Markdown';import {createMarkdownHeadingLinks} from '@astryxdesign/core/Markdown/plugins';const headingLinks = createMarkdownHeadingLinks({permalinkBaseUrl: 'https://astryx.atmeta.com/blog/astryx-v0-7-0',});<Markdown plugins={[headingLinks]}>{source}</Markdown>;
Connect rendered blocks to source lines

Illustrative gutter: the page reads each block's
data-source-line and data-source-line-end attributes to attach comments, selections, and search results.tsximport {Markdown} from '@astryxdesign/core/Markdown';import {markdownSourceLinesPlugin} from '@astryxdesign/core/Markdown/plugins';<Markdown plugins={[markdownSourceLinesPlugin]}>{source}</Markdown>;
Render math with your product's renderer

Providing a math renderer turns on dollar-delimited parsing; the typesetting shown is an example renderer, and your app owns the final accessible output.
tsximport {Markdown} from '@astryxdesign/core/Markdown';function MathExpression({value,display,}: {value: string;display: 'inline' | 'block';}) {return <ProductMath source={value} display={display} />;}<Markdown components={{math: MathExpression}}>{source}</Markdown>;
Themes and visual foundations
Components read more clearly in every stable theme, and data colors have one StyleX source.
Use data colors from one StyleX source
Apps can read the same data-color values that Astryx charts and themes use.
tsximport * as stylex from '@stylexjs/stylex';import {dataVars} from '@astryxdesign/core/theme/dataTokens.stylex';const styles = stylex.create({series: {backgroundColor: dataVars['--color-data-categorical-blue'],},});
The three comparisons below use the same scene in v0.6.0 and the current release.
Read keyboard shortcuts clearly

Keycap text now meets WCAG AA across every stable theme, and common key aliases render consistently.
tsx<Kbd keys="mod+k" /><Kbd keys="mod+shift+s" /><Kbd keys="esc" />
Keep Slider marks with the fill

Marks at or behind the thumb use the fill color, while unfilled marks use the track color across themes.
tsx<Sliderlabel="Quality"value={quality}onChange={setQuality}min={0}max={100}step={5}marks={[{value: 0, label: '0'},{value: 25},{value: 50, label: '50'},{value: 75},{value: 100, label: '100'},]}/>
Size icons with their Button

An Icon without an explicit size now follows its Button, so small buttons no longer look crowded.
tsx<Buttonlabel="Upload"variant="primary"icon={<Icon icon={ArrowUpTrayIcon} />}/><Button label="Upload" size="lg" icon={<Icon icon={ArrowUpTrayIcon} />} />
Accessibility and localization
Layouts grow with the reader's text size, focus rings follow the input people actually used, and banners announce the async outcome instead of guessing from their color.
Let layouts follow the reader's text size
Readers who raise their browser font size now get padding and gaps that grow with the text, instead of larger words squeezed into the same fixed spacing.

Spacing tokens use rem (
--spacing-N = N × 0.25rem), including Matcha and Y2K's own steps; rendered pixel sizes are unchanged at the default 16px root.Keep focus cues matched to the last input

The pointer icon is added to show mouse input. Dialog's initial focus and the focus it returns on close follow the last input: the indicator shows after keyboard use and stays hidden after a mouse click or tap.
Announce an async outcome
Previously, Banner's visual
status automatically chose a live-region role, so the banner's appearance—not the async action—decided when screen readers heard it. Banners are now quiet by default; announce once from the action that finished, or set a role when the Banner itself is the persistent message.tsximport {Banner} from '@astryxdesign/core/Banner';import {useAnnounce} from '@astryxdesign/core/hooks';const announce = useAnnounce();async function saveSettings() {await save();announce('Settings saved');}<Banner status="success" title="Settings saved" />;
CLI and integrations
Other libraries can now contribute components, docs, templates, upgrade scripts, coding guidance, and source themes through the same Astryx CLI an app already uses. The CLI can add and verify those components.
Add and verify an integration component
![A terminal. The command npx @astryxdesign/cli integration add component AcmeChart is typed and runs, reporting component contribution added, [ok] AcmeChart, and the four files it created. Then npx @astryxdesign/cli integration verify runs and reports Integration package ready, [ok] @acme/charts@1.2.0, and 5 packed files with 3 of 3 required files present.](/blog/astryx-v0-7-0/cli-integration.webp)
Real output from the shipped CLI in a package whose
package.json already declares an exports map; the terminal frame is an illustration styled with Astryx tokens.bashnpx @astryxdesign/cli integration add component AcmeChartnpx @astryxdesign/cli integration verify
Upgrading to 0.7.0
Start here:
- Pin Core and the CLI to 0.7.0, plus every other stable Astryx package your project uses.bashOLD_VERSION=0.6.8 # replace with the Core version your project uses todaynpm i @astryxdesign/core@0.7.0 @astryxdesign/cli@0.7.0 @astryxdesign/build@0.7.0 @astryxdesign/theme-neutral@0.7.0The example includes Neutral; update every other stable Astryx theme in your project in the same install step.
- Run the 0.7.0 upgrade. It rewrites supported callsites, validates data-color ownership, and leaves a TODO where it cannot choose safely.bashnpx @astryxdesign/cli upgrade --from "$OLD_VERSION" --apply --path .
- Check the items below. Four need a manual code edit, ten affect CLI commands or scripts, four are visual or token changes to inspect in your app, and eight are handled or checked by the upgrade.
Biggest risk: props that used to be ignored now reach the DOM. Layout, MetadataList, NavHeadingMenuItem, and TopNavMegaMenuItem already accepted BaseProps, so there is no type error or codemod. Audit callsites that pass IDs, ARIA or data attributes, event handlers, or style props only if they relied on those props doing nothing.
Manual code edits — no codemod (4)
- Banner announcements are no longer inferred from status → call
useAnnounce()from the async transition, or add an explicit persistent role. - Previously ignored BaseProps now apply → audit the four components in the risk callout only where a passed prop was meant to stay inert.
- Complete icon registries require
upload→ add anuploadentry and artwork to every custom completeIconRegistry. - Standalone empty-operator filters no longer save on open → save at the callsite when opening should commit the filter, or skip opening it.
CLI commands and scripts — no codemod (10)
theme addnow imports built themes → change source-copying scripts totheme eject; existingtheme add --importcalls need no change.diff- npx @astryxdesign/cli theme add neutral ./src/theme --overwrite+ npx @astryxdesign/cli theme eject neutral ./src/theme --overwriteinit --remove-agentsreportsremoved: false→ treatdata.removedas a boolean, not literaltrue.- Conflicting CLI options are refused → remove the formerly ignored option from affected command pairs.
- Invalid CLI input now fails → pass valid types, packages, limits, and arguments, and handle
ERR_AMBIGUOUS_COMPONENT. - Empty positional arguments are rejected → omit
""when the intended operation is to list entries. - Integration validation needs a package → run it where
package.jsonexists, or handle exit 1. - Astryx-prefixed agent variables are removed → use
AGENTandAGENT_SESSION_ID; agent metadata has no replacement. - The
layoutcommand group is removed → useastryx build,astryx template, orastryx docs layout. - Private theme variables fail the build → fix every reported private
--_*input instead of relying on exit 0. - Doctor fails on unloadable configured projects → install or repair the named integration, remove it from config, or fix the reported field.
Visual and token changes — inspect your app; usually no code (4)
reset.cssnow reaches pseudo-elements and search fields → do nothing unless you need the old browser result; then add a later-layer override.- Phone typography starts at 16px in four themes → take the new Neutral, Chocolate, Butter, and Stone scale, or add a touch-and-narrow override to keep 14px.
- Core spacing uses rem → CSS variable users do nothing; code that parses px must convert with the root size or read the computed length.
- Matcha and Y2K spacing uses rem → take the pixel-equal default at a 16px root, or override spacing tokens in px for fixed sizing.
Handled or checked by astryx upgrade (8)
- Data-color defaults move to StyleX → fix anything
validate-data-token-ownershipreports, then rebuild committed theme CSS. - Date and time input styling targets the control → review the
migrate-field-input-stylingrewrite or TODO. - Typeahead, Tokenizer, CheckboxList, RadioList, and Slider styling targets the field → review the generated
container*orcontrol*prop. - Selector styling targets the control → review the rewritten control styling and forwarded trigger props.
- Text input styling targets the control → review the rewritten control styling and native input ID.
- Theme styling uses current names and data attributes → review rewritten keys and selectors, especially any TODO for custom CSS.
- BottomSheet adds default content padding → review the generated
padding={0}or TODO; remove the prop when your theme already supplies padding. emptySearchResultsTextis removed → review the rename toemptySearchTextand any TODO on spreads or trigger objects.
All 26 breaking migrations
theme addnow imports built themes (#7138) — copy scripts move fromtheme add <slug> <path>totheme eject <slug> <path>; no codemod.- Banner announcements are explicit (#6630) — move from status-derived live regions to
useAnnounce()in the async action or an explicit persistent role; no codemod. - Previously ignored DOM props now work (#7331) — remove props on Layout, MetadataList, NavHeadingMenuItem, or TopNavMegaMenuItem only if your code depended on them doing nothing; no codemod.
- Complete icon registries include Upload (#7329) — add an
uploadentry to every customIconRegistry; bespoke artwork has no codemod. init --remove-agentsreportsremoved: false(#6623) — readdata.removedas a boolean instead of narrowing it to literaltrue; no codemod.- Conflicting CLI options are refused (#7132) — remove ignored input from
gap-report --list-categories,init --remove-agents,upgrade --list, andtemplate --cdn,--list, or--skeleton; no codemod. - Invalid CLI input no longer succeeds silently (#7149) — pass valid types, packages, limits, and arguments, and handle
ERR_AMBIGUOUS_COMPONENT; no codemod. - Empty positional arguments are rejected (#6620) — omit
""when the intended operation is to list entries; no codemod. - Integration validation needs a package (#7150) — run
doctor integration validatewherepackage.jsonexists or handle exit 1; no codemod. - Data-color defaults move to StyleX (#6834) — import
dataVarsfor CSS and rebuild committed theme CSS;validate-data-token-ownershipreports unsafe variables and stale output without rewriting them. - Date and time input styling targets the control (#7337) — root styling becomes
controlXstyle/controlClassName/controlStyle;migrate-field-input-stylingrewrites direct Astryx JSX. - Typeahead, Tokenizer, CheckboxList, RadioList, and Slider styling targets the field (#7339) — root styling becomes
container*, except Typeahead inside InputGroup usescontrol*;migrate-field-input-stylinghandles direct JSX. - Selector styling targets the control (#7341) — root styling becomes
control*, while DOM props and refs reach the trigger;migrate-field-input-stylinghandles direct JSX. - Text input styling targets the control (#7336) — root styling becomes
control*, and IDs you pass reach the native input;migrate-field-input-stylinghandles direct JSX. - Standalone filter editors do not save on open (#7347) — save at the callsite that opens an already-complete empty-operator filter, or skip opening it; no codemod.
- Astryx-prefixed agent variables are removed (#7127) — replace
ASTRYX_AGENT_IDandASTRYX_AGENT_SESSION_IDwithAGENTandAGENT_SESSION_ID; metadata has no replacement and no codemod. - Theme styling uses current names and data attributes (#7302) — replace 21 old styling names and bare state classes;
migrate-deprecated-theme-surfacerewrites known keys and selectors and marks ambiguous cases. - The
layoutcommand group is removed (#7137) — useastryx build,astryx template, andastryx docs layout; no codemod. reset.cssreaches pseudo-elements and search fields (#7355) — add your own later-layerbox-sizing, border, placeholder color, or search appearance only when you need the old browser result; no codemod.- Private theme variables fail the build (#6872) — fix every reported private
--_*input instead of relying on exit 0; no codemod. - BottomSheet adds default content padding (#6884) — remove duplicate caller padding or pass
padding={0}for the old box;preserve-bottom-sheet-content-paddingskips a sole Section or Layout, marks nested Layouts with a TODO, and otherwise addspadding={0}. Remove that added prop when your theme already sets BottomSheet padding so the theme inset remains. - Phone typography starts at 16px (#6699) — take the new Neutral, Chocolate, Butter, and Stone scale as-is, or add a later touch-and-narrow rule to keep 14px or your own scale; no codemod.
emptySearchResultsTextis removed (#7289) — rename it toemptySearchText;rename-empty-search-results-texthandles supported components and marks ambiguous spreads or trigger objects with a TODO.- Core spacing follows the root font size (#7359) — CSS variable users need no change; code that parses px converts rem with the root size or reads the computed length; no codemod.
- Matcha and Y2K spacing follows the root font size (#7374) — take the new default, which is pixel-equal at a 16px root, or override the spacing tokens in px to keep fixed sizing; no codemod.
- Doctor fails on unloadable configured projects (#7130) — install or repair the named integration, remove it from config, or fix the config field in the error; no codemod.
Thank you
Thanks to the contributors behind v0.6.1 through v0.7.0: @AKnassa, @aldentan, @andrskr, @athz, @bhamodi, @cixzhang, @Cypher-Aura-19, @ejhammond, @ernestt, @freddymeta, @fullstackhacker, @Geervan, @Han5991, @harjothkhara, @HelloOjasMutreja, @humbertovirtudes, @imdreamrunner, @jiunshinn, @josephfarina, @kentonquatman, @korkt-kim, @ksying, @kyu-rong, @Kyujenius, @Lee-Dongwook, @light-merlin-dark, @ManoharPaturi, @markselby9, @nynexman4464, @oliprovscode, @rubyycheung, @rupesh-kumar-sah, @thedjpetersen, and @vjeux.
Read the complete package-by-package notes on the Astryx v0.7.0 release page.
ReleaseThemingComponentsAccessibility
