Skip to content
On this page Decide first: incremental or rewrite

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

  1. Create the app. bunx sv create my-app and choose SvelteKit with TypeScript. Svelte 5 runes mode is the default.

  2. Install and wire the package.

    bun add @codeatlasdev/ui @lucide/svelte
    bunx @codeatlasdev/ui init --write

    init writes the theme file, the Vite plugins, the server hook, the global.css import and a mobile-ready viewport meta. Details: Installation.

  3. Check the setup. bunx @codeatlasdev/ui doctor lists missing peers and wiring with a one-line fix each, see Installation.

  4. Install optional peers only for what you port. @tanstack/svelte-table for DataTable, echarts for charts, shiki for code blocks.

  5. Give your agent the manifest. claude mcp add codeatlas-ui -- npx -y -p @codeatlasdev/ui ca-ui-mcp, see AI agents. Tell it to call get_component before 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 $effect to compute a value. useEffect plus setState becomes $derived. An effect that writes state it also reads can loop.
  • Reads after await are not tracked. Read every reactive value before the first await inside 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, localStorage or matchMedia belongs in onMount or $effect, or behind browser from $app/environment.
  • Reading $state outside a component (a plain .ts file) does not track. Put shared reactive state in a .svelte.ts file.
<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:

  1. Check twice. Call search_demos, get_component and get_export with the React name and with a description of what it does. list_components shows everything.

  2. 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.

  3. Report it. Call the MCP tool report_issue with kind: "migration-gap", a neutral title, expected (what the React component did), actual (what the library offers), a minimal repro and your workaround. It returns a sanitized issue body and a prefilled link; it never posts by itself. Open the link, or run the gh issue create command 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.