Skip to main content
Light Dark System

Migrate from Shoelace

Cordwainer Elements is a rename-and-retheme fork of Shoelace: same component shapes and attributes, different tag names (sl-cw-), CSS custom property namespace (--sl---cw-), custom event names (sl-cw-), and TypeScript class/type names (Sl*Cw*) — including for anyone who’s built their own components on top of Shoelace’s. Adopting Cordwainer in an existing Shoelace project means finding and renaming all four by hand, across every file type in the project.

@cordwainer/cw-migrate (source) is a standalone CLI that automates it. It’s a tool you run directly against a project you’re migrating — not something you add as a dependency of that project.

Why not find-and-replace?

Naive find-and-replace isn’t safe here — sl-button and sl-button-group need to be told apart, and a closing tag has to match its opener. cw-migrate uses a real parser for each domain it rewrites (parse5 for markup, PostCSS for stylesheets, ts-morph for TypeScript/JSX) instead of regex, so it understands the file it’s rewriting rather than pattern-matching text.

Quick Start

Run it directly with npx — no separate install step:

npx @cordwainer/cw-migrate <path>

With no subcommand, cw-migrate scans first, then rewrites every domain in order and prints a summary of what changed.

# Preview every change without writing anything
npx @cordwainer/cw-migrate ./my-project --dry-run

# Actually rewrite the project (requires a clean working tree)
npx @cordwainer/cw-migrate ./my-project

What it rewrites

Each of these is also runnable on its own (e.g. cw-migrate elements <path>), for anyone who’d rather convert incrementally than all at once:

  • Elements — custom element tags (<sl-button><cw-button>) across HTML, JSX/TSX, Vue, and Svelte, plus Lit html/svg tagged templates in .ts/.js. Also renames sl-* CSS type selectors (sl-button { ... }cw-button { ... }) in stylesheets and css/styled-components template literals, and sl-* selector strings passed to .querySelector(...)/.querySelectorAll(...)/.closest(...)/.matches(...)
  • Tokens — CSS custom properties (--sl-color-primary-600--cw-color-primary-600) in stylesheets, inline style attributes (including inside Lit html/svg tagged templates), JSX style object props, and styled-components/emotion template literals
  • Events — custom event names (sl-changecw-change) in addEventListener calls and framework event bindings (Vue, Angular, the React wrapper)
  • TypeScript references — exported class/type identifiers (SlButtonCwButton) everywhere they’re referenced, not just imported
  • Imports — JS/TS import specifiers and CDN URLs
  • package.json — swaps the @shoelace-style/shoelace dependency for @cordwainer/cw-elements at a version you choose (run separately, since the tool can’t safely guess which version you want)

You can also run cw-migrate detect <path> on its own at any time — it scans and reports every Shoelace reference found without writing anything, useful as a CI check or just to see the scope of a migration before starting.

For the full command reference and flags, see the project README. Version history and the published package itself are on npm.

Real-world testing

cw-migrate has been run end-to-end against GoogleChrome/chromium-dashboard (chromestatus.com) — a production Lit + TypeScript app with 80 Shoelace-referencing files — as its first real-world validation, ahead of this stable release. Sample migration branch and write-up: matthandus/chromium-dashboard-migrate-shoelace-to-cordwainer#1.

That test surfaced and fixed four gaps in the tool itself, all shipped in this release:

  • elements wasn’t scanning .ts/.js files at all, so sl-* tags inside Lit html tagged templates — the majority of this project’s Shoelace usage — went untouched. detect’s element-match count went from 2 to 1102 after the fix.
  • No command renamed sl-* CSS type selectors (sl-button { ... }), only --sl-* custom properties — left as-is, this would have silently broken component styling post-migration.
  • Plain style="..." attributes inside Lit templates weren’t covered by the custom-property rewrite.
  • sl-* selector strings passed to .querySelector(...)/.querySelectorAll(...)/.closest(...)/.matches(...) weren’t renamed, which would have left tests silently querying for elements that no longer exist.

Post-fix, the migrated result installs and builds cleanly (npm install, tsc, rollup), with zero Shoelace tag/token/event/import/type references remaining in source.

Beyond the Shoelace migration

Once the initial wave of Shoelace-to-Cordwainer conversions is done, cw-migrate doesn’t go away — it becomes the permanent home for codemods that ship alongside future Cordwainer Elements major-version bumps, the same role tools like react-codemod or Angular’s ng update schematics play for their own ecosystems.