Treat styling as a contract between the host page and the component.
Capsule gives you sealed components (Shadow DOM or CSS Modules) plus a tiny, well-documented “Style API” so teams stop renegotiating “how we style” on every project.
Isolated styles by default, instant theming via CSS variables, safe customization via ::part, and responsive by container queries. No runtime CSS-in-JS.
Inspired by this article.
For developers migrating from Tailwind or CSS‑in‑JS, see the migration and adoption guide.
ThemeManager scopes tokens per tenant so styles can't leak across boundaries.dispatchSafeEvent keeps custom events inside component boundaries.::part, CSS vars).Pick one:
::part.@layer for order.Design tokens become CSS custom properties (and optional TS types). All component styles reference tokens; hosts theme by swapping token sets (e.g., <body data-theme="dark">), not by editing component code.
A single project-wide contract:
@layer reset, base, utilities, components, overrides;
reset — normalizer + box-sizingbase — typography, page bgutilities — low-specificity helperscomponents — component-local styles (Capsule lives here)overrides — explicit escape hatchUse :where(...) inside components to keep specificity low and stable.
Props map to recipes that produce class names at build time (e.g., CVA, Panda, UnoCSS, stylex’s compiled mode). No runtime styled-components/emotion unless you truly need dynamic computed styles.
Example:
import { buttonRecipe } from '@capsule-ui/core/button.recipe';
import { cardRecipe } from '@capsule-ui/core/card.recipe';
const btnClass = buttonRecipe({ size: 'lg', variant: 'secondary' });
const cardClass = cardRecipe({ variant: 'outline' });
Each core component ships with a matching recipe (e.g., inputRecipe, selectRecipe, tabsRecipe, modalRecipe) for CSS Module workflows.
Components adapt to their container:
:host { container-type: inline-size; }
@container (min-width: 420px) { /* ... */ }
::part(...) when using Shadow DOMtheme="dark", variant="ghost", density="compact")Everything else stays private.
An early preview package @capsule-ui/core publishes foundational elements for experimentation:
<caps-button> – styled button element<caps-input> – basic text input<caps-card> – surface container<caps-tabs> – tabbed interface<caps-modal> – modal dialog<caps-select> – styled select elementInstall with pnpm add @capsule-ui/core and try them in your project. Early adopters are encouraged to experiment and share feedback while these components evolve.
Prefer to stay in a framework? Tiny adapters wrap the Web Components so they behave like native React, Vue, or Svelte components:
@capsule-ui/react@capsule-ui/vue@capsule-ui/svelteEach forwards attributes and events and keeps the same Style API for
::part, CSS variables, and attributes. See
framework adapter docs for usage examples.
When building or deploying the Docusaurus site in website/, set these environment variables:
DOCS_SITE_URL (required in production) – Full docs origin (for example, https://docs.capsule-ui.dev).DOCS_ORGANIZATION_NAME (required in production) – Git host organization/user for edit/deploy metadata.DOCS_PROJECT_NAME (required in production) – Repository/project name.DOCS_BASE_URL (optional, default /) – Base path where the docs app is served.DOCS_GTAG_ID (optional) – Google Analytics measurement ID. If unset, gtag config is omitted from the build.website/docusaurus.config.ts now throws at startup/build time when NODE_ENV=production and required metadata is missing, preventing placeholder deployments.
Drop this into any HTML page:
<script type="module">
import "https://cdn.jsdelivr.net/gh/your-org/capsule-ui/examples/booking-widget.js";
</script>
<booking-widget id="w"></booking-widget>
<script>
// Theme at runtime: flip a token
document.getElementById("w").style.setProperty("--bk-brand", "#ff3b3b");
// Set a variant
document.getElementById("w").setAttribute("theme", "dark");
</script>
booking-widget::part(button) { text-transform: uppercase; }
booking-widget::part(card) { border-radius: 24px; }
Don’t want Web Components? Use the CSS Modules flavor and expose the same Style API via module classes and token variables.
Capsule components don’t bypass accessibility—they expose focus rings,
keyboard navigation and ARIA roles by default. CSS custom properties respect
prefers-reduced-motion and prefers-contrast so hosts can theme high‑contrast
or motion‑reduced modes. Locale helpers (getLocale, setLocale,
formatNumber, formatDate) wire up RTL/LTR and locale‑aware formatting.
See Component Accessibility Checklist.
customElements.define("booking-widget", class extends HTMLElement {
constructor(){
super();
const r = this.attachShadow({mode:"open"});
const style = document.createElement("style");
style.textContent = `
@layer reset, base, components;
:host{
--bk-brand: #4f46e5; --bk-text: #0f172a;
container-type: inline-size; display:block; color:var(--bk-text);
}
:host([theme="dark"]){ --bk-text:#e6e8ef; }
@layer base {
.button{ background: var(--bk-brand); color:white; border:0; padding:.7rem 1rem; border-radius:12px; }
}
`;
const btn = document.createElement("button");
btn.className = "button";
btn.setAttribute("part", "button");
btn.type = "button";
btn.textContent = "Book";
r.append(style, btn);
}
});
Capsule works best with a few non-negotiables:
0-1-0; no !important outside @layer overrides.@layer components in component CSS files; override the layer name with CAPSULE_LAYER_NAMES="utilities,components" or disable with CAPSULE_LAYER_NAMES=off.CAPSULE_ALLOW_RUNTIME_STYLES=true to permit one-off dynamic styles and use CSS-in-JS only for cases that can’t be expressed with tokens or precompiled utilities.pnpm run check:bundle-size gates bundle size growth in CI.CAPSULE_LAYER_NAMES and CAPSULE_ALLOW_RUNTIME_STYLES are escape hatches. Use them sparingly and see governance flag guidelines for trade-offs and review practices.@layer overrides.Isn’t this just Tailwind?
Similar goals (speed, predictability), different contract. Capsule exposes Style APIs (vars/parts) and supports runtime theming and true encapsulation; Tailwind is app-global utilities and usually build-time theming.
Do I need Web Components?
No. Shadow DOM is great for embeddables. For app-internal components, CSS Modules + layers work well.
What about SSR?
Browser support?
Shadow DOM v1, ::part, and container queries are supported in all modern evergreen browsers. For legacy support, use the CSS Modules flavor and avoid Shadow-only features.
Accessibility? Capsule doesn’t bypass a11y—your components still need focus states, ARIA, contrast, keyboard handling, and reduced-motion respect. The isolation helps keep a11y styles consistent.
Source tokens live in tokens/source/tokens.json using the W3C draft design tokens structure. The build pipeline is implemented in scripts/build-tokens.ts and runs via pnpm tokens:build to generate dist/tokens.css, dist/tokens.d.ts, and dist/tokens.json. The CSS file exposes custom properties for the built-in light, dark, and ocean themes; toggling [data-theme="dark"] (or any other theme name) on the page swaps the values.
For development convenience, pnpm tokens:watch monitors tokens/source/tokens.json and rebuilds the output whenever it changes.
Use helpers from @capsule-ui/core to switch themes by updating the data-theme attribute:
import { setTheme, getTheme, onThemeChange } from '@capsule-ui/core';
setTheme('dark'); // <html data-theme="dark">
console.log(getTheme());
const stop = onThemeChange(t => console.log('theme', t));
Add a new theme by defining values for it in tokens/source/tokens.json and rebuilding with pnpm tokens:build. Then call setTheme('<name>') or set <html data-theme="<name>"> at runtime.
For multi-tenant apps, ThemeManager can load and switch tenant-specific presets without a page reload:
import { ThemeManager } from '@capsule-ui/core';
const loaded = await ThemeManager.load('tenantA', '/themes/tenant-a.json');
if (!loaded) {
console.warn('Falling back to defaults for tenantA');
}
ThemeManager.applyTheme('tenantA', 'dark');
// Later, when tearing down a micro-frontend:
ThemeManager.unregister('tenantA');
ThemeManager.reset();
Designers can experiment with token values in the theming lab. The page updates components live as you tweak CSS variables and can export a JSON preset for reuse.
Shareable presets can be uploaded to a lightweight theme registry. Each upload receives a unique URL that teams can reference at runtime or package for distribution on npm. Browse, fetch, and reuse themes without copying token files between projects.
Use dispatchSafeEvent to emit sanitized custom events that stay inside the
component's shadow DOM by default. This prevents accidental leakage across
micro-frontend boundaries:
import { dispatchSafeEvent } from '@capsule-ui/core';
dispatchSafeEvent(el, 'capsule:change', { value });
A public documentation site built with Docusaurus lives in website. Run pnpm docs:dev to start a local server or pnpm docs:build to generate static files.
Capsule provides a published command line interface.
pnpm add -g capsule-cli
capsule new component Button # scaffolds component, tests, docs and ADR stub
capsule tokens build # runs the token pipeline (pnpm run tokens:build)
capsule check # runs lint, token and test checks