Migrating Astro Projects to Starwind UI v3
A practical guide to migrating a legacy Astro project to Starwind UI v3's Runtime, including the manual changes the CLI cannot safely make.
Starwind UI v3 moves component behavior into the Starwind Runtime while keeping the styled Astro components in your project. The migration command handles the mechanical part of that change, but it cannot understand every customization in an existing application.
This guide covers both parts: running the migration and reviewing the application code that may need manual changes afterward. It is written for people migrating a project themselves and for teams that want to give an AI coding agent a reliable migration checklist.
What Changed in Starwind UI v3
Earlier Starwind UI components carried their behavior in scripts inside each copied component folder. In v3, the styled components remain local and editable, but shared behavior, state, events, forms, overlays, and accessibility logic come from the Starwind Runtime.
Most public imports continue to use paths such as @/components/starwind/dialog, so a migrated project should not need a broad import rewrite. The important differences are at the boundaries between your application and a component: custom event names, initial state, programmatic APIs, theme initialization, and code that depends on the old internal DOM.
That distinction is useful during review. A page that only renders a component using a current documented example may need no changes. A page that listens for component events, reaches into its DOM, or carries custom behavior is much more likely to need attention.
Before Migrating
Start from a clean Git branch or commit. The migration command can copy the complete existing Starwind component directory, including custom folders, but Git is still the clearest way to review the complete change and recover application code outside that directory.
Before running it:
- Use Node.js 22.12 or newer.
- Confirm the project uses Astro 5 or newer and Tailwind CSS 4.
- Run the project’s normal build and typecheck so you know whether any failures predate the migration.
- Note which copied Starwind components you have customized.
- Identify custom CSS, scripts, tests, and analytics that query component internals.
- Keep the migration backup until the migrated application has been tested and accepted.
If the project cannot pass its baseline checks, record the existing failures before continuing. Otherwise, it becomes difficult to distinguish migration regressions from earlier problems.
Run the Migration
# npmnpx starwind@latest migrate
# pnpmpnpm dlx starwind@latest migrate
# Yarnyarn dlx starwind@latest migrateThe interactive flow is the safest default because it lets you decide whether to create the component backup. If you decline the backup, the CLI asks whether to overwrite all conflicts and then asks about each remaining conflict individually.
For a clean branch in an automated workflow, accept the prompts non-interactively:
pnpm dlx starwind@latest migrate --yesThe CLI detects the project’s package manager. The --yes option creates the component backup and overwrites registered conflicts without asking again, so only use it when the working tree is safe to change.
In short, migrate can back up the Starwind component directory, install Runtime-backed replacements and requirements, and update the Starwind configuration. When the bundled registry declares a public component rename, the CLI can also offer to update matching import paths and exported names in project source files. It reports whether each rename was applied, skipped, or had no matching usage.
Those targeted codemods do not rewrite arbitrary props, event listeners, application scripts, CSS selectors, or component customizations.
See the migration command documentation for the complete backup, conflict, configuration, and report behavior.
Read the final migration summary before changing application code. A skipped component, an unresolved registry component, or a component recorded with source: "legacy" still requires migration work, even if the command completed successfully.
Manual Changes to Review
The following changes matter when your project uses the affected API. Do not rewrite components blindly: search for the old pattern, compare each occurrence with the current component documentation, and change only the code that actually depends on it.
Replace the Legacy Theme Helper
The old Theme Toggle documentation included an inline script that read localStorage, changed the dark class, and re-ran after Astro navigation. Remove that helper instead of running it alongside the new controller.
Add ThemeInitScript to the shared layout’s <head>:
---import { ThemeInitScript } from "@starwind-ui/astro/theme";---
<head> <ThemeInitScript /></head>The head script applies the saved or system theme before the page paints. The Runtime controller then keeps theme controls synchronized across system preference changes and Astro page transitions.
For programmatic theme changes, use the shared controller:
import { initThemeController } from "@starwind-ui/astro/theme";
const theme = initThemeController();theme.setTheme("system");New listeners should use starwind:theme-change. The legacy theme:change event is currently emitted for compatibility, but new code should use the Runtime event rather than extending the legacy contract. See the dark mode guide for the complete setup.
Update Component Events and Event Types
Runtime events are normally dispatched from the component root. Listen on that root when possible, and dispatch programmatic commands to the intended component rather than to document.
| Legacy event | Runtime event |
|---|---|
starwind-select:change | starwind:value-change |
starwind-select:select | starwind:set-value on the Select root |
starwind-switch:change | starwind:checked-change |
slider-change | starwind:value-change |
slider-commit | starwind:value-committed |
starwind-color-picker:change | starwind:value-change |
starwind-dropdown-checkbox:change | starwind:checked-change |
starwind-toggle:change | starwind:pressed-change |
starwind-input-otp:change | starwind:value-change |
Toggle and Input OTP still emit their old events for compatibility, but the shared Runtime events are the forward-looking APIs. Radio Group continues to emit starwind:value-change; its event detail now includes information such as the current value, previous value, changed radio value, and reason instead of the old component-id detail.
Legacy component folders also exported local types such as SelectChangeEvent, SwitchChangeEvent, and ColorPickerChangeEvent. Those types are no longer exported from the styled component folders. Import the current detail type from the corresponding Runtime-backed package when needed—for example, SelectValueChangeDetails from @starwind-ui/astro/select, SwitchCheckedChangeDetails from @starwind-ui/astro/switch, or ColorPickerValueChangeDetails from @starwind-ui/astro/color-picker—and type the listener as a CustomEvent with that detail.
Review Select and Searchable Selects
The Select root now owns its form and state props, including name, required, readOnly, disabled, and defaultValue. Move those props from SelectTrigger or other child parts to Select. Put the placeholder on SelectTrigger; the current trigger renders its value part by default.
Programmatic updates are now targeted. Dispatch starwind:set-value on the specific Select root instead of dispatching starwind-select:select on document. Listen for starwind:value-change on that same root.
SelectSearch has been removed. Replace searchable Select compositions with the standalone Combobox and its documented input, content, group, and item parts. A normal Select that does not use SelectSearch does not need to become a Combobox.
Move Imperative Toast Imports
Keep the styled Toaster import from your local component folder, but import the imperative toast API and its Runtime types from @starwind-ui/astro/toast.
---import { Toaster } from "@/components/starwind/toast";---
<Toaster />import { toast } from "@starwind-ui/astro/toast";
toast.success("Project migrated");Audit any local Toast manager imports or namespace access as well; the old copied manager is no longer the application-facing imperative API.
Update Carousel Plugins and Programmatic Access
Projects that only render a standard Carousel can continue using the styled components. Projects that initialize plugins or access the Carousel API should replace the old local initCarousel helper with createCarousel from @starwind-ui/astro/carousel.
Set autoInit={false} on a Carousel that you initialize manually. This prevents the component’s normal Runtime setup from creating a second instance before your plugins or custom options are supplied.
Keep the lifecycle handling needed by the application. In an Astro site with client-side navigation, custom initialization may need to run after astro:after-swap and starwind:init, as shown in the current Carousel documentation. Review old CarouselManager, CarouselApi, or Carousel.init usage against the current Runtime instance API.
Update External Overlay Triggers
External Dialog and Alert Dialog triggers now use targetId instead of for:
<DialogTrigger targetId="account-dialog">Edit account</DialogTrigger>
<Dialog id="account-dialog"> <!-- dialog content --></Dialog>Triggers nested inside their Dialog or Alert Dialog do not need a target id.
Remove animationDuration
Remove the legacy animationDuration prop from Runtime-managed overlay content such as Dialog, Alert Dialog, Dropdown, Hover Card, Popover, Select, Tooltip, and Color Picker content.
The prop is no longer needed because the Runtime presence lifecycle observes the element’s actual CSS animations and transitions before hiding it. It waits for animations reported by the Web Animations API and falls back to the computed CSS animation or transition duration and delay. This keeps presence synchronized with the styles that really ran instead of requiring the same duration to be configured in both CSS and a component prop.
Define timing through normal CSS or Tailwind animation utilities. Use the documented Runtime state attributes, including data-starting-style, data-ending-style, data-open, data-closed, or the component’s data-state, to describe enter and exit styles.
Use Astro Initial-State Props
For Checkbox, Switch, and checked menu items in Astro, use defaultChecked to describe the initial checked state. The Runtime owns later interaction and form-reset updates after the static Astro markup hydrates.
Passing checked to an Astro adapter also affects the initial rendered state, but it does not create a reactive controlled component. Prefer defaultChecked because it states the Astro behavior accurately. Use the component’s Runtime instance or documented command event when application code must change the state later.
Use defaultValue for Select’s initial value. The styled Astro Select does not expose a reactive value prop; dispatch starwind:set-value on its root for later programmatic changes. React components inside an Astro project use the React adapter’s controlled props and callbacks instead.
Use Native Select Defaults
In Astro, NativeSelect no longer provides the legacy React-style defaultValue prop. Use native HTML option semantics instead: add selected to the intended NativeSelectOption, or allow the first option to be selected. In React, continue to pass defaultValue to NativeSelect.
If application state changes the native select after load, use the normal DOM value property and change event rather than a Starwind-specific default-value API.
Replace Polymorphic Dropdown Links
Legacy Dropdown items could render anchors through as="a":
<DropdownItem as="a" href="/settings">Settings</DropdownItem>Replace that polymorphic item with the dedicated link part:
<DropdownLinkItem href="/settings">Settings</DropdownLinkItem>This is a component rename, not a restoration of polymorphism. DropdownItem remains the part for commands performed in the current interface, while DropdownLinkItem renders a native anchor and accepts normal anchor attributes. Disabled link items omit their href. Link items keep the menu open by default; add closeOnClick when navigation does not unload the page and the menu should close immediately.
Context Menu remains action-only in v3. Move Context Menu navigation to normal links or the Navigation Menu rather than carrying its legacy polymorphic item pattern forward.
Replace the legacy destructive-item variant with normal classes that preserve its highlighted and focus states, such as text-destructive data-highlighted:text-destructive focus:text-destructive. Migrate nested menus to the dedicated ContextMenuSub, ContextMenuSubTrigger, and ContextMenuSubContent parts shown in the current Context Menu documentation.
Review Legacy Color Picker Markup
The v3 styled Color Picker once required explicit compound anatomy, but now provides a complete default editor again. Existing one-line usage can migrate to <ColorPicker label="…" />, with convenience props for alpha, format controls, EyeDropper visibility, clearing, and swatches. Set inline when the editor should remain visible; the hidden form input is rendered automatically.
Custom children still replace the visible default anatomy. Use the parts shown in the current Color Picker documentation for authored layouts, or vendor the Color Picker Primitive when raw area-thumb, format-selector, or hidden-input anatomy is required.
Replace calls to the old DOM-patched element.setValue() method with a Runtime instance from createColorPicker(root) in @starwind-ui/astro/color-picker. Listen for starwind:value-change; use the documented committed-value and format events only when the application needs those separate moments.
Review Component Anatomy and Accessible Names
InputGroupAddon is now presentational and no longer focuses its input when clicked. Give the actual control an accessible name and use InputGroupButton when the addon performs an action.
Give each focusable Radio Group item an explicit accessible name. Also make sure the Input OTP root has an accessible name, because assistive technology interacts with the Runtime-owned control rather than only the visible slots.
Audit Imports and Removed Namespace Members
Named imports remain the safest public path. Audit legacy default imports for Button and Color Picker because their default exports no longer represent the same single component. Also review namespace-style access such as Select.Search, Carousel.init, and Toast.Manager; those members were removed or replaced by the APIs described above.
When an import fails, inspect the current local component index.ts for styled parts and the corresponding @starwind-ui/astro/<component> entry point for behavior, instances, and event detail types.
Audit Custom CSS and DOM Queries
Search application CSS, scripts, tests, and analytics for .starwind-* classes and selectors such as .starwind-theme-toggle. Runtime-backed components intentionally use a different internal discovery layer, so selectors that depended on the old copied implementation may stop matching after the component folder is replaced.
Use data-slot as the normal styling and public-part hook. Attributes beginning with data-sw-* are primarily Runtime behavior and discovery hooks; do not make them the first choice for custom visual styling when a data-slot exists. Use documented state attributes such as data-state, data-checked, and data-highlighted for state-dependent styles.
Also search for the legacy event names, event-type imports, SelectSearch, local Toast APIs, initCarousel, manual Carousels without autoInit={false}, animationDuration, external trigger for, static Astro checked props that should express initial state with defaultChecked, Native Select defaultValue, DropdownItem as="a" usages that should become DropdownLinkItem, other polymorphic Context Menu links, and removed default or namespace imports described above.
The migration backup is useful for identifying intentional visual changes. Port those decisions into the new styled source carefully, but do not restore an entire legacy component folder or copy its behavior script over the Runtime-backed component. Doing so can reintroduce the behavior the migration was meant to replace.
Verify the Migrated Project
Run the same build, typecheck, and test commands recorded before migration. Then test the behavior that static checks cannot cover:
- Load the site with light, dark, and system themes. Check for a flash of the wrong theme, system-preference updates, and Astro client-side navigation.
- Submit and reset forms containing Checkbox, Switch, Select, Native Select, Radio Group, Input OTP, and Color Picker values.
- Open and close overlays with their triggers, the Escape key, outside interaction, and any application-controlled state. Confirm focus returns correctly.
- Exercise programmatic Select, Color Picker, Carousel, and Toast usage.
- Confirm event listeners run once, receive the expected detail, and target the intended component root.
- Review customized components at narrow and wide viewport sizes.
- Check custom styles, tests, and analytics that previously depended on legacy classes or DOM structure.
Do not delete the component backup until these checks pass and the migration summary contains no unintended skipped or legacy components. If a behavior is unclear, compare the new component documentation with the backed-up source to identify the application requirement—then implement that requirement through the current API rather than restoring the old script.
AI Agent Migration Runbook
When assigning this migration to an AI coding agent, give it this article and the migration command documentation, then require the following sequence:
- Inspect Git status, the package manager, Node version, Astro version, Tailwind version, Starwind configuration, and installed component directory. Do not migrate a dirty working tree without first accounting for the existing changes.
- Run the project’s build, typecheck, and relevant tests. Record pre-existing failures.
- Run the appropriate
starwind@latest migratecommand. Prefer the interactive flow; use--yesonly on a clean, recoverable branch. - Retain the generated component backup. Review migrated, skipped, legacy, custom-folder, and rename-codemod outcomes in the migration summary. Inspect every configuration entry still marked
source: "legacy"before claiming success. - Search the project for each legacy event, prop, import, helper, selector, and composition pattern documented in this article. Inspect each match in context rather than applying an unconditional replacement.
- Update the theme helper first, then events and types, component APIs, programmatic integrations, and finally CSS or DOM customizations.
- Reapply only deliberate visual or composition changes from the backup. Do not copy legacy behavior scripts back into Runtime-backed components.
- Run formatting and the full project validation. Exercise theme, form, overlay, event, and navigation behavior in a browser.
- Report the commands run, files changed, behavior verified, pre-existing failures, remaining legacy components, and any decisions that still require a maintainer.
The migration is complete when the project uses the Runtime-backed components intentionally, its application integrations use the current contracts, and all remaining legacy entries are understood—not merely when the CLI exits successfully.