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.
The commands in this article use the current v3 beta release. The migration steps and manual review areas also describe the intended v3 architecture.
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, controlled 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 create a copy of the existing registered component folders, but Git is still the clearest way to review the complete change and recover application code outside those folders.
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
Run the beta CLI with the package manager used by the project.
# npmnpx starwind@beta migrate
# pnpmpnpm dlx starwind@beta migrate
# Yarnyarn dlx starwind@beta migrateThe interactive flow is the safest default because it lets you confirm the component backup and conflicts. For a clean branch in an automated workflow, pass the package manager explicitly and accept the prompts non-interactively:
pnpm dlx starwind@beta migrate --yes --package-manager pnpmChange the package manager value when the project uses npm or Yarn. 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 backs up registered components, installs their Runtime-backed replacements and requirements, and updates the Starwind configuration. It cannot safely 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 or a component recorded with source: "legacy" is still migration work, even if the command itself 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.
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.
Distinguish Controlled and Uncontrolled State
For Checkbox, Switch, and checked menu items, checked represents controlled state. Use defaultChecked when the component should own its mutable state after the initial render.
If you keep checked, your application is responsible for updating and re-synchronizing it when the value changes. This is especially important in static Astro markup where a one-time checked value may look like an uncontrolled default even though it is not.
Apply the same controlled-versus-default review to other stateful roots, including Select’s value and defaultValue.
Use Native Select Defaults
NativeSelect no longer provides a React-style defaultValue prop. Use native HTML option semantics: add selected to the intended NativeSelectOption, or allow the first option to be selected.
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.
Move Navigation Out of Dropdown Items
Legacy Dropdown items could use polymorphic composition such as as="a". Runtime menu items are actions, not navigation links, and that polymorphic anchor pattern has been removed from Dropdown and Context Menu items.
Use real links in normal navigation or the Navigation Menu when the destination is another page. Keep Dropdown and Context Menu items for commands performed in the current interface.
Rewrite Legacy Color Picker Markup
The old Color Picker was a compact, mostly self-contained component. The v3 Color Picker uses compound parts so its label, trigger, content, fields, controls, and hidden form value can be composed explicitly.
An existing one-line <ColorPicker /> should therefore be rewritten using the anatomy in the current Color Picker documentation. Move positioning props to ColorPickerContent and preserve a hidden input when the value participates in form submission.
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 Collection Values and Component Anatomy
Accordion roots using type="multiple" now require an array for defaultValue, even when only one item starts open.
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, animationDuration, external trigger for, mutable checked, Native Select defaultValue, polymorphic dropdown 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@beta migratecommand. Prefer the interactive flow; use--yesonly on a clean, recoverable branch. - Retain the generated component backup. Review the migration summary and 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.