Theming
LoomiUI theming works with standard CSS variables and requires no special setup. Define the variables in your app stylesheet, and every LoomiUI component on the page uses them automatically.
You can iterate quickly: override one token, refresh, and fine-tune. Once it looks right, the same theme values flow across buttons, inputs, alerts, and the rest of the library.
Which approach do I need?
Section titled “Which approach do I need?”Most projects only need the first option below. If you’re unsure, start with Token overrides and stop there unless you have a specific reason to go further.
Use this quick decision guide:
- If you only want LoomiUI components (like
<loomi-button>,<loomi-input>) to match your brand colors, choose Token overrides. - If you also style non-Loomi elements with Tailwind utility classes and want those classes to use the same colors, add Tailwind color sync.
- If you need to change framework internals (for example, introduce an entirely new palette family beyond the built-in six), choose Forking.
Pick based on your setup:
- Token overrides — the default path. Works in any project, with or without Tailwind. Start here.
- Tailwind color sync — add this only if you use Tailwind utility classes (like
bg-primary-600) and want them to match your Loomi theme. - Forking — only for framework-level changes (e.g. adding a brand-new palette color) that CSS variables can’t express.
Put your overrides in your global CSS entry file — the stylesheet imported once at your app’s root (for example src/index.css, src/main.css, or styles/global.css). That’s also the file that has @import "tailwindcss" if you use Tailwind.
Overriding the defaults
Section titled “Overriding the defaults”LoomiUI color tokens follow a predictable naming convention, making it easy to customize and override any palette color.
--loomi-<color>-<shade>
<color> is one of the six palette colors — primary, secondary, success, warning,
error, or gray (see The 6 palette colors). <shade> is the strength
of the color: 50 is very light, 600 is the normal button color, and 950 is very dark.
For example, this changes the main primary color to green:
:root { --loomi-primary-600: #16a34a; --loomi-primary-700: #15803d;}
You only need to override the shades you actually use, and any token you leave out keeps its default.
600 is the base color and 700 is the hover state; see How components use shades
for the full mapping.
Once you define these variables, every LoomiUI component using color="primary" picks them up automatically.
That includes buttons, inputs, tags, alerts, and any other component mapped to the primary tokens.
Put your overrides in :root so they’re available app-wide.
Because :root variables are global, LoomiUI can resolve them even inside the Shadow DOM (the isolated DOM each web component renders into).
In practice, this gives you one central place to theme your app instead of styling components one by one.
Tip: change the whole color ramp when you can. Components often use
more than one shade: 600 for the main color, 700 for hover,
and lighter shades for soft backgrounds.
Using Tailwind CSS
Section titled “Using Tailwind CSS”You can customize LoomiUI without Tailwind. The token overrides above
are standard CSS variables, so they work in any stack. If you are using Tailwind, put the same
:root block in the stylesheet that already has @import "tailwindcss", right after that import:
@import "tailwindcss";
:root { /* the same token overrides from above */ --loomi-primary-600: #16a34a;}Matching Tailwind utility classes
Section titled “Matching Tailwind utility classes”Out of the box, Tailwind utilities like bg-primary-600 are not connected to
LoomiUI tokens like --loomi-primary-600.
That means if you override LoomiUI’s primary color, components such as
<loomi-button> update, but your Tailwind markup (for example,
<div class="bg-primary-600">) still uses Tailwind’s default color palette.
To connect the two, import one extra file after Tailwind:
@import "tailwindcss";@import "@loomidev/theme/tailwind-colors.css";
With this import in place, Tailwind generates bg-primary-600, text-error-500,
and all other <color>-<shade> utilities from LoomiUI’s --loomi-* tokens
instead of Tailwind’s default palette. In practice, one token override updates
both your Tailwind classes and Loomi components together:
:root { --loomi-primary-600: #16a34a;}<!-- Both turn green --><div class="rounded-lg bg-primary-600 p-4 text-white"> Tailwind markup</div>
<loomi-button color="primary">Loomi component</loomi-button>
@loomidev/theme ships with every @loomidev/* component package.
If you already installed any Loomi component, you can import this file right away with no extra install needed.
Tailwind v4 only. The tailwind-colors.css import above is built
with the @theme inline directive, which exists only in Tailwind v4 (the CSS-first
config). On Tailwind v3, importing it will break your build — skip it and use
the v3 setup below instead.
Tailwind v3 setup
Section titled “Tailwind v3 setup”On Tailwind v3, connect the utilities to Loomi tokens in tailwind.config.js instead of importing
tailwind-colors.css. Point each color at the matching --loomi-* variable:
// tailwind.config.js (Tailwind v3)/** @type {import('tailwindcss').Config} */module.exports = { content: ["./src/**/*.{html,js,ts,jsx,tsx,vue}"], theme: { extend: { colors: { primary: shades("primary"), secondary: shades("secondary"), success: shades("success"), warning: shades("warning"), error: shades("error"), gray: shades("gray"), }, }, },};
// Maps every shade of a color to its Loomi CSS variablefunction shades(name) { const steps = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]; return Object.fromEntries(steps.map((s) => [s, `var(--loomi-${name}-${s})`]));}With this in place, a single
:root override updates both your Tailwind classes and Loomi
components — exactly like the v4 import does:
:root { /* updates bg-primary-600 AND <loomi-button color="primary"> */ --loomi-primary-600: #16a34a;}If you would rather not touch the Tailwind config at all, the
:root token overrides
still theme every Loomi component on their own in any Tailwind version, but you won’t get matching
bg-primary-600-style utility classes without the mapping above.
The 7 palette colors
Section titled “The 7 palette colors”These are the built-in semantic palettes available out of the box.
You can use any of these names with the color attribute across components.
| Palette | Description |
|---|---|
primary | your brand-first or default action color |
secondary | alternative emphasis for less prominent actions |
success | positive states (saved, completed, approved) |
warning | caution states (needs attention) |
error | destructive or failure states |
info | informational states and non-critical guidance |
gray | neutral UI surfaces, text-adjacent accents, and subtle actions |
These semantic names are intentionally reusable, so the same token name can theme buttons, alerts, tags, badges, and other components consistently.
For example:
<loomi-button color="secondary">Open</loomi-button><loomi-tag color="warning">Draft</loomi-tag><loomi-alert color="error">Action required</loomi-alert>
Each palette in LoomiUI uses the same 11-step shade scale:
50, 100, 200, 300, 400, 500, 600, 700, 800, 900, and 950.
Because the scale is consistent, token names are always predictable. Any
combination of a supported color + one of these shade values is valid, for example:
--loomi-success-500, --loomi-error-700, or --loomi-gray-100.
If you override tokens for one of these colors (such as --loomi-secondary-600),
every component using that color updates automatically.
This is a deliberately small, semantic set rather than a full Tailwind-style color
grid, where every color maps to a meaning (success, warning, error) or a brand role
(primary, secondary), so component authors and consumers never have to guess
which raw hue to reach for. Add more palettes by forking the build
if your product needs additional semantic colors (e.g. accent).
How components use shades
Section titled “How components use shades”Most interactive components map shades to states in a consistent way:
| Shade(s) | Usage |
|---|---|
600 | Base/primary action color |
700 | Hover and active state |
50–200 | Soft backgrounds, accents, and low-emphasis surfaces |
300–500 | Borders, outlines, and intermediate emphasis |
800–950 | High-contrast text and strong surfaces |
For best visual consistency, override the full shade ramp for a color instead of only one or two tokens. Updating the complete set keeps buttons, tags, alerts, and form controls visually aligned across all states.
Per-instance colors
Section titled “Per-instance colors”Use the color attribute when you want to apply a specific palette to an individual component instance.
This is useful when a single UI element needs semantic meaning (for example, danger, success, or warning)
without changing the global theme.
<loomi-checkbox color="error" checked>Critical</loomi-checkbox><loomi-checkbox color="success" checked>Resolved</loomi-checkbox>The color value must match one of the 6 supported LoomiUI palette names
(such as error, success, secondary, or primary).
Because LoomiUI components resolve these values from CSS tokens, instance colors remain fully themeable.
If you later override a token like --loomi-error-600, every component that uses color="error" updates
automatically across the app. This lets you keep component markup simple while still getting centralized control over visual styles.
Dark mode
Section titled “Dark mode”LoomiUI supports runtime theme switching and dark-mode token overrides out of the box.
The recommended option is the built-in <loomi-theme-switcher> component, which handles
mode selection, persistence, and class application automatically.
The switcher supports three modes:
| Mode | Behavior |
|---|---|
light | Always uses the light theme |
dark | Always uses the dark theme |
system | Follows the user’s OS/browser color-scheme preference |
Using the built-in switcher
Section titled “Using the built-in switcher”Import the component once, then render it where users can access theme controls (for example, your app header, profile menu, or settings page):
<script type="module"> import "@loomidev/theme-switcher";</script>
<loomi-theme-switcher></loomi-theme-switcher>When dark mode is active, the switcher applies a dark class to the root <html> element.
LoomiUI components automatically resolve any token overrides declared under html.dark.
The selected mode is saved in localStorage, so the user preference is restored on
subsequent visits without extra setup.
Customize switcher labels
Section titled “Customize switcher labels”Use custom text labels when needed:
<loomi-theme-switcher system-text="System"></loomi-theme-switcher>Use helper functions in custom UI
Section titled “Use helper functions in custom UI”If you are building your own theme toggle, import the helpers:
import { applyLoomiTheme, getLoomiTheme } from "@loomidev/theme-switcher";
applyLoomiTheme("dark"); // "light" | "dark" | "system"getLoomiTheme(); // reads the persisted choiceapplyLoomiTheme() applies the selected mode and updates the root class.
getLoomiTheme() reads the currently persisted preference.
Use your own theme system (no switcher)
Section titled “Use your own theme system (no switcher)”If your app already has theme controls, you do not need <loomi-theme-switcher>.
LoomiUI only requires a dark class on an ancestor element (typically <html>).
For example:
<html class="dark"> <body> <loomi-button color="primary">Save</loomi-button> </body></html>You can toggle the class directly:
document.documentElement.classList.add("dark"); // turn dark mode ondocument.documentElement.classList.remove("dark"); // turn dark mode offFor a persistent manual toggle, store the selected theme and apply it on load:
const savedTheme = localStorage.getItem("theme");
if (savedTheme === "dark") { document.documentElement.classList.add("dark");}
function setTheme(theme) { localStorage.setItem("theme", theme); document.documentElement.classList.toggle("dark", theme === "dark");}If your app uses a different class name (for example, theme-dark), place your LoomiUI
token overrides under that selector:
html.theme-dark { --loomi-gray-50: #0f172a; --loomi-gray-900: #f1f5f9;}Define dark-mode tokens
Section titled “Define dark-mode tokens”To customize dark mode colors, declare token overrides in html.dark:
html.dark { --loomi-gray-50: #0f172a; --loomi-gray-900: #f1f5f9; /* …override whichever tokens your dark palette needs */}Note that dark mode flips the neutral ramp: the light end (gray-50) becomes dark and the
dark end (gray-900) becomes light. That is why the values above look inverted — it keeps text
readable against dark surfaces without changing any component markup.
applyLoomiTheme(getLoomiTheme()) as early as possible during app startup.Forking for deeper changes
Section titled “Forking for deeper changes”For most teams, CSS token overrides are the right and sufficient approach. They are fast to ship, easy to maintain, and keep you on the official LoomiUI upgrade path.
You should fork LoomiUI only when you need framework-level changes that cannot be expressed with app-level CSS variables.
Common examples:
- Changing the default color values shipped to all consumers of your design system
- Adding a new built-in palette color beyond the standard 6 palettes
- Modifying how default tokens are generated at build time
These defaults are produced from palette.json during the LoomiUI build process.
As a result, this is a source-code customization workflow (inside a LoomiUI fork),
not a runtime theming workflow in your app.
If you only need product-level branding or per-app theme control, prefer token overrides in your stylesheet instead of maintaining a fork.