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 XDSTheme 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 XDSTheme 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
npm run xds -- docs migration
npm run xds -- docs theme
npm run xds -- docs styling
npm run xds -- template --list --type block
npm run xds -- template AppShellTopNavWithSideNav --skeleton
npm run xds -- template PopoverSettingsPanel --skeleton
npm run xds -- component AppShell
npm run xds -- component SideNav
npm run xds -- component TopNav
npm run xds -- component CommandPalette
npm run xds -- component Button
npm run xds -- 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
npm run xds -- docs migration --dense
npm run xds -- component Button --json

Theme and CSS Setup

Mount XDSTheme 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 {XDSTheme} from '@xds/core/theme';
import {defaultTheme} from '@xds/theme-default/built';
import {useState} from 'react';
import '@xds/theme-default/theme.css';
export function AppRoot({children}: {children: React.ReactNode}) {
const [mode, setMode] = useState<'system' | 'light' | 'dark'>('system');
return (
<XDSTheme theme={defaultTheme} mode={mode}>
<SettingsContext.Provider value={{mode, setMode}}>
{children}
</SettingsContext.Provider>
</XDSTheme>
);
}
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, xds-base, xds-theme, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);
@import "@xds/core/reset.css";
@import "@xds/core/xds.css";
@import "@xds/theme-default/theme.css";
@import "@xds/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
HeaderXDSTopNavUse for product identity, global actions, account entry, and command/search trigger.
SidebarXDSSideNavUse sections and nested nav items for route groups. Keep selection state driven by the router.
Main page wrapperXDSAppShell + XDSLayoutLet the shell own persistent structure; let route components own page content.
Mobile drawer navXDSMobileNav or AppShell mobile behaviorVerify focus, close behavior, and route changes on narrow viewports.
Settings menuXDSPopover + XDSLayout + XDSSwitchUse 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 ButtonXDSButton or XDSIconButtonUse Button for labeled commands and IconButton for icon-only toolbar actions.
inputXDSTextInputKeep validation state in status props rather than ad hoc border classes.
textareaXDSTextAreaUse when multiline editing is the primary action.
switchXDSSwitchUse for persisted boolean settings, including theme mode when represented as a binary choice.
checkboxXDSCheckboxInput or XDSCheckboxListUse list variants for grouped selection.
radio groupXDSRadioListUse when one option must be selected from a visible set.
select / comboboxXDSSelector or XDSTypeaheadUse Selector for bounded options and Typeahead for searchable async options.
tabs used as page navXDSTabListUse route state or current page state as the source of truth.
command dialogXDSCommandPaletteKeep app-specific search sources outside the shell and feed searchable items.
dropdown action menuXDSDropdownMenu or XDSMoreMenuUse MoreMenu for compact overflow actions.
alert / calloutXDSBanner or XDSToastUse Banner for page or section messages and Toast for transient feedback.
dialogXDSDialog or XDSAlertDialogUse AlertDialog for destructive confirmation and Dialog for task flows.
card-like list rowXDSListItemPrefer ListItem for selectable rows instead of styling Button as a row.

Command Palette, Settings, and Theme

Move global search to XDSCommandPalette 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 XDSTheme, not toggle isolated body classes.
Settings popover theme control
tsx
function ThemeModeSwitch() {
const {mode, setMode} = useSettings();
const isDark = mode === 'dark';
return (
<XDSSwitch
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:
- npm run xds -- docs migration --dense
- npm run xds -- docs theme --dense
- npm run xds -- docs styling --dense
- npm run xds -- 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.