Try the Theme Playground to visually customize and preview your theme, then copy the generated CSS variables.
display:none to hide features; toggle them with a prop on the primitive instead.
The model
Velt exposes a large set of CSS custom properties (variables), all prefixed--velt-…. You override them in your own stylesheet. Velt reads them at render time, so your values flow through its whole UI: including (for variables) inside the shadow DOM.
There are two families:
- Modern tokens:
--velt-light-mode-*,--velt-dark-mode-*,--velt-spacing-*,--velt-border-radius-*,--velt-font-size-*, plus z-index tokens. Use these. - Legacy tokens:
--legacy-velt-*(older components still read some of these). Only touch them if a specific older surface needs it.
CSS variables.
Steps
Step 1: Make sure your selector CSS can reach Velt
CSS variables cross the shadow boundary (so Step 2 variable theming works no matter what), but class/element selectors do not reach inside a shadow DOM. If you’ll use any selector-based CSS (Step 5) or styled wireframes, pick one of these two supported approaches: Option A: turn shadow DOM off (simplest; your normal stylesheet then reaches the elements):injectCustomCss API. Use this when you want to keep Velt’s style isolation (so Velt’s CSS can’t leak into your app and vice-versa) but still style inside it:
injectCustomCss pushes your CSS into Velt’s shadow root, so selectors match there. type: "styles" = a raw CSS string; type: "link" = a stylesheet URL.
Pick A or B per project: don’t do both for the same surface. (See Setup.)
Shadow DOM & wireframes (root vs nested)
Verified in-browser. When you customize with wireframes, whether you still needshadowDom={false} depends on which wireframe you registered:
- You registered the component’s ROOT wireframe (e.g.
VeltCommentDialogWireframe/velt-comment-dialog-wireframe): Velt auto-removes that component’s own shadow DOM, so it renders in light DOM and your normal class CSS reaches it: noshadowDom={false}needed for that component. - You registered only a NESTED / leaf wireframe (e.g. just the
ThreadCard, with no root dialog wireframe): shadow DOM is NOT auto-removed. Your layout markup applies, but class CSS won’t reach inside, so the look may change without your styles landing. For this case you must setshadowDom={false}(or the per-surface flag /injectCustomCss) explicitly. - Either way, inline
style=""on your wireframe markup always works (it survives the clone and lives in whatever DOM the slot renders into). - Nesting caveat: a root-wireframed component that renders inside another shadow-DOM component (e.g. the dialog shown inside the sidebar with the sidebar’s default shadow on) still sits in that outer component’s shadow root: turn the outer component’s shadow off too if you need class CSS there.
Step 2: Override variables at :root
Put all Velt CSS in one stylesheet. Override the tokens you care about:
CSS variables. Never invent a token name.
Step 3: Support dark mode
Velt switches modes via thedata-velt-theme="dark" attribute on the document root. Override the dark tokens under that selector:
data-velt-theme="dark" attribute when dark mode is on. To toggle dark mode, use any of:
- the
darkModeprop on a component:<VeltComments darkMode={isDark} />(alsodialogDarkMode,pinDarkMode, … for finer control); - the client API:
const { client } = useVeltClient(); client.setDarkMode(true)(app-wide); - follow the OS preference: wire your own
prefers-color-schememedia query tosetDarkMode.
setDarkMode / OS), and you supply the dark token values under [data-velt-theme="dark"].
Step 4: Change the default font
Velt reads one global font token,--velt-default-font-family (default sans-serif). Override it at :root and every Velt surface picks it up: no per-component work:
--velt-font-size-* scale can be overridden the same way.)
Step 5: Override specific elements by class (always with !important)
For anything variables don’t cover, target Velt’s classes directly. Velt injects its own styles at runtime with high specificity, so your overrides must use !important to win. Treat this as the rule for class-based Velt CSS, not a hack.
The workflow (do this for any element you want to restyle):
- Run the app with
shadowDom={false}and inspect the element in DevTools. - Read the classes on it: Velt applies dual names: a short legacy class (e.g.
selected) and a BEMvelt-*class (e.g.velt-comment-dialog--selected). Prefer thevelt-*class. - Write a rule for that class, ending each declaration with
!important:
Step 6 (optional): Start from a blank slate with unstyled mode
If you’re restyling most of the UI anyway, remove Velt’s visual styling entirely withsetUnstyledMode() (v6.0.0-beta.10+) and bring your own CSS. Layout and positioning styles are kept by default so components stay functional.
- React / Next.js
- Other Frameworks
What it can and can’t do
If you’re writing
display:none to remove parts, or wishing you could move a button, you’ve hit CSS’s ceiling: escalate to wireframes (restructure) or primitives (toggle features / compose).
Checklist
-
shadowDom={false}on customized components. - All Velt CSS in one stylesheet.
- Only
--velt-*/--legacy-velt-*names that exist inCSS variables. - Dark values under
:root[data-velt-theme="dark"]. - No
display:nonehacks to remove features (toggle with a prop instead).

