Migration Guide

How to migrate an existing Tailwind, shadcn, or Radix application to the design system incrementally.

Overview

Treat migration as a product-shell and workflow migration, not a global class replacement. Start by putting the app inside Theme and AppShell, then move one route or surface at a time to design system primitives while keeping existing data, routing, and business logic intact.Tailwind can coexist during migration. Use it for legacy wrappers and local layout while replacing interactive controls, navigation, command surfaces, forms, alerts, dialogs, and settings UI with components.

Recommended Order

  1. Install the design system and run init so the project has package scripts, theme CSS, and agent docs.
  2. Wrap the app root with Theme and choose the initial light, dark, or system mode behavior.
  3. Make Tailwind and design system CSS layer order explicit before replacing components.
  4. Move the persistent frame first: AppShell, TopNav, SideNav, page content, and mobile navigation.
  5. Replace shared primitives: Button, IconButton, TextInput, NumberInput, Switch, CheckboxInput, RadioList, Selector, Tabs, Dialog, AlertDialog, Banner, Toast, Badge, Card, Table, and ListItem.
  6. Replace global workflows: command palette, settings popover, theme toggle, search, filters, create flows, and destructive confirmation dialogs.
  7. Remove legacy Tailwind classes from each completed surface, keeping only token-backed layout utilities or local wrappers that still need to be migrated.
  8. Verify both light and dark modes, keyboard navigation, responsive layout, and empty/error/loading states before moving to the next route.

CLI Workflow

Use the CLI as the migration checklist. Read the docs for the pattern you are about to touch, inspect a matching template skeleton, then read the exact component docs before editing.
Migration-oriented CLI pass
bash
npx astryx docs migration
npx astryx docs theme
npx astryx docs styling
npx astryx template --list --type block
npx astryx template AppShellTopNavWithSideNav --skeleton
npx astryx template PopoverSettingsPanel --skeleton
npx astryx component AppShell
npx astryx component SideNav
npx astryx component TopNav
npx astryx component CommandPalette
npx astryx component Button
npx astryx component TextInput
Use --dense when pasting output into an AI coding tool, and use --json when building automated migration reports.
Dense and JSON modes
bash
npx astryx docs migration --dense
npx astryx component Button --json

Theme and CSS Setup

Mount Theme at the app root so every migrated component reads the same token set. Keep the mode in application state if users can switch between light and dark themes.
Root provider with explicit mode
tsx
import {Theme} from '@astryxdesign/core/theme';
import {defaultTheme} from '@astryxdesign/theme-default/built';
import {useState} from 'react';
import '@astryxdesign/theme-default/theme.css';
export function AppRoot({children}: {children: React.ReactNode}) {
const [mode, setMode] = useState<'system' | 'light' | 'dark'>('system');
return (
<Theme theme={defaultTheme} mode={mode}>
<SettingsContext.Provider value={{mode, setMode}}>
{children}
</SettingsContext.Provider>
</Theme>
);
}
When Tailwind remains in the app, declare layer order once in the global CSS file. design system reset and theme CSS should load before Tailwind utilities so migrated components keep design system defaults while legacy utility classes still work.
Tailwind v4 coexistence
css
@layer reset, theme, base, astryx-base, astryx-theme, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);
@import "@astryxdesign/core/reset.css";
@import "@astryxdesign/core/astryx.css";
@import "@astryxdesign/theme-default/theme.css";
@import "@astryxdesign/core/tailwind-theme.css";
@import "tailwindcss/utilities.css" layer(utilities);

Move the App Frame First

Start with AppShell so page migration happens inside the final navigation, spacing, surface, and responsive frame. This also exposes theme and color issues early because every route shares the same shell.
Legacy surfaceComponentNotes
HeaderTopNavUse for product identity, global actions, account entry, and command/search trigger.
SidebarSideNavUse sections and nested nav items for route groups. Keep selection state driven by the router.
Main page wrapperAppShell + LayoutLet the shell own persistent structure; let route components own page content.
Mobile drawer navMobileNav or AppShell mobile behaviorVerify focus, close behavior, and route changes on narrow viewports.
Settings menuPopover + Layout + SwitchUse as the home for theme mode and app preferences.

Map shadcn and Radix Primitives

Do not wrap old shadcn components in design system styles. Replace the primitive with the component that owns the behavior, accessibility, state classes, and token usage.
Existing primitiveComponentMigration note
button / shadcn ButtonButton or IconButtonUse Button for labeled commands and IconButton for icon-only toolbar actions.
inputTextInputKeep validation state in status props rather than ad hoc border classes.
textareaTextAreaUse when multiline editing is the primary action.
switchSwitchUse for persisted boolean settings, including theme mode when represented as a binary choice.
checkboxCheckboxInput or CheckboxListUse list variants for grouped selection.
radio groupRadioListUse when one option must be selected from a visible set.
select / comboboxSelector or TypeaheadUse Selector for bounded options and Typeahead for searchable async options.
tabs used as page navTabListUse route state or current page state as the source of truth.
command dialogCommandPaletteKeep app-specific search sources outside the shell and feed searchable items.
dropdown action menuDropdownMenu or MoreMenuUse MoreMenu for compact overflow actions.
alert / calloutBanner or ToastUse Banner for page or section messages and Toast for transient feedback.
dialogDialog or AlertDialogUse AlertDialog for destructive confirmation and Dialog for task flows.
card-like list rowListItemPrefer ListItem for selectable rows instead of styling Button as a row.

Command Palette, Settings, and Theme

Move global search to CommandPalette once the shell exists. Treat the palette as a view over app commands: routes, contextual actions, create actions, filters, recent items, and entity results. Keep data normalization in app code so search sources always return arrays of searchable items.Put light and dark mode controls in the settings popover or account menu. The switch or selector should update the mode passed to Theme, not toggle isolated body classes.
Settings popover theme control
tsx
function ThemeModeSwitch() {
const {mode, setMode} = useSettings();
const isDark = mode === 'dark';
return (
<Switch
label="Dark mode"
description="Use the dark color theme"
value={isDark}
onChange={next => setMode(next ? 'dark' : 'light')}
/>
);
}

Verification Checklist

  • Run the app in light and dark mode and check that surfaces, borders, text, icons, hover states, focus rings, and status colors flow together.
  • Open the command palette from the shell, type into it, select items by keyboard, and confirm focus returns to the trigger.
  • Check the SideNav at collapsed, expanded, active, hover, nested, and mobile states.
  • Verify settings popovers and dialogs in jsdom and in a real browser because native dialog and Popover APIs may need test shims.
  • Search for leftover hardcoded Tailwind colors, arbitrary hex values, and one-off hover colors after each route migration.
  • Run component tests, build, and at least one browser screenshot pass for each migrated route.

AI Migration Prompt

When using an AI coding agent, give it an explicit migration loop instead of asking for a full-app rewrite.
Paste this into your AI
text
We are migrating this existing Tailwind/shadcn app to XDS incrementally.
First run:
- npx astryx docs migration --dense
- npx astryx docs theme --dense
- npx astryx docs styling --dense
- npx astryx template AppShellTopNavWithSideNav --skeleton
Then migrate one route or shell surface at a time. Keep business logic and routing intact. Replace shadcn/Radix/Tailwind primitives with XDS components, remove hardcoded colors, verify light and dark mode, and take screenshots before moving to the next surface.