YeSvelte — Svelte Components With Swappable Themes

YeSvelte is an open-source UI component library for Svelte. Its components render clean, semantic markup, and the look comes from a theme you choose: Tabler (built on Bootstrap 5) or daisyUI (built on Tailwind). Switching themes means swapping one stylesheet, not rewriting components. Most Svelte UI libraries are tied to a single CSS framework; YeSvelte is designed not to be.

At a glance

  • My role: creator and maintainer; designed the core architecture
  • Started: February 2023, with the first npm release 15 days later
  • Adoption: 222 GitHub stars and about 136,000 npm downloads since 2023, about 10,000 of them in the past year
  • Community: 8 contributors, 217 merged pull requests and 877 commits
  • Scope: 99 components in 44 modules, with 488 live examples in the docs
  • Themes: Tabler (Bootstrap 5) and daisyUI (Tailwind), each with dark mode and a right-to-left build
  • Runs on: Svelte 5 (migration in progress) and SvelteKit, written in TypeScript
  • License: MIT
  • Status: pre-1.0; latest build 0.2.1-next.4 (August 2025)

as of September 2026

Why I built it

Most component libraries make you choose twice: once for the components, and again for the CSS framework they’re welded to. I wanted Svelte components for real business apps (forms, tables, dialogs, dashboards) where the behavior and markup are written once and the look is a replaceable layer. A team already on Bootstrap or Tailwind shouldn’t have to fight a second design system to use them.

How it’s built

YeSvelte has two halves that never call each other. One is a component library that renders semantic markup; the other is a theme build that turns an existing CSS framework into rules for that markup. They meet in exactly one place: the y-* class names.

Runtime · inside your Svelte appBuild time · npm run style<Button color=primary size=lg>El + classname()turns props into class names<button class="y-buttony-button-color-primaryy-button-size-lg">CSS framework sourceTabler (Bootstrap 5) or daisyUI (Tailwind)Placeholder SCSSevery .class becomes a %placeholderTheme partials.y-button-color-primary extends %btn-primaryTheme stylesheettabler.css or daisyui.cssplus .min.css and .rtl.min.cssThe only contract: y-* class namesswap the stylesheet link, keep every componentcomponents emitthemes style

Every component renders through one element. El is a polymorphic element that sits under every YeSvelte component. A component decides only three things: the HTML tag, its name, and which of its props are style modifiers. El turns those props into predictable class names:

<Button color="primary" size="lg" outline>Save</Button>
<!-- renders class="y-button y-button-color-primary y-button-size-lg y-button-outline" -->

The same element gives every component about 100 utility props for spacing, grid, flex, typography, borders and more. Components never emit framework classes like btn-primary, and none of them carries its own styles.

One positioning engine for every overlay. Popup anchors floating content to an element with Floating UI, opens on click, hover or focus, and closes on outside clicks. Tooltips, popovers, dropdown menus, navbar and sidebar menus, and the autocomplete list are all Popup under a different name, so a fix to positioning lands everywhere at once.

Proven libraries, wrapped and loaded lazily. Date picking (Litepicker), rich text (Quill), multi-handle sliders (noUiSlider), input masks (Inputmask), focus trapping (focus-trap) and fuzzy search come from mature libraries instead of being rebuilt. The heaviest ones, for the editor, date picker, input masks and focus trap, load through a dynamic import after their component mounts. Server rendering stays clean, and that code downloads only when a page uses it.

Forms that still submit like forms. Each Form* component pairs a control with a shared field wrapper for the label, hint and validation state, and wires the label to the input automatically. Custom controls like the autocomplete, date picker, editor and slider each render a hidden native input. A plain HTML form submission carries their values with no extra code.

The docs can’t drift from the code. yesvelte.com is a prerendered SvelteKit site that uses the library the way any app would. Each example is a standalone file, and a custom build-time preprocessor places that file’s source next to the live component. The code a visitor copies is exactly the code running on the page, across all 488 examples.

The components themselves stack in three layers. Composite components reuse a few shared engines, and everything renders through El:

Tooltip · Popover · DropdownMenuNavbar and Sidebar menusAutocomplete list12 Form* componentsFormInput · FormSelectFormDatePicker …Modal · Offcanvas · ToastAccordion · AlertButton · Badge · CardTable · Tabs · StepsPagination …PopupFloating UI positioningclick · hover · focus triggersFormField + input controlslabel wired to the input idLitepicker · Quill · Inputmaskloaded on mountShow/hide state machineopen → opening → openedfocus-trap for dialogsEl · the one rendering primitivetag + componentName + cssProps → y-* classesabout 100 utility props · id · two-way value

One library, more than one CSS framework

YeSvelte ships two themes today, and the components don’t know which one is running. A theme is a single precompiled stylesheet, so choosing one is an import:

import 'yesvelte/css/tabler.min.css'   // the Bootstrap 5 look, via Tabler
// or
import 'yesvelte/css/daisyui.min.css'  // the Tailwind look, via daisyUI

Here is what happens to the same button under each theme:

Theme 1 · tabler.min.css · Bootstrap 5Theme 2 · daisyui.min.css · Tailwind<Button color=danger><button class="y-buttony-button-color-danger">.y-button-color-dangerextends %btn-dangerTabler's .btn-danger rulescolor from --y-dangerdark mode viadata-bs-theme attribute.y-button-color-dangerextends %btn-errordaisyUI's .btn-error rulesdanger maps to the error role29 color themes viadata-theme attributelink tabler.min.cssor link daisyui.min.css

What stays the same: every component, prop and line of markup. color="danger", size="lg" and mt="3" mean the same thing under both themes, so an app can change its look without touching a single .svelte file.

What the theme decides:

  • The visual language: Tabler’s clean admin-dashboard style, or daisyUI’s
  • Color roles: the daisyUI theme maps YeSvelte’s roles onto daisyUI’s, with primary, secondary and accent carried over directly and danger mapped to daisyUI’s error
  • Named and brand colors: the daisyUI theme defines Tabler’s 14 named colors and 15 brand colors (GitHub, LinkedIn, YouTube and more) itself, so color="azure" or color="github" works under both themes
  • Dark mode and color schemes: the Tabler theme switches with data-bs-theme="dark", and the daisyUI theme carries all 29 daisyUI color themes, from dark to dracula and corporate, switched with data-theme
  • Direction: each theme also ships a right-to-left build, tabler.rtl.min.css and daisyui.rtl.min.css

Adding a third framework is a theme folder, not a fork:

  1. Create src/scss/<name>/<name>.scss, which imports the framework’s full stylesheet.
  2. Create index.scss with the framework’s variables, the generated placeholder file, and one small mapping partial per component. The Tabler theme has 50 of them.
  3. Run npm run style -- <name> to build <name>.css, <name>.min.css and <name>.rtl.min.css.
  4. Apps import yesvelte/css/<name>.min.css. No component changes.

The SCSS layer

The technique that makes this work is a Sass feature most people never use: placeholder selectors. A %placeholder is a rule that outputs nothing unless something @extends it. YeSvelte’s build turns an entire CSS framework into placeholders, then lets each theme pull in only the rules it maps onto YeSvelte’s classes.

Phase 1 · framework → placeholdersPhase 2 · y-* classes → placeholdersOutputstabler.scssimports all of @tabler/coreand the noUiSlider stylesFull framework CSS.btn { … }.btn-primary { … }_tabler.placeholder.scss%btn { … }%btn-primary { … }outputs nothing unless extendedindex.scssBootstrap + Tabler variables+ placeholder file+ 50 component partialstabler.cssonly the mapped rulesunder .y-* selectorstokens as --y-* variablestabler.min.csscssnanotabler.rtl.min.csscssnano + postcss-rtlcsssrc/lib/css → npmyesvelte/css/*static/css → docs siteSassPostCSS: . → %Sass · @extend copiesonly what is mapped

The repo’s smallest theme shows the whole technique in three files:

// example.scss stands in for a whole framework
.btn          { background-color: red; }
.unused-class { background-color: blue; }

// Phase 1 rewrites it as _example.placeholder.scss
%btn          { background-color: red; }
%unused-class { background-color: blue; }

// Phase 2: the theme's mapping partial
.y-button { @extend %btn; }

That compiles to .y-button { background-color: red; } and nothing else. Neither .btn nor .unused-class reaches the output.

Real themes map whole components declaratively. This is most of the Tabler theme’s button partial:

$props: (
  null:  '%btn' '%gap-2',
  color: generate_props($theme_colors, ('%btn-')),
  shape: (pill: '%btn-pill', tile: '%btn-square'),
  size:  (sm: '%btn-sm', lg: '%btn-lg' '%gap-3'),
);

.#{$prefix-button} {
  @include apply-props($props);
}

The apply-props mixin turns each key into a class suffix: .y-button, .y-button-color-primary, .y-button-shape-pill, .y-button-size-sm and so on. Those are exactly the class names El generates. generate_props expands one line into a rule for every theme color, and every @extend is marked !optional, so a class a framework lacks produces no rule instead of breaking the build. What comes out is Tabler’s real button styling under YeSvelte’s names:

.y-button-color-danger {
  --y-btn-bg: var(--y-danger);
  --y-btn-hover-bg: rgba(var(--y-danger-rgb), .8);
  /* ...the rest of Tabler's .btn-danger */
}

Three more details matter in practice:

  • Namespaced all the way down. The build sets Bootstrap’s $prefix to y- before compiling, so even the CSS variables come out as --y-primary, not --bs-primary. In the compiled Tabler stylesheet, every class selector is a y-* class apart from the rich-text editor’s own ql-* classes. YeSvelte can run inside an app that already loads its own Bootstrap or Tailwind, even a different version, without the two fighting over .btn. The framework’s base reset for elements like body, links and form controls still applies globally, as it would with the framework itself.
  • Tailwind becomes a normal stylesheet. The daisyUI theme compiles daisyUI, a Tailwind build that keeps every class, and Bootstrap’s grid into placeholders: 570,970 lines of them, against 25,624 for Tabler. Your app gets plain precompiled CSS, with no Tailwind config, PostCSS setup or Sass step of its own.
  • Right-to-left comes free. Because themes are generated, the RTL version is one more PostCSS pass (postcss-rtlcss) rather than a second set of styles to maintain.

How it compares

Most Svelte UI libraries tie their components to one styling system. The table compares the main options on the questions YeSvelte’s design is built around, as of September 2026.

LibraryWhat its styling depends onCSS build step in your app?Same components on another CSS framework?
YeSvelteA precompiled theme: Tabler (Bootstrap 5) or daisyUI (Tailwind)No; import one stylesheetYes; swap the stylesheet
Flowbite-SvelteTailwind v4 utility classes built into each componentYes (Tailwind)No
SkeletonTailwind v4; its framework-agnostic core spans React and Svelte, not CSS frameworksYes (Tailwind)No
shadcn-svelteTailwind v4 on top of Bits UI, copied into your projectYes (Tailwind)Only by rewriting the copied code
SveltestrapBootstrap 5’s own classesNo; load Bootstrap’s CSSNo
Carbon Components SvelteIBM’s Carbon design system, with five precompiled Carbon themesNoNo; Carbon only
Svelte Material UIMaterial Design, from premade CSS or a custom Sass buildOnly to customize the themeNo; Material only
Bits UI (headless)Nothing; components ship unstyledUp to youNothing to swap; you write every style

Where YeSvelte comes out ahead:

  • No lock-in to a CSS framework. Carbon and Svelte Material UI also pair semantic classes with a precompiled theme, but each is locked to one design language. YeSvelte applies the same idea across frameworks. Among the libraries above, it’s the only one whose themes span both Bootstrap and Tailwind.
  • Nothing to configure. A theme is plain CSS. Your app needs no Tailwind config, no content paths to scan and no Sass pipeline, and a theme works straight from a CDN link.
  • Safe to drop into an existing app. Namespaced y-* classes and --y-* variables let YeSvelte run beside an app’s own Bootstrap or Tailwind. That makes gradual migrations and embedded widgets practical. A thin Bootstrap wrapper like Sveltestrap emits .btn and shares whatever Bootstrap version the host page loads.
  • Markup you can read. A primary button is y-button y-button-color-primary, not a string of utilities like Flowbite-Svelte’s text-white bg-primary-700 hover:bg-primary-800 dark:bg-primary-600. What you see in devtools tells you what the element is.
  • Right-to-left and dark mode in every theme. Each theme ships an RTL build and a dark mode. shadcn-svelte, by comparison, has an open issue about its components using left and right instead of start and end.
  • Layout without Tailwind. About 100 utility props (mt="3", dMd="none", col="6") give every component spacing, grid and display control under either theme.
  • Business-app components in the box. Date-range picking, rich text, masked inputs, an autocomplete that can create new options, multi-handle sliders, and form fields that submit natively all ship with the library.

It’s fair to say where others lead, too. Flowbite-Svelte, Skeleton and Bits UI have far larger communities and faster release cycles. Headless libraries like Bits UI give complete design freedom and build in WAI-ARIA patterns and keyboard navigation. And YeSvelte’s theme stylesheets aren’t yet trimmed to the components an app actually uses, which is on the roadmap below.

What you get out of the box

  • 99 components in 44 modules: buttons, cards, tables, tabs, steps, accordions, navbars, sidebars, modals, off-canvas panels, toasts, tooltips, popovers and dropdowns
  • Rich inputs: an autocomplete with fuzzy search, multi-select and create-new; a date and date-range picker; a rich-text editor; a multi-handle slider; masked inputs; file upload
  • 12 form-field components, such as FormInput, FormSelect and FormDatePicker, with labels, hints and validation states built in
  • Two themes, Tabler (Bootstrap 5) and daisyUI (Tailwind), each with light and dark modes and right-to-left builds, plus 29 daisyUI color themes
  • About 100 utility props on every component, for spacing, grid, flex, typography, borders, backgrounds and more
  • Any icon from the Iconify collections, with Tabler Icons as the default
  • Svelte 5 runes and snippets, with TypeScript types for every component
  • 11 ready-made page templates built only from YeSvelte: five sign-in and sign-up screens, two pricing layouts, an FAQ, two forms and a widgets page
  • A docs site with 70 pages, 488 live examples and Ctrl/Cmd+K search

My role

I started YeSvelte in February 2023 and designed its core: the El rendering primitive, the class-name contract between components and themes, the first version of the theme build, and the docs site with its example-inlining preprocessor. In the first two months I also wrote about two dozen of the early components, from Button and Card to Table, DatePicker and Autocomplete. I set up the project’s roadmap, code of conduct and issue templates, and the docs site’s SEO.

The first npm release shipped 15 days after the first commit. By mid-April 2023, ten weeks in, every component on the first quarter’s roadmap had shipped: 41 components and 11 form components.

As maintainer I reviewed and merged 187 of the project’s 217 pull requests. A small core team built much of the rest, including the overlay engine, the animation system, the Sass placeholder theme build, the DaisyUI theme and the move to Svelte 5.

Where it’s headed

YeSvelte is pre-1.0, and builds currently go to npm from every push as pre-release versions. An end-to-end review of the codebase found the core design sound. What remains before 1.0:

  1. A stable release: finish the Svelte 5 migration and ship 1.0 as npm’s default version, published from tagged releases instead of every push
  2. A safety net: component tests, a browser smoke test over the 488 docs examples, and a CI gate before anything publishes
  3. Accessibility: ARIA roles and full keyboard support for dialogs, tabs, tooltips and the autocomplete, following the WAI-ARIA Authoring Practices
  4. Lighter CSS: per-component stylesheets, or a purge step that keeps only the classes an app actually uses

Stack

Svelte 5 · SvelteKit · TypeScript · Vite · Sass · PostCSS (postcss-selector-replace, cssnano, postcss-rtlcss) · Tabler · Bootstrap 5 · daisyUI · Tailwind CSS · Floating UI · focus-trap · Litepicker · Quill · noUiSlider · Inputmask · Iconify · Prism.js · GitHub Actions · GitHub Pages · npm