A Component Library That Isn't Tied to Your Framework

Most component libraries force you into an ecosystem. React libraries demand React. Svelte libraries expect Svelte. Shoelace, a UX component library by Cory LaViska, sidesteps that entirely by building everything on Web Components. You get tabs, modals, accordions, auto-completes, alerts, and more, all styled and accessible out of the box — and all usable from whatever framework you happen to be working in.

That framework independence comes with some trade-offs worth knowing about before you commit. React's Web Component support is currently weak, though Shoelace provides React-specific wrappers to bridge the gap. A cleaner long-term option is a thin wrapper component that handles attribute and property propagation, then delete it once React ships its Web Component fixes (targeted for version 19). Server-side rendering has similar rough edges. Declarative Shadow DOM would solve this in theory, but browser support is thin and it requires server-side cooperation. For client-rendered SPAs, none of this matters — Web Components just work.

Getting Started With a Demo

To see Shoelace in action, we'll build a small Svelte app with tab groups and a dialog component. The markup is largely straight from the Shoelace docs:

<sl-tab-group>
  <sl-tab slot="nav" panel="general">General</sl-tab>
  <sl-tab slot="nav" panel="custom">Custom</sl-tab>
  <sl-tab slot="nav" panel="advanced">Advanced</sl-tab>
  <sl-tab slot="nav" panel="disabled" disabled>Disabled</sl-tab>

  <sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
  <sl-tab-panel name="custom">This is the custom tab panel.</sl-tab-panel>
  <sl-tab-panel name="advanced">This is the advanced tab panel.</sl-tab-panel>
  <sl-tab-panel name="disabled">This is a disabled tab panel.</sl-tab-panel>
</sl-tab-group>

<sl-dialog no-header label="Dialog">
  Hello World!
  <button slot="footer" variant="primary">Close</button>
</sl-dialog>

<br />
<button>Open Dialog</button>

That renders clean, styled tabs. The active underline animates and slides between selections without any extra work.

Four horizontal tab headings with the first active in blue with placeholder content contained in a panel below.
Default tabs in Shoelace

Working With Methods and Events

Web Component APIs are slightly different from framework-native ones, but the pattern is simple. The <sl-tab-group> component exposes a show method for programmatically selecting tabs. In Svelte, grab a reference with bind:this:

<script>
  let tabs;
</script>

Then bind it to the element:

<sl-tab-group bind:this="{tabs}"></sl-tab-group>

Now a button can trigger it:

<button on:click={() => tabs.show("custom")}>Show custom</button>

Events work the same way. The sl-tab-show event fires when a tab changes. Svelte's on:event-name syntax handles it:

<sl-tab-group bind:this={tabs} on:sl-tab-show={e => console.log(e)}>

That logs each event object as you switch tabs.

Event object meta shown in DevTools.

The dialog component (<sl-dialog>) follows the same pattern. It takes an open prop, and fires an sl-hide event when closed — including when the user clicks outside. Declare the state, bind the event, and wire up the close button:

<script>
  let tabs;
  let open = false;
</script>

Pass the prop, listen for hide so the state stays in sync, and add a click handler to the close button:

<sl-dialog no-header {open} label="Dialog" on:sl-hide={() => open = false}>
  Hello World!
  <button slot="footer" variant="primary" on:click={() => open = false}>Close</button>
</sl-dialog>

Finally, open it from a button:

<button on:click={() => (open = true)}>Open Dialog</button>

Styling Despite the Shadow DOM

Shoelace is still in beta, and default styles are subject to change, but the customization concepts are stable. The tricky part is that Web Components use the Shadow DOM, which encapsulates styles. Outside rules don't normally affect inside elements. But there are three ways through, and Shoelace makes good use of all of them.

The tabs component markup shown in DevTools.

Inspect a tab header in DevTools and you'll see the component's internal div with a part="base" attribute, sitting inside a shadow root. A <slot> element renders whatever content you placed between the component tags.

Inheritable Styles Pierce the Boundary

Properties like font-family and letter-spacing inherit by default, and that inheritance crosses shadow root boundaries. In the :root section of the demo's app.css, there's a letter-spacing: normal declaration. Change it to something like 2px, and tab headers pick it up immediately.

Four horizontal tab headers with the first active in blue with plqceholder content contained in a panel below. The text is slightly stretched with letter spacing.

CSS Custom Properties Work Across Roots

CSS custom properties are the second escape hatch. A shadow root can always read custom properties defined outside it. The <sl-tab-group> component reads an --indicator-color variable for the active tab underline, so customizing it is plain CSS:

sl-tab-group {
  --indicator-color: green;
}

That gives you a green indicator.

Four horizontal tab headers with the first active with blue text and a green underline.

The ::part Selector Handles the Rest

For non-inheritable styles, Shoelace exposes internal elements via the part attribute, which you can target from outside using the ::part selector. In Shoelace 2.0.0-beta.83, enabled tabs get a pointer cursor. To change the active tab to a default cursor, combine the part="base" attribute with the active attribute that Shoelace adds to the selected tab:

sl-tab[active]::part(base) {
  cursor: default;
}

Customizing Animations

Shoelace uses the Web Animations API for its transitions and exposes a setDefaultAnimation method to override them. The dialog defaults to expanding outward on open and shrinking on close. Here's how to make it slide down from the top instead, then drop back out when hidden:

import { setDefaultAnimation } from "@shoelace-style/shoelace/dist/utilities/animation-registry";

setDefaultAnimation("dialog.show", {
  keyframes: [
    { opacity: 0, transform: "translate3d(0px, -20px, 0px)" },
    { opacity: 1, transform: "translate3d(0px, 0px, 0px)" },
  ],
  options: { duration: 250, easing: "cubic-bezier(0.785, 0.135, 0.150, 0.860)" },
});
setDefaultAnimation("dialog.hide", {
  keyframes: [
    { opacity: 1, transform: "translate3d(0px, 0px, 0px)" },
    { opacity: 0, transform: "translate3d(0px, 20px, 0px)" },
  ],
  options: { duration: 200, easing: "cubic-bezier(0.785, 0.135, 0.150, 0.860)" },
});

The call lives in the demo's App.svelte file — comment it out to see the stock behavior.

Final Thoughts

Shoelace is an ambitious answer to a real problem: high-quality UX components that don't lock you into a framework. With new frameworks appearing regularly, and each bringing its own performance and ergonomics story, having a solid component layer that works everywhere is increasingly valuable. The Web Component ecosystem has rough edges — React and SSR being the main ones — but Shoelace ships with practical workarounds for both.