Getting started
Migrate from React
Move a React app (MUI, shadcn/Radix or Chakra) to Svelte 5 and CodeAtlas UI. Decision flow, hook-to-rune map, component maps, layout, theme and data table recipes, and how to report gaps.
This page is written for a coding agent and for the person reviewing its work. Every component and prop named here exists in the generated manifest (get_component, get_export), and a test fails when one stops existing. If you cannot find an equivalent for something in your app, that is a gap: write a documented workaround and report it, do not invent a component.
Decide first: incremental or rewrite
| Situation | Choose | Why |
|---|---|---|
| Under about 15 routes, few shared stores, no heavy React-only dependency | Rewrite into a fresh SvelteKit app | One framework at the end, no bridge code to delete later. |
| Large app that ships weekly | Incremental, route by route | Put the new Svelte app behind the same domain (reverse proxy per path prefix), migrate a route at a time, share only the session cookie and the API. |
| Design system is the main goal | Rewrite the shell and the list/form pages first | AppList, AppForm, AppSettings and PageShell replace most hand-built page wrappers in one step. |
Never mount React inside Svelte (or the reverse) to save time: two reconcilers on one page means two focus models and two sets of effects. A route is either React or Svelte.
Port in this order, whatever you picked: theme and shell, then one list page, one form page, one settings page, then the rest. Keep the React app’s API calls as they are; only the UI layer moves.
Project setup
-
Create the app.
bunx sv create my-appand choose SvelteKit with TypeScript. Svelte 5 runes mode is the default. -
Install and wire the package.
bun add @codeatlasdev/ui @lucide/svelte bunx @codeatlasdev/ui init --writeinitwrites the theme file, the Vite plugins, the server hook, theglobal.cssimport and a mobile-ready viewport meta. Details: Installation. -
Check the setup.
bunx @codeatlasdev/ui doctorlists missing peers and wiring with a one-line fix each, see Installation. -
Install optional peers only for what you port.
@tanstack/svelte-tableforDataTable,echartsfor charts,shikifor code blocks. -
Give your agent the manifest.
claude mcp add codeatlas-ui -- npx -y -p @codeatlasdev/ui ca-ui-mcp, see AI agents. Tell it to callget_componentbefore it writes any prop.
React to Svelte 5 runes
| React | Svelte 5 | Notes |
|---|---|---|
useState(0) |
let n = $state(0) |
Assign to change it: n += 1. Objects and arrays are deep reactive, mutate in place (list.push(x)). |
useMemo(() => a * 2, [a]) |
let b = $derived(a * 2) |
No dependency array. Use $derived.by(() => { … }) for several statements. |
useEffect(fn, [dep]) |
$effect(() => { … }) |
Dependencies are whatever the effect reads. Runs in the browser only, after the DOM updates. |
useEffect(() => { fetch… }, []) |
onMount or a SvelteKit load |
Fetch in load (server and client) instead of in an effect. |
useCallback(fn, [dep]) |
a plain function | Functions are not recreated per render, there is nothing to memoize. |
useRef(null) + ref={r} |
let el = $state<HTMLElement>() + bind:this={el} |
Read el inside $effect or an event handler. |
props and children |
let { title, children } = $props() and {@render children?.()} |
children is a snippet. |
| render props, slots | named snippets: {#snippet actions()}…{/snippet} |
The component renders it with {@render actions?.()}. Components of this library use snippets for every hole, see get_component. |
createContext / useContext |
setContext(key, value) / getContext(key) from svelte |
Call both during component init. For shared app state prefer a .svelte.ts module exporting $state. |
controlled value + onChange |
bind:value |
Two-way binding replaces the pair. Use onchange only for side effects. |
<form onSubmit> + e.preventDefault() |
<form method="POST" use:enhance> |
SvelteKit form actions validate on the server and work without JavaScript. |
Effect pitfalls
- Do not use
$effectto compute a value.useEffectplussetStatebecomes$derived. An effect that writes state it also reads can loop. - Reads after
awaitare not tracked. Read every reactive value before the firstawaitinside an effect. - Return a cleanup function for timers, listeners and observers, exactly like
useEffect. It runs before the next run and on destroy. - There is no effect on the server. Code that touches
window,localStorageormatchMediabelongs inonMountor$effect, or behindbrowserfrom$app/environment. - Reading
$stateoutside a component (a plain.tsfile) does not track. Put shared reactive state in a.svelte.tsfile.
<script lang="ts">
import { Button, TextField } from "@codeatlasdev/ui";
let name = $state("");
let saving = $state(false);
let valid = $derived(name.trim().length > 1);
async function save() {
saving = true;
await fetch("/api/users", { method: "POST", body: JSON.stringify({ name }) });
saving = false;
}
</script>
<TextField label="Name" bind:value={name} />
<Button variant="filled" loading={saving} disabled={!valid} onclick={save}>Save</Button>
Component map
Targets are real exports of @codeatlasdev/ui unless the import path says otherwise. Props that differ from what you expect are listed; for the rest, call get_component.
MUI
| MUI | CodeAtlas UI | Differences |
|---|---|---|
Button (contained, outlined, text) |
Button |
variant is filled, outlined, ghost, glass or destructive. loading, fullWidth, href, snippets leading and trailing replace startIcon and endIcon. |
IconButton |
IconButton |
aria-label is required, the child is the icon. |
TextField |
TextField |
label, error (a string), helperText, bind:value. Multiline: TextArea. Number: NumberField. |
Select, MenuItem |
Select |
Options go in the options array, not as children. For search use Combobox. |
Autocomplete |
Combobox |
options, bind:value, emptyText. |
Checkbox + FormControlLabel |
Checkbox |
label is built in, bind:checked. |
Switch |
Switch |
label, bind:checked. |
RadioGroup, Radio |
RadioGroup |
items array and bind:value. |
Slider |
Slider |
bind:value, or bind:values for a range. |
ToggleButtonGroup |
ToggleGroup |
Or SegmentedControl for a single choice. |
DatePicker, DateRangePicker |
DatePicker, DateRangePicker |
Values are @internationalized/date values, not Date. Also DateField, TimeField, Calendar, RangeCalendar. |
Dialog |
Dialog |
bind:open, title, description, snippets children and actions. Confirmations: AlertDialog. |
Drawer |
Drawer |
side is left, right or bottom. On a phone, a bottom Sheet is the native pattern. |
Menu, MenuItem |
DropdownMenu, DropdownMenuItem |
Also DropdownMenuLabel and DropdownMenuSeparator. |
Popover |
Popover |
Snippets trigger and children. |
Tooltip |
Tooltip |
content is a string. |
Tabs, Tab |
Tabs, TabsList, TabsTrigger, TabsContent |
bind:value on Tabs. |
Accordion |
Accordion, AccordionItem |
type is single or multiple. |
Alert |
Alert |
severity is success, warning, error or info. |
Snackbar |
Toaster plus toast |
Mount Toaster once; call toast.success(msg), toast.error(msg), toast.show(msg, { variant }). |
Chip |
Chip |
deletable and ondelete. Status labels: StatusChip. |
Badge |
Badge |
count, dot, color. |
Avatar |
Avatar |
src, alt, fallback. |
Card |
Card |
title, headerActions, flush. |
Paper |
Surface |
elevation, material, tone. |
Skeleton |
Skeleton |
variant, rows. |
CircularProgress |
Spinner |
label for assistive tech. |
LinearProgress |
Progress |
value, indeterminate. |
Divider |
Divider |
orientation. |
Breadcrumbs |
Breadcrumb |
items array. |
Pagination |
Pagination |
count, perPage, bind:page. |
Stepper |
Stepper |
steps array, bind:current. |
Box, Stack, Grid, Container |
Box, Stack, Grid, Container |
gap, columns and the other layout props are numbers on the token scale. No sx. |
Table family |
SimpleTable |
Native <thead>/<tbody> markup inside. Sorting, search and paging: Data tables. |
shadcn/ui and Radix
| shadcn / Radix | CodeAtlas UI | Differences |
|---|---|---|
Button |
Button |
Same variant names for outline → outlined, ghost, destructive; default → filled. |
Input, Label |
TextField |
The label, hint (description) and error are props, not siblings. |
Textarea |
TextArea |
rows, resize. |
Select |
Select |
options array. |
Command |
CommandPalette, Combobox |
Palette is for app navigation (modules); a searchable field is Combobox. |
Checkbox, Switch, RadioGroup, Slider |
Checkbox, Switch, RadioGroup, Slider |
All bind:. |
ToggleGroup |
ToggleGroup |
items array. |
Dialog |
Dialog |
Controlled with bind:open. There is no DialogTrigger: put a Button that sets open = true. |
AlertDialog |
AlertDialog |
onconfirm, oncancel, destructive. |
Sheet |
Drawer or Sheet |
Drawer slides from a side; Sheet is the mobile bottom sheet. |
Popover |
Popover |
|
HoverCard |
HoverCard |
openDelay, closeDelay; touch never opens it. |
DropdownMenu |
DropdownMenu, DropdownMenuItem |
|
Tooltip |
Tooltip |
|
Tabs |
Tabs, TabsList, TabsTrigger, TabsContent |
|
Accordion, Collapsible |
Accordion, Collapsible |
|
Alert |
Alert |
|
Sonner / Toast |
Toaster plus toast |
|
Badge |
Badge, Chip |
|
Avatar |
Avatar |
|
Card |
Card |
Header, content and footer are title, children and headerActions. |
Skeleton |
Skeleton |
|
Progress |
Progress |
|
Separator |
Divider |
|
Breadcrumb, Pagination |
Breadcrumb, Pagination |
|
ScrollArea |
ScrollArea |
|
Calendar, DatePicker |
Calendar, DatePicker |
|
Table |
SimpleTable |
|
DataTable (TanStack recipe) |
DataTable from @codeatlasdev/ui/table |
See Data tables. |
cn() / clsx |
not needed | Components take a class prop; layout comes from Stack, Flex, Grid and tokens. |
Chakra UI
| Chakra | CodeAtlas UI |
|---|---|
Button, IconButton |
Button, IconButton |
Input, Textarea, NumberInput |
TextField, TextArea, NumberField |
FormControl (label, helper, error) |
the same props on each field (label, description, error) |
Select |
Select |
Checkbox, Switch, RadioGroup, Slider |
Checkbox, Switch, RadioGroup, Slider |
PinInput |
PinInput |
Modal |
Dialog |
AlertDialog |
AlertDialog |
Drawer |
Drawer |
Menu |
DropdownMenu, DropdownMenuItem |
Popover, Tooltip |
Popover, Tooltip |
Tabs |
Tabs, TabsList, TabsTrigger, TabsContent |
Accordion |
Accordion, AccordionItem |
Alert, useToast |
Alert, Toaster plus toast |
Badge, Tag |
Badge, Chip |
Avatar |
Avatar |
Skeleton, Spinner, Progress |
Skeleton, Spinner, Progress |
Divider, Breadcrumb |
Divider, Breadcrumb |
Box, Flex, Stack, Grid, Container |
Box, Flex, Stack, Grid, Container |
Table |
SimpleTable |
style props (p, mt, bg) |
tokens (--ca-space-*, --ca-*) through gap, padding props on layout components, never inline styles |
Every shadcn/ui component
One row per component on ui.shadcn.com/docs/components, with the Radix primitive, Base UI package and MUI component that mean the same thing. A row marked none (gap #n) has no component here: use the workaround in the row and keep the issue number in a ca-ui-workaround(#n) marker. The same data answers get_equivalent("Dialog") in the MCP server and drives bunx @codeatlasdev/ui migrate-react.
| shadcn/ui | Radix, Base UI | MUI | CodeAtlas UI | Notes |
|---|---|---|---|---|
| Accordion | @radix-ui/react-accordion, @base-ui-components/react/accordion |
Accordion |
Accordion, AccordionItem |
type is single or multiple; one AccordionItem per section. |
| Alert | Alert |
Alert |
severity is success, warning, error or info; title, closable. |
|
| Alert Dialog | @radix-ui/react-alert-dialog, @base-ui-components/react/alert-dialog |
AlertDialog |
onconfirm, oncancel, destructive, confirmLabel, cancelLabel, pending. |
|
| Aspect Ratio | @radix-ui/react-aspect-ratio |
AspectRatio, Image |
AspectRatio takes ratio for a video or an embed; a plain picture is Image with ratio and fit. |
|
| Attachment | FileUpload |
Upload flow with progress and removal; for a read-only file chip use Chip. |
||
| Avatar | @radix-ui/react-avatar, @base-ui-components/react/avatar |
Avatar |
Avatar |
src, alt, fallback, size, shape, status. |
| Badge | Badge |
Badge, Chip |
Badge is a count or dot on another element; Chip is a labelled pill. |
|
| Breadcrumb | Breadcrumbs |
Breadcrumb |
items array (label, href) instead of nested list parts. |
|
| Bubble | MessageBubble |
Chat message bubble. | ||
| Button | Button |
Button |
variant is filled, outlined, ghost, glass or destructive; loading, fullWidth, href. |
|
| Button Group | ButtonGroup |
ButtonGroup |
orientation="vertical" stacks; a split button is a DropdownMenu with a triggerChild as the last member. A single choice is SegmentedControl. |
|
| Calendar | DateCalendar |
Calendar, RangeCalendar |
Values are @internationalized/date values, not Date. |
|
| Card | Card |
Card |
CardHeader/CardTitle/CardContent/CardFooter become title, children and headerActions. |
|
| Carousel | Carousel, CarouselItem |
Native scroll-snap, no Embla. label is required; perView, loop, autoplay (ms, opt in), bind:index. |
||
| Chart | LineChart, BarChart, PieChart, ScatterChart, RadarChart, GaugeChart, HeatmapChart, Sparkline |
Pass data only: no options object, no colors. Import from @codeatlasdev/ui/charts. |
||
| Checkbox | @radix-ui/react-checkbox, @base-ui-components/react/checkbox |
Checkbox |
Checkbox |
label is built in; bind:checked. |
| Code Block | CodeSnippet, CodeTabs |
Needs the optional peer shiki. |
||
| Collapsible | @radix-ui/react-collapsible, @base-ui-components/react/collapsible |
Collapsible |
title, bind:open. |
|
| Combobox | Autocomplete |
Combobox |
options, bind:value, emptyText, creatable, multiple. |
|
| Command | CommandPalette, Combobox |
CommandPalette is app-wide navigation (modules); a searchable field is Combobox. |
||
| Context Menu | @radix-ui/react-context-menu |
ContextMenu, DropdownMenuItem |
Wrap the region in ContextMenu; the menu snippet takes the DropdownMenu items. Right-click, long-press and Shift+F10. |
|
| Data Table | DataTable |
columns and data; sorting, search and paging are built in. Needs @tanstack/svelte-table. |
||
| Date Picker | DatePicker |
DatePicker, DateRangePicker |
Values are @internationalized/date values, not Date. |
|
| Dialog | @radix-ui/react-dialog, @base-ui-components/react/dialog |
Dialog |
Dialog |
bind:open, title, description; no DialogTrigger: a Button sets open = true. |
| Direction | I18nProvider |
Text direction follows the locale of I18nProvider. |
||
| Drawer | Drawer |
Drawer, Sheet |
Drawer slides from a side (side); Sheet is the mobile bottom sheet. |
|
| Dropdown Menu | @radix-ui/react-dropdown-menu, @base-ui-components/react/menu |
Menu |
DropdownMenu, DropdownMenuItem |
Also DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuSub. |
| Empty | EmptyState |
title, description, snippet actions. |
||
| Field | TextField, FormSection |
Label, description and error are props of each field; group fields with FormSection. |
||
| Hover Card | @radix-ui/react-hover-card |
HoverCard |
openDelay, closeDelay; touch never opens it (use Popover for tap-to-open). |
|
| Input | TextField |
TextField |
label, description, error (a string), bind:value. |
|
| Input Group | TextField |
prefix and suffix on TextField: a string is text addon, a snippet is a Button/IconButton/select inside the border. |
||
| Input OTP | PinInput |
length, oncomplete, mask. |
||
| Item | LinkCard, SettingItem |
A navigable row is LinkCard; a settings row is SettingItem. |
||
| Kbd | Kbd |
|||
| Label | TextField |
The label is a prop of the field; there is no standalone Label. |
||
| Marker | Divider |
A labelled separator is Divider. |
||
| Menubar | @radix-ui/react-menubar |
Menubar, MenubarMenu, DropdownMenuItem |
Menubar holds MenubarMenu labels; each menu’s content is the DropdownMenu items. |
|
| Message | MessageBubble, ConversationItem |
Chat rows. | ||
| Message Scroller | LiveList |
A list that follows appended items. | ||
| Native Select | Select |
options array; it is a custom listbox, not a native <select>. |
||
| Navigation Menu | @radix-ui/react-navigation-menu |
NavigationMenu, NavigationMenuItem, NavigationMenuLink |
Disclosure navigation (not role=menu): NavigationMenuItem label holds the panel links; plain entries are NavigationMenuLink. |
|
| Pagination | Pagination |
Pagination |
count, perPage, bind:page. |
|
| Popover | @radix-ui/react-popover, @base-ui-components/react/popover |
Popover |
Popover |
Snippets trigger and children. |
| Progress | @radix-ui/react-progress, @base-ui-components/react/progress |
LinearProgress |
Progress |
value, max, indeterminate. |
| Questionnaire | Stepper |
A multi-step flow is Stepper with FormSections. |
||
| Radio Group | @radix-ui/react-radio-group, @base-ui-components/react/radio-group |
RadioGroup |
RadioGroup |
items array and bind:value. |
| Resizable | react-resizable-panels |
ResizablePanels, ResizablePanel, ResizableHandle |
direction, persist, onlayout; each panel takes id, defaultSize, minSize, maxSize in percent. |
|
| Scroll Area | @radix-ui/react-scroll-area, @base-ui-components/react/scroll-area |
ScrollArea |
type, orientation, maxHeight. |
|
| Select | @radix-ui/react-select, @base-ui-components/react/select |
Select |
Select |
options array, not children; for search use Combobox. |
| Separator | @radix-ui/react-separator, @base-ui-components/react/separator |
Divider |
Divider |
orientation. |
| Sheet | Drawer, Sheet |
Drawer for a side panel, Sheet for the mobile bottom sheet. |
||
| Sidebar | AppShellLayout |
modules, pathname, onNavigate; only in signed-in route layouts. |
||
| Skeleton | Skeleton |
Skeleton |
variant, rows. |
|
| Slider | @radix-ui/react-slider, @base-ui-components/react/slider |
Slider |
Slider |
bind:value, or bind:values for a range. |
| Spinner | CircularProgress |
Spinner |
label for assistive tech. |
|
| Switch | @radix-ui/react-switch, @base-ui-components/react/switch |
Switch |
Switch |
label, bind:checked. |
| Table | Table |
SimpleTable |
Native <thead>/<tbody> markup inside; sorting and search: DataTable. |
|
| Tabs | @radix-ui/react-tabs, @base-ui-components/react/tabs |
Tabs |
Tabs, TabsList, TabsTrigger, TabsContent |
bind:value on Tabs. |
| Textarea | TextareaAutosize |
TextArea |
rows, resize, bind:value. |
|
| Toast | Snackbar |
Toaster, toast |
Mount Toaster once; call toast.success(msg). |
|
| Toggle | @radix-ui/react-toggle, @base-ui-components/react/toggle |
ToggleGroup |
A single toggle is a ToggleGroup with one item. |
|
| Toggle Group | @radix-ui/react-toggle-group, @base-ui-components/react/toggle-group |
ToggleButtonGroup |
ToggleGroup, SegmentedControl |
items array; a single choice is SegmentedControl. |
| Tooltip | @radix-ui/react-tooltip, @base-ui-components/react/tooltip |
Tooltip |
Tooltip |
content is a string. |
| Trace | Timeline |
Ordered steps with status. | ||
| Typography | Heading |
Headings; body text uses the .ca-prose class. |
From Tailwind
The library does not need utility classes for what its components own, and it keeps layout in typed props so there is no class string to drift. Map the intent, not the class.
| Tailwind | CodeAtlas UI |
|---|---|
flex gap-4 items-center |
<Flex gap="4" align="center"> |
grid grid-cols-3 gap-4 |
<Grid columns={3} gap="4"> |
flex flex-col gap-2 |
<Stack gap="2"> |
p-4, px-6, py-2 |
p="4", px="6", py="2" on a layout primitive; the step is the Tailwind number |
md:grid-cols-2 |
columns={{ base: 1, md: 2 }} |
max-w-3xl mx-auto |
<Container> |
rounded-md, rounded-lg |
nothing on a component; var(--ca-radius-md) / var(--ca-radius-lg) in your CSS |
shadow-md |
var(--ca-shadow-md), or elevation on Surface and Card |
bg-background, bg-card |
var(--ca-surface-base), var(--ca-surface-raised) |
text-foreground, text-muted-foreground |
var(--ca-text-primary), var(--ca-text-secondary) |
bg-primary, text-primary |
var(--ca-brand-fill), var(--ca-brand-text) |
border, border-border |
var(--ca-outline-default) |
dark: variants |
none: every token already has a dark value, set by the theme |
cn(...) / clsx |
not needed; pass class |
Two rules keep the port clean. Never write a hex color or a pixel size: use a token (bun run a search_tokens call to find it). And do not recreate a utility layer; a one-off style goes in a scoped class using tokens.
<script lang="ts">
import { Card, Flex, Grid } from "@codeatlasdev/ui";
</script>
<Grid columns={{ base: 1, md: 3 }} gap="4">
<Card title="Revenue"><Flex align="center" gap="2">$12k</Flex></Card>
<Card title="Orders"><Flex align="center" gap="2">340</Flex></Card>
<Card title="Refunds"><Flex align="center" gap="2">12</Flex></Card>
</Grid>
Layout patterns
Do not port a React page wrapper one-to-one. Pick the template that matches what the page is, then fill its snippets. They own the scroll behaviour, the header, the footer and the mobile layout.
| Your React page | Use | Holes |
|---|---|---|
| A list or CRUD index with search, filters and paging | AppList |
data, searchKeys, perPage; snippets actions, filters, children. |
| Create or edit one entity | AppForm with FormSection |
backHref, snippets footer for Cancel and Save, leading. |
| Settings with several sections | AppSettings |
tabs, bind:activeTab; sections are FormSection and SettingItem. |
| Any other content page (dashboard, detail) | PageShell |
title, description, snippet actions. |
| The signed-in frame (sidebar, bottom nav) | AppShellLayout |
modules, pathname, onNavigate. Only in authenticated route layouts. |
| Login, sign-up | AuthLayout with AuthCard |
|
| Empty and error states | EmptyState, ErrorPage |
<script lang="ts">
import { AppList, Button, SimpleTable, StatusChip } from "@codeatlasdev/ui";
let { data } = $props();
</script>
<AppList title="Users" data={data.users} searchKeys={["name", "email"]} perPage={10}>
{#snippet actions()}
<Button variant="filled" href="/users/new">New user</Button>
{/snippet}
{#snippet children(list)}
<SimpleTable>
<thead><tr><th>Name</th><th>Status</th></tr></thead>
<tbody>
{#each list.rows as u (u.id)}
<tr>
<td>{u.name}</td>
<td><StatusChip status={u.active ? "success" : "neutral"} label={u.active ? "Active" : "Inactive"} /></td>
</tr>
{/each}
</tbody>
</SimpleTable>
{/snippet}
</AppList>
Do not wrap fields in Stack or Flex for layout inside FormSection: layout is grid by default, list for switches and stack for composite blocks. Mobile rules (viewport units, safe areas, keyboard) are in Mobile.
Theme mapping
There is one ThemeOptions object in src/lib/theme.ts. Map your design tokens into it instead of recreating a theme provider:
| React source | ThemeOptions |
|---|---|
MUI palette.primary.main, Tailwind primary, Chakra brand.500 |
presets: [{ id: "app", color: "#rrggbb" }] and defaultPreset: "app". The library derives the full light and dark ramp in OKLCH. |
Per-role colors (background, text, error) |
presets[].colors.light and .dark, see Theming. |
MUI palette.mode, next-themes, Chakra colorMode |
defaultScheme: "system" and the colorScheme store (colorScheme.toggle(), or the ThemeToggle component). |
MUI shape.borderRadius, Tailwind rounded-* |
radius (multiplier) and controlRadius. |
MUI spacing, Tailwind spacing scale, Chakra space |
density: compact, comfortable or airy. |
MUI typography.fontFamily, Tailwind fontFamily |
fonts: { body, mono, display }. |
| Anything else (a one-off color or size) | overrides: { light: { "--ca-…": "…" }, dark: { … } }. Find the token with search_tokens. |
// src/lib/theme.ts
import type { ThemeOptions } from "@codeatlasdev/ui/vite";
export const theme: ThemeOptions = {
presets: [{ id: "app", color: "#2563eb" }],
defaultPreset: "app",
defaultScheme: "system",
radius: 0.8,
density: "comfortable",
fonts: { body: "Inter, system-ui, sans-serif" },
};
Delete the React theme provider, the Tailwind color config and every sx, className and style prop that only set color, spacing or radius.
Data tables
TanStack Table for React becomes DataTable from the @codeatlasdev/ui/table entry, built on @tanstack/svelte-table. Column helpers keep the same accessor idea; search, sorting, paging and the column toggle are built in.
| TanStack Table (React) | @codeatlasdev/ui/table |
|---|---|
createColumnHelper<T>() |
createCaColumnHelper<T>() |
useReactTable({ data, columns, … }) + your markup |
DataTable with columns and data (simple mode), or createCaTable to render your own markup |
getFilteredRowModel, globalFilter state |
built in; controlled with globalFilter and onGlobalFilterChange |
| sorting state | built in; controlled with sorting and onSortingChange |
| pagination state, manual pagination | built in; controlled with pagination, onPaginationChange and total for server data |
flexRender(cell.column.columnDef.cell, …) |
renderComponent from @tanstack/svelte-table for header or cell components |
| column visibility menu | DataTableColumnToggle, already in the toolbar |
| faceted filter chips | DataTableFacetedFilter as a child of DataTable |
<script lang="ts">
import { DataTable, createCaColumnHelper } from "@codeatlasdev/ui/table";
type User = { name: string; email: string };
let { users }: { users: User[] } = $props();
const col = createCaColumnHelper<User>();
const columns = col.columns([
col.accessor("name", { header: "Name", meta: { label: "Name" } }),
col.accessor("email", { header: "Email", meta: { label: "Email" } }),
]);
</script>
<DataTable {columns} data={users} />
Details and controlled mode: DataTable. For a plain list without search or sorting, SimpleTable inside AppList is lighter.
React patterns that change shape
Each pattern shows the React code and the Svelte code that replaces it. The Svelte blocks are type-checked against the built package in CI, like every demo, so they keep working when the API moves.
Context and global state
const ThemeContext = createContext<Theme>("light");
export const useTheme = () => useContext(ThemeContext);
A module with $state replaces the provider. Put it in a .svelte.ts file; a plain .ts file does not track.
<script lang="ts">
import { colorScheme, toast, Button } from "@codeatlasdev/ui";
// colorScheme is the library's own store: preference, resolved scheme, preset.
const label = $derived(`Theme: ${colorScheme.resolved}`);
</script>
<Button variant="ghost" onclick={() => toast.show(label)}>{label}</Button>
Data fetching
const { data, isPending } = useQuery({ queryKey: ["users"], queryFn: fetchUsers });
Fetch in load, so the page renders with its data on the server. Reach for TanStack Query only for client-side caching, and then the Svelte adapter takes the options as a function.
<script lang="ts">
import { AppList, EmptyState } from "@codeatlasdev/ui";
// data comes from +page.ts / +page.server.ts `load`; no effect, no loading flag.
let { data } = $props();
</script>
{#if data.users.length}
<AppList title="Users" data={data.users} searchKeys={["name"]} perPage={10}>
{#snippet children(list)}
{#each list.rows as u (u.id)}<p>{u.name}</p>{/each}
{/snippet}
</AppList>
{:else}
<EmptyState title="No users yet" />
{/if}
Routing
| React Router, TanStack Router | SvelteKit |
|---|---|
useParams() |
page.params from $app/state |
useSearchParams() |
page.url.searchParams |
useNavigate() |
goto() from $app/navigation |
<Link to> |
a plain <a href>; SvelteKit intercepts it |
route loader |
+page.ts / +page.server.ts load |
<Outlet /> |
{@render children()} in +layout.svelte |
| route guard | hooks.server.ts or load that throws redirect(303, "/login") |
The signed-in frame takes the router as two props, so it needs no adapter:
<script lang="ts">
import { goto } from "$app/navigation";
import { page } from "$app/state";
import { AppShellLayout } from "@codeatlasdev/ui";
let { children, data } = $props();
</script>
<AppShellLayout modules={data.modules} pathname={page.url.pathname} onNavigate={(path) => goto(path)}>
{@render children()}
</AppShellLayout>
Forms
const [name, setName] = useState("");
<form onSubmit={(e) => { e.preventDefault(); save(name); }}>
A form action replaces the handler and works without JavaScript; use:enhance keeps the page in place.
<script lang="ts">
import { enhance } from "$app/forms";
import { Button, TextField } from "@codeatlasdev/ui";
let { form } = $props();
let saving = $state(false);
</script>
<form method="POST" use:enhance={() => { saving = true; return async ({ update }) => { await update(); saving = false; }; }}>
<TextField name="name" label="Name" error={form?.errors?.name} />
<Button type="submit" variant="filled" loading={saving}>Save</Button>
</form>
SSR and global state
A module-level $state is created once per server process, so it is shared by every request. Do not keep per-user data in it. Per-request data goes through load and event.locals; per-tree data goes through setContext during component init. The library follows the same rule: setLocale is client-only.
i18n per request
<IntlProvider locale={locale} messages={messages}>
I18nProvider in the root layout is the SSR-safe equivalent. The locale comes from load, never from a module.
<script lang="ts">
import { I18nProvider } from "@codeatlasdev/ui";
let { children, data } = $props();
</script>
<I18nProvider locale={data.locale}>
{@render children()}
</I18nProvider>
Your own strings keep using your catalog; useLocale() gives the current tag inside the tree for Intl formatting.
Compare the result with the old app
bunx @codeatlasdev/ui compare <oldUrl> <newUrl> opens both in Chromium at 390 and 1280 px (--viewport), measures each data-testid element (or --selector CSS) and prints the differences sorted by size: box, computed styles (--props) and a pixel diff. A button that differs in weight and padding gives exactly those rows. Box deltas up to --tolerance px (0.5) are ignored; exit 1 on any difference, 2 without playwright in the project. In tests, measure(el, props) from @codeatlasdev/ui/testing gives the same data.
Gaps and workarounds
If something in the React app has no equivalent in the library (a component, a prop, a behaviour), do not fake it and do not leave it undocumented:
-
Check twice. Call
search_demos,get_componentandget_exportwith the React name and with a description of what it does.list_componentsshows everything. -
Write a workaround in the app’s own code, with a one-line marker so it can be found and removed later:
<!-- ca-ui-workaround(#123): no equivalent of MUI TreeView, using nested Collapsible -->In script code use
// ca-ui-workaround(#123): reason. Use the issue number once you have it; until then write the marker without a number and add it after step 3. -
Report it. Call the MCP tool
report_issuewithkind: "migration-gap", a neutraltitle,expected(what the React component did),actual(what the library offers), a minimalreproand yourworkaround. It returns a sanitized issue body and a prefilled link; it never posts by itself. Open the link, or run thegh issue createcommand it prints.
Never put project, company or customer names, file paths, URLs, environment values, secrets, data or business logic in the report. Use neutral names (Foo, items) in the repro. bunx @codeatlasdev/ui workarounds lists the markers in your code and which of their issues are closed, so you know when to delete a workaround.
Tip
Run svelte-check after every ported page. A wrong prop or snippet name is a type error, which is the fastest signal that a component was guessed instead of looked up.