> ## Documentation Index
> Fetch the complete documentation index at: https://velt.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# CSS

> Theme Velt UI with CSS variables, dark mode tokens, and supported class overrides.

Change Velt's look (colors, spacing, fonts, radius, shadows) without touching its structure or behavior. It's the lowest-effort approach, and it layers on top of every other one.

<Info>
  Two shortcuts before you write any CSS: build a theme visually in the [Theme Playground](https://playground.velt.dev/themes), or have the [UI Customization Plugin](/docs/get-started/ui-customization-plugin) extract the values from your Figma design and write the overrides for you.
</Info>

<Warning>
  `className` and `style` props on Velt components do nothing. Style Velt with the variables and classes below, or add classes to your own markup inside a [wireframe](/docs/ui-customization/layout).
</Warning>

## Make your CSS reach Velt

Velt can render inside a shadow DOM, which blocks your stylesheets.

* **CSS variables** (`--velt-*`) always cross the boundary. Theming with variables alone needs nothing here.
* **Class and element selectors** don't. Pick one of these:

**Option A: turn shadow DOM off.**

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    <VeltComments shadowDom={false} />
    <VeltCommentsSidebar shadowDom={false} />
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```html theme={null}
    <velt-comments shadow-dom="false"></velt-comments>
    <velt-comments-sidebar shadow-dom="false"></velt-comments-sidebar>
    ```
  </Tab>
</Tabs>

**Option B: keep it on and inject your CSS into the shadow root.** Use `type: "styles"` for a CSS string, `type: "link"` for a stylesheet URL.

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    const { client } = useVeltClient();
    client.injectCustomCss({ type: "styles", value: `
      .velt-comment-dialog-composer { border-radius: 10px !important; }
    ` });
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```js theme={null}
    Velt.injectCustomCss({ type: "styles", value: `
      .velt-comment-dialog-composer { border-radius: 10px !important; }
    ` });
    ```
  </Tab>
</Tabs>

<Note>
  **Wireframes change this.** Registering a component's **root** wireframe (e.g. `VeltCommentDialogWireframe`) removes that component's shadow DOM automatically, so your class CSS reaches it. A **nested-only** wireframe doesn't: set `shadowDom={false}` yourself. Inline `style=""` always works either way.

  Not every component takes `shadowDom`: the per-component prop lists are in [`Props`](/docs/ui-customization/reference/props).
</Note>

## Theme with variables

Put all Velt CSS in one stylesheet and override the tokens you need:

```css theme={null}
/* velt.css */
:root {
  --velt-light-mode-accent: #FF6B35;
  --velt-light-mode-accent-hover: #E55A29;
  --velt-light-mode-background-0: #FFFFFF;
  --velt-light-mode-text-0: #0A0A0A;

  --velt-border-radius-md: 10px;
  --velt-spacing-md: 14px;
  --velt-font-size-sm: 13px;
}
```

Every token is listed in [`CSS variables`](/docs/ui-customization/reference/css-variables); don't invent names. Older surfaces read a few `--legacy-velt-*` tokens, which are listed there too.

<Tip>
  Rather than hand-picking values, build your theme in the [Theme Playground](https://playground.velt.dev/themes): adjust colors, radius, spacing, and typography against a live preview, then copy the generated variables straight into this stylesheet.
</Tip>

## Dark mode

Velt sets `data-velt-theme="dark"` on the document root when dark mode is on. You supply the values:

```css theme={null}
:root[data-velt-theme="dark"] {
  --velt-dark-mode-accent: #FF8A5C;
  --velt-dark-mode-background-0: #0F0F0F;
  --velt-dark-mode-text-0: #FFFFFF;
}
```

Turn it on with the `darkMode` prop (also `dialogDarkMode`, `pinDarkMode`, … for components Velt injects for you), with `setDarkMode()` app-wide, or by wiring your own `prefers-color-scheme` listener to it:

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    // Global
    const { client } = useVeltClient();
    client.setDarkMode(true);

    // Per component
    <VeltComments darkMode={true} dialogDarkMode={true} pinDarkMode={true} />
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```html theme={null}
    <script>
      Velt.setDarkMode(true);
    </script>

    <velt-comments dark-mode="true" dialog-dark-mode="true" pin-dark-mode="true"></velt-comments>
    ```
  </Tab>
</Tabs>

## Fonts

One global token sets the font across every Velt surface:

```css theme={null}
:root {
  --velt-default-font-family: "Inter", "Helvetica Neue", Arial, sans-serif;
}
```

Font sizes use the `--velt-font-size-*` scale. Line-height and weight are per-component.

## Override classes

For anything variables don't cover, target Velt's classes. **Velt's own styles are high-specificity, so your overrides need `!important`.** That's the supported way to do class-based Velt CSS, not a hack.

1. Run with `shadowDom={false}` and inspect the element.
2. Prefer its `velt-*` BEM class over the short legacy twin: `velt-comment-dialog--selected`, not `selected`.
3. Write the rule with `!important`:

```css theme={null}
.velt-composer--submit-button {
  background: #3d5afe !important;
  border-radius: 6px !important;
}
```

<Tip>
  [`CSS classes`](/docs/ui-customization/reference/css-classes) lists every structural and stateful class (unread, resolved, selected, hover, filter-applied, …). Class names can shift between versions, so prefer a `--velt-*` variable where one exists and re-check overrides on upgrade.
</Tip>

## Unstyled mode

Restyling most of the UI anyway? Strip Velt's visual styling with [`setUnstyledMode()`](/docs/api-reference/sdk/api/api-methods#setunstyledmode) (v6.0.0-beta.10+) and bring your own CSS. Layout and positioning styles are kept so components stay functional. It covers styles in the page head and inside shadow roots, and is reversible.

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    const { client } = useVeltClient();

    client.setUnstyledMode(true);                                    // keep layout styles
    client.setUnstyledMode(true, { keepFunctionalStyles: false });   // strip everything
    client.setUnstyledMode(false);                                   // restore
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```js theme={null}
    Velt.setUnstyledMode(true);
    Velt.setUnstyledMode(true, { keepFunctionalStyles: false });
    Velt.setUnstyledMode(false);
    ```
  </Tab>
</Tabs>

To also drop Velt's global styles (the ones outside its own components), set `globalStyles: false` in your config:

<Tabs>
  <Tab title="React / Next.js">
    ```jsx theme={null}
    <VeltProvider apiKey='API_KEY' config={{ globalStyles: false }}>
        {/* Your app content */}
    </VeltProvider>
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```js theme={null}
    Velt.initConfig('API_KEY', { globalStyles: false });
    ```
  </Tab>
</Tabs>

## Recipes

Specific selector tricks for things variables don't reach. All of these assume `shadowDom={false}` and use `!important` for the reason above.

<AccordionGroup>
  <Accordion title="Map your brand tokens onto Velt's">
    Define your brand once as your own variables, then point the `--velt-*` tokens at them. One place to change your brand, and Velt stays in sync with your app.

    ```css theme={null}
    :root {
      --brand-accent: #FF6B35;
      --velt-light-mode-accent: var(--brand-accent);
      --velt-light-mode-accent-hover: color-mix(in srgb, var(--brand-accent) 85%, black);
    }
    ```
  </Accordion>

  <Accordion title="Reveal actions only on hover">
    Hide the kebab, options, and reaction icons until hover. Velt already wires this on `.velt-thread-card--container:hover`, and you can force it with `.velt-thread-card--show-actions`.

    ```css theme={null}
    .hw-comment-actions { opacity: 0; transition: opacity .12s ease; }
    .hw-comment:hover .hw-comment-actions,
    .hw-comment-actions:focus-within { opacity: 1; }
    ```

    There is no hover on touch devices. Velt force-shows per-comment actions at mobile widths, so if you build your own hover-reveal add an always-visible fallback under a mobile media query.
  </Accordion>

  <Accordion title="Swap the reaction tool and reaction panel by count">
    The pin carries `velt-reaction-pin--no-reactions` only when the count is 0. Use it, and `:has(app-reaction-pin)`, to position or swap the reaction UI.

    ```css theme={null}
    .velt-thread-card--reactions:not(:has(app-reaction-pin)) { display: none !important; }
    ```
  </Accordion>

  <Accordion title="Style the unread dot">
    ```css theme={null}
    .velt-thread-card--name--unread {
      width:6px !important; height:6px !important; border-radius:50% !important;
      background: var(--brand) !important; margin-left:0 !important;
    }
    ```
  </Accordion>

  <Accordion title="Style resolved threads">
    There is no `--resolved` class. Detect the unresolve button instead.

    ```css theme={null}
    .velt-comment-dialog--sidebar-mode:has(velt-comment-dialog-unresolve-button-internal .icon)
      .velt-thread-card--message { color: var(--muted) !important; }
    ```
  </Accordion>

  <Accordion title="Hide a container when its data slot is empty">
    ```css theme={null}
    .context-row:has(app-data:empty) { display:none !important; }
    app-if:empty { display:none !important; }   /* unwrap empty conditionals */
    ```
  </Accordion>

  <Accordion title="Resize the default avatar">
    ```css theme={null}
    snippyly-user-avatar {
      --legacy-velt-user-avatar-height:20px !important;
      --legacy-velt-user-avatar-width:20px !important;
    }
    ```
  </Accordion>

  <Accordion title="Indent threaded replies">
    ```css theme={null}
    app-comment-dialog-threads > :not(:first-child) { padding-left: 26px; }
    ```
  </Accordion>

  <Accordion title="Neutralize an SDK popover so your menu takes over">
    ```css theme={null}
    velt-comment-dialog-options-dropdown-content-internal,
    div:has(> velt-comment-dialog-options-dropdown-content-internal) {
      background:transparent !important; box-shadow:none !important;
      border:none !important; padding:0 !important; width:max-content !important;
    }
    ```
  </Accordion>

  <Accordion title="Remove default styling a wireframe didn't replace">
    Wireframing a slot doesn't strip all of Velt's surrounding defaults: borders, padding, backgrounds, fixed widths, and popover chrome often remain. Inspect, find the `velt-*` class, override it.

    ```css theme={null}
    .velt-status-dropdown--content {
      border:none !important; background:transparent !important;
      box-shadow:none !important; padding:0 !important;
    }
    ```

    Treat "inspect, find class, override" as part of every wireframe pass, not a failure.
  </Accordion>

  <Accordion title="Switch a collapsed composer to expanded">
    Render a collapsed input and an expanded one in the composer slot, and let Velt's state classes switch between them.

    ```css theme={null}
    .velt-composer-open .composer-collapsed { display:none !important; }
    .velt-composer-open .composer-expanded  { display:flex !important; }
    ```

    The switches are `velt-composer-open`, `velt-comment-dialog--no-comments`, and `velt-composer-edit-mode`.
  </Accordion>

  <Accordion title="Make a sidebar or list take the available height and scroll">
    The tricky one. Velt's **own internal container elements** keep their default styles and sit *between* your layout and the scrollable list. If any link in that flex chain lacks `min-height:0`, `flex:1`, or `height:100%`, the list grows past the panel and scrolling silently breaks. Inspect the Velt internal element and force the chain.

    ```css theme={null}
    .my-panel { display:flex; flex-direction:column; height:100%; min-height:0; }
    /* Velt's internal panel element: the hidden link you find by inspecting */
    .my-panel > app-comment-sidebar-panel { display:flex; flex-direction:column; flex:1 1 auto; min-height:0; }
    .my-list-body { flex:1; min-height:0; overflow-y:auto; }
    /* sometimes Velt's own list viewport needs forcing too: */
    velt-comments-sidebar /* …inspect for the exact internal el… */ { height:100% !important; min-height:0 !important; overflow-y:auto !important; }
    ```

    `min-height:0` (plus `flex:1` or `height:100%`) must hold on **every** element from your wrapper down to the scroll container, including Velt's internal ones. One missing link kills the scroll, so re-test that scrolling actually works.
  </Accordion>

  <Accordion title="Fix a dialog or pin rendering in the wrong place">
    Some Velt pieces (dialogs, pins, overlays, reaction pins) render with `position: absolute`. If a mounted primitive or wireframe appears top-left or escaping its box, give its **parent** `position: relative` so the absolute child anchors to it.

    ```css theme={null}
    .my-comment-dialog-host { position: relative; }
    ```

    The most common "why is my dialog floating in the wrong spot?" fix.
  </Accordion>
</AccordionGroup>

## What CSS can and can't do

| ✅ CSS can                              | ❌ CSS cannot                                     |
| -------------------------------------- | ------------------------------------------------ |
| Recolor everything (light + dark)      | Reorder or restructure the UI                    |
| Change spacing, radius, fonts, shadows | Add or remove UI parts (use a prop or wireframe) |
| Theme to match a brand                 | Insert your own markup between Velt's pieces     |
| Adjust z-index layering                | Change behavior                                  |

Writing `display:none` to remove parts, or wishing you could move a button? You've hit CSS's ceiling: escalate to [wireframes](/docs/ui-customization/layout) to restructure, or [primitives](/docs/ui-customization/primitives) to toggle features.

## Troubleshooting

Common CSS symptoms, in [`Debugging`](/docs/ui-customization/debugging):

* ["My CSS does nothing"](/docs/ui-customization/debugging#my-css-does-nothing)
* ["Dark mode colors are wrong"](/docs/ui-customization/debugging#dark-mode-colors-are-wrong)
* ["Default styling is still there even though I wireframed it"](/docs/ui-customization/debugging#default-styling-is-still-there-even-though-i-wireframed-it)
* ["My sidebar or list won't scroll"](/docs/ui-customization/debugging#my-sidebar-or-list-wont-scroll-or-wont-take-the-available-height)
* ["My dialog, pin, or primitive renders in the wrong place"](/docs/ui-customization/debugging#my-dialog-pin-or-primitive-renders-in-the-wrong-place-top-left-escaping-its-box)

## Checklist

* [ ] `shadowDom={false}` on components you style with classes.
* [ ] All Velt CSS in **one** stylesheet.
* [ ] Only token names that exist in [`CSS variables`](/docs/ui-customization/reference/css-variables).
* [ ] Dark values under `:root[data-velt-theme="dark"]`.
* [ ] No `display:none` to remove features: toggle them with a prop.
