Developing a Web Components Library (Shoelace)
Shoelace (now Web Awesome) is a ready-made library of UI components on Web Components. About 50 components: buttons, inputs, selects, dialogs, tooltips, trees, date picker. Works without a framework and with any: React, Vue, Angular, Svelte.
The task when working with Shoelace is not to write a library from scratch, but to properly set up, customize to your design system, and if necessary, extend with custom components.
Installation
npm install @shoelace-style/shoelace
Or CDN (without bundler):
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/[email protected]/cdn/themes/light.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/[email protected]/cdn/shoelace-autoloader.js"></script>
Cherry-pick import (tree-shaking)
Don't import the entire library — only the components you need:
// Register specific components
import '@shoelace-style/shoelace/dist/components/button/button.js'
import '@shoelace-style/shoelace/dist/components/input/input.js'
import '@shoelace-style/shoelace/dist/components/dialog/dialog.js'
import '@shoelace-style/shoelace/dist/components/select/select.js'
// Set up the path to icons and assets
import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path.js'
setBasePath('/shoelace') // copy assets to public/shoelace
// vite.config.ts — copy assets during build
import { viteStaticCopy } from 'vite-plugin-static-copy'
export default defineConfig({
plugins: [
viteStaticCopy({
targets: [{
src: 'node_modules/@shoelace-style/shoelace/dist/assets',
dest: 'shoelace/assets',
}],
}),
],
})
Customization via CSS Custom Properties
Shoelace is built on design tokens. Override them through CSS:
/* Global token override */
:root {
/* Primary color */
--sl-color-primary-50: hsl(262 100% 97%);
--sl-color-primary-100: hsl(262 95% 92%);
--sl-color-primary-200: hsl(262 90% 85%);
--sl-color-primary-300: hsl(262 85% 75%);
--sl-color-primary-400: hsl(262 80% 65%);
--sl-color-primary-500: hsl(262 75% 55%);
--sl-color-primary-600: hsl(262 75% 45%); /* ← main */
--sl-color-primary-700: hsl(262 75% 38%);
--sl-color-primary-800: hsl(262 75% 30%);
--sl-color-primary-900: hsl(262 75% 20%);
--sl-color-primary-950: hsl(262 75% 12%);
/* Typography */
--sl-font-sans: 'Inter', system-ui, sans-serif;
--sl-font-mono: 'JetBrains Mono', monospace;
--sl-font-size-medium: 0.9375rem; /* 15px */
/* Radii */
--sl-border-radius-small: 4px;
--sl-border-radius-medium: 8px;
--sl-border-radius-large: 12px;
/* Transitions */
--sl-transition-medium: 200ms ease;
}
CSS Parts: targeted customization
Each Shoelace component exports ::part() for external styling:
/* Button customization */
sl-button::part(base) {
font-weight: 700;
letter-spacing: 0.02em;
text-transform: uppercase;
font-size: 13px;
}
sl-button[variant="primary"]::part(base) {
background: linear-gradient(135deg, #7000ff, #b600ff);
border: none;
}
sl-button[variant="primary"]::part(base):hover {
background: linear-gradient(135deg, #5500cc, #9400cc);
}
/* Input */
sl-input::part(base) {
border-radius: 10px;
border-width: 1.5px;
}
sl-input::part(input) {
font-size: 15px;
}
/* Dialog */
sl-dialog::part(panel) {
border-radius: 20px;
box-shadow: 0 25px 60px rgba(0,0,0,0.3);
}
sl-dialog::part(overlay) {
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
}
React integration
npm install @shoelace-style/shoelace
// src/shoelace-setup.ts — run once
import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path.js'
import '@shoelace-style/shoelace/dist/components/button/button.js'
import '@shoelace-style/shoelace/dist/components/input/input.js'
import '@shoelace-style/shoelace/dist/components/dialog/dialog.js'
import '@shoelace-style/shoelace/dist/themes/light.css'
setBasePath('/shoelace')
// Types for TypeScript + JSX
// src/types/shoelace.d.ts
import type { SlButton, SlInput, SlDialog } from '@shoelace-style/shoelace'
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'sl-button': React.DetailedHTMLProps<
React.HTMLAttributes<SlButton> & {
variant?: 'default' | 'primary' | 'success' | 'neutral' | 'warning' | 'danger' | 'text'
size?: 'small' | 'medium' | 'large'
disabled?: boolean
loading?: boolean
outline?: boolean
pill?: boolean
circle?: boolean
type?: 'button' | 'submit' | 'reset'
href?: string
target?: string
},
SlButton
>
'sl-input': React.DetailedHTMLProps<
React.HTMLAttributes<SlInput> & {
type?: string
label?: string
placeholder?: string
value?: string
required?: boolean
disabled?: boolean
readonly?: boolean
'help-text'?: string
'error-message'?: string
clearable?: boolean
'password-toggle'?: boolean
},
SlInput
>
'sl-dialog': React.DetailedHTMLProps<
React.HTMLAttributes<SlDialog> & {
label?: string
open?: boolean
'no-header'?: boolean
},
SlDialog
>
}
}
}
import { useRef } from 'react'
import type SlDialog from '@shoelace-style/shoelace/dist/components/dialog/dialog.js'
import '../shoelace-setup'
export function ContactModal() {
const dialogRef = useRef<SlDialog>(null)
const openDialog = () => dialogRef.current?.show()
const closeDialog = () => dialogRef.current?.hide()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// ...
closeDialog()
}
return (
<>
<sl-button variant="primary" onClick={openDialog}>
Contact us
</sl-button>
<sl-dialog ref={dialogRef} label="Contact form">
<form onSubmit={handleSubmit}>
<sl-input
label="Name"
required
placeholder="John Doe"
style={{ marginBottom: '16px' }}
/>
<sl-input
label="Email"
type="email"
required
placeholder="[email protected]"
style={{ marginBottom: '16px' }}
/>
<sl-textarea
label="Message"
rows={4}
required
/>
</form>
<sl-button slot="footer" variant="default" onClick={closeDialog}>
Cancel
</sl-button>
<sl-button slot="footer" variant="primary" type="submit">
Send
</sl-button>
</sl-dialog>
</>
)
}
Dark theme
Shoelace supports dark/light via CSS class on <html>:
<!-- Light -->
<html class="sl-theme-light">
<!-- Dark -->
<html class="sl-theme-dark">
// Theme toggle
function toggleTheme() {
const html = document.documentElement
const isDark = html.classList.contains('sl-theme-dark')
html.classList.toggle('sl-theme-dark', !isDark)
html.classList.toggle('sl-theme-light', isDark)
localStorage.setItem('theme', isDark ? 'light' : 'dark')
}
// Initialize from prefers-color-scheme
const savedTheme = localStorage.getItem('theme')
|| (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
document.documentElement.classList.add(`sl-theme-${savedTheme}`)
Creating custom components in Shoelace style
If the library extends with custom components, inherit from ShoelaceElement:
import ShoelaceElement from '@shoelace-style/shoelace/dist/internal/shoelace-element.js'
import { customElement, property } from 'lit/decorators.js'
import { html, css } from 'lit'
@customElement('app-stat-card')
export class AppStatCard extends ShoelaceElement {
static styles = css`
:host { display: block; }
/* ... */
`
@property() label = ''
@property({ type: Number }) value = 0
@property() unit = ''
render() {
return html`
<div class="stat-card">
<div class="stat-value">${this.value}<span>${this.unit}</span></div>
<div class="stat-label">${this.label}</div>
</div>
`
}
}
Timeline
Setup Shoelace + customization for your design system + React integration — 2–4 days. Additional custom components on Shoelace/Lit basis + dark theme + documentation — another 1–2 weeks.







