---
url: "https://vueda.dev/v3/guides/customize-vueda-appearance.md"
title: "Customize VUEDA Appearance"
---

# Customize VUEDA Appearance

The default theme gives every view a working look. Most views keep it. This guide covers the views and brands that need a change, from one element up to the whole app. [Theming and Customization](/v3/core-concepts/theming-and-customization.md) describes the four scopes and how their layers combine. [Style Unovis Charts](/v3/guides/style-unovis-charts.md) covers charts. Icons come from the [Icon Registry](/v3/reference/glossary.md#icon-registry), which is separate from the theme; [Register the Icons](/v3/guides/client-plugin-prerequisites.md#register-the-icons) sets it up.

## Pick the right scope

Use the smallest scope that covers the change. A broader scope changes screens that you did not check.

| Goal                                                                                          | Scope     | Mechanism                                    | Section                                                                 |
| --------------------------------------------------------------------------------------------- | --------- | -------------------------------------------- | ----------------------------------------------------------------------- |
| Make one element, or one part of a page, look different.                                      | Instance  | `themeOverride` prop                         | [Override one instance](/v3/guides/customize-vueda-appearance.md#override-one-instance)                         |
| Make every Button (or Input, or Dialog) look different across the app.                        | Component | `overrideTheme` on the component's theme key | [Restyle one component system-wide](/v3/guides/customize-vueda-appearance.md#restyle-one-component-system-wide) |
| Make every button-shaped thing (Button, calendar day cells, pagination items) look different. | Family    | `overrideTheme` on a composition primitive   | [Restyle a visual family](/v3/guides/customize-vueda-appearance.md#restyle-a-visual-family)                     |
| Re-skin the whole app: brand color, control sizes, radius, focus ring, shadows.               | Brand     | CSS token override                           | [Re-skin via tokens](/v3/guides/customize-vueda-appearance.md#re-skin-via-tokens)                               |

A change to a value, such as a color, a size, or a duration, belongs in a token. A change to which classes a component receives belongs in the JavaScript theme.

## Override one instance

Pass a [Theme Override](/v3/reference/glossary.md#theme-override) to the component's [`themeOverride`](/v3/reference/api/js/use/useTheme.md#THEME_OVERRIDE_PROPS) prop. The override is an object keyed by component name. It merges over the default and any project override for this component and for every component rendered inside it.

```vue
<template>
    <Button :theme-override="amberOverride">Save</Button>
</template>

<script setup>
import Button from "@vueda/controls/button/Button.vue";

const amberOverride = {
    Button: {
        root: {
            class: "bg-amber-500 text-white hover:bg-amber-600 active:bg-amber-700",
        },
    },
};
</script>
```

Other Buttons in the app keep their default theme.

The default theme's class merger keeps the later of two utilities that set the same CSS property under the same variant. Each class in this override therefore replaces the matching default class, such as the default background and hover background. Default classes that set other properties, or that apply under other variants such as `disabled:`, stay. To remove one of those, add its name as a key set to `false`. A bare `Button` composes [\_ButtonBase](/v3/reference/theming/keys/_ButtonBase.md) and [\_ButtonSecondary](/v3/reference/theming/keys/_ButtonSecondary.md), whose theme key pages list their classes. To use a built-in look instead, set the [`tone`](/v3/reference/api/vue/Button.md#prop-tone) and [`emphasis`](/v3/reference/api/vue/Button.md#prop-emphasis) props.

The component's `class` prop adds classes after the theme resolves, so a class there does not replace a theme class. [How the layers interact](/v3/core-concepts/theming-and-customization.md#how-the-layers-interact) describes the order.

A component passes its merged override to every descendant component. An override on a container therefore restyles components inside it, and the components in between need no extra props:

```vue
<template>
    <!-- Every Input inside this card uses the monospace font. -->
    <Card :theme-override="monoInputs">
        <slot />
    </Card>
</template>

<script setup>
import Card from "@vueda/shell/card/Card.vue";

const monoInputs = {
    Input: {
        root: { class: "font-mono" },
    },
};
</script>
```

## Restyle one component system-wide

Call [overrideTheme](/v3/reference/api/js/use/themeRegistry/functions/overrideTheme.md) in `main.js`. `overrideTheme` adds a project override, which the registry keeps apart from the registered defaults. The override applies whether the component's default registers before or after the call, including from a lazy route. Every instance of the component starts from the overridden version.

```js
// main.js
import "@vueda/theme/vueda-tailwind/index.js";
import { overrideTheme } from "@vueda/use/useTheme.js";

overrideTheme({
    Button: {
        root: { class: "uppercase tracking-wide" },
    },
});
```

The existing Button classes stay, and the override adds its own. With the default theme, an override class replaces a default class that sets the same CSS property. A later `overrideTheme` call combines its classes with those of earlier calls. [setTheme](/v3/reference/api/js/use/themeRegistry/functions/setTheme.md) replaces the registered defaults, and project overrides stay in effect. Reserve `setTheme` for installing a complete theme object. `clearThemeOverrides()` removes every project override. [How the theme is registered](/v3/core-concepts/theming-and-customization.md#how-the-theme-is-registered) describes the registration paths and when `patchTheme` applies.

A `themeOverride` on one instance still merges on top of the project override. The override reaches every component that renders a `Button`, such as `AlertDialogAction`. Calendar day cells and pagination items have their own theme keys, so the override does not reach them. To change them with Button, restyle the family.

## Restyle a visual family

To change Button, calendar day cells, pagination items, and other button-shaped components together, override the [Composition Primitive](/v3/reference/glossary.md#composition-primitive) that they share, such as `_ButtonBase` or `_ButtonGhost`:

```js
// main.js
import "@vueda/theme/vueda-tailwind/index.js";
import { overrideTheme } from "@vueda/use/useTheme.js";

overrideTheme({
    _ButtonGhost: {
        root: {
            class: "hover:bg-blue-100 dark:hover:bg-blue-900",
        },
    },
});
```

Every theme slot that lists `_ButtonGhost.root` in its `composes` array picks up the change: ghost Buttons, calendar day cells, and inactive pagination items. The override does not reach slots that do not list it.

A `composes` array in an override replaces the default array. To make a component build on a different primitive, override its `composes` array. This override makes the active pagination item use the primary fill:

```js
overrideTheme({
    PaginationItem: {
        root: ({ isActive }) => ({
            // The default composes _ButtonOutline for the active item and
            // _ButtonGhost for the others. This list replaces it.
            composes: ["_ButtonBase.root", isActive ? "_ButtonDefault.root" : "_ButtonGhost.root"],
        }),
    },
});
```

Override a primitive when the change is about how related components look together. Override a component's own key when the change is about that one component. The [theme keys reference](/v3/reference/theming/keys.md) lists every primitive and the components that compose it.

## Re-skin via tokens

Colors, radius, control heights, focus ring, shadows, sidebar widths, and calendar cell sizes come from [theme tokens](/v3/reference/glossary.md#theme-token). These are CSS custom properties that `@vueda/theme/vueda-tailwind/base.css` defines. Override the tokens in your own stylesheet, after the `base.css` import:

```css
/* src/index.css */
@import "tailwindcss";
@import "@vueda/theme/vueda-tailwind/base.css";

:root {
    --primary: oklch(0.6 0.15 180); /* teal fill in both modes */
    --vueda-control-height: 36px; /* taller controls */
    --vueda-control-radius: 6px; /* softer corners */
}
```

You change no JavaScript. Every Tailwind utility that reads an overridden token (`bg-primary`, `h-vueda-control`, `rounded-vueda-control`) uses the new value across the app. `base.css` sets the dark mode values under the `.dark` selector.

The primary color has several roles, each with its own token:

- [--primary](/v3/reference/theming/tokens/color-palette-light.md#css-token-primary): the fill of primary actions. The default is the same in light and dark mode.
- [--primary-foreground](/v3/reference/theming/tokens/color-palette-light.md#css-token-primary-foreground): dark labels on primary fills. Use `text-primary-foreground` for them.
- [--primary-text](/v3/reference/theming/tokens/color-palette-light.md#css-token-primary-text): primary-colored text, such as links, on the page, cards, and lightly tinted surfaces. Use `text-primary-text` for it.
- [--primary-text-active](/v3/reference/theming/tokens/color-palette-light.md#css-token-primary-text-active): the text of a pressed link.
- [--ring](/v3/reference/theming/tokens/color-palette-light.md#css-token-ring) and [--info](/v3/reference/theming/tokens/color-palette-light.md#css-token-info): the focus ring and informational color. By default both use `--primary-text`.

`base.css` computes the hover, pressed, and text colors from `--primary`, so they follow a new brand color. Check their contrast against the new color, and override any role that falls short.

[--vueda-brand-blue](/v3/reference/theming/tokens/color-palette-light.md#css-token-vueda-brand-blue), [--vueda-brand-navy](/v3/reference/theming/tokens/color-palette-light.md#css-token-vueda-brand-navy), and [--vueda-brand-grey](/v3/reference/theming/tokens/color-palette-light.md#css-token-vueda-brand-grey) define the identity palette. `base.css` computes the colors of supporting surfaces and body text by mixing navy and grey with white or black. Override the identity tokens to change the palette as a whole, or override single tokens to change one surface.

A selection of tokens, by group:

- **Color**: `--primary`, `--background`, `--foreground`, `--card`, `--muted`, `--accent`, `--destructive`, `--success`, `--warning`, `--info`, `--border`, `--input`, `--ring`. The sidebar has its own set under `--sidebar-*`.
- **Control sizing**: `--vueda-control-height`, `--vueda-control-height-sm`, `--vueda-control-height-lg`, `--vueda-control-px-*`.
- **Specialized sizes**: `--vueda-chip-height`, `--vueda-cmd-input-height`, `--vueda-cal-day`, `--vueda-cal-cell`, `--vueda-sidebar-width`.
- **Radius**: `--vueda-control-radius`, `--vueda-card-radius`, `--vueda-modal-radius`, `--vueda-pill-radius`.
- **Shadows**: `--vueda-shadow-control`, `--vueda-shadow-card`, `--vueda-shadow-popover`, `--vueda-shadow-overlay`.
- **Motion**: `--vueda-duration-interaction`, `--vueda-ease-interaction`.
- **Fonts**: [--vueda-font-sans](/v3/reference/theming/tokens/typography.md#css-token-vueda-font-sans) and [--vueda-font-mono](/v3/reference/theming/tokens/typography.md#css-token-vueda-font-mono). See [Load or replace the fonts](/v3/guides/customize-vueda-appearance.md#load-or-replace-the-fonts).

The [theme tokens reference](/v3/reference/theming/tokens.md) lists every token with its light and dark values.

### Load or replace the fonts

`base.css` names fonts but ships no font files. The default stacks ask for IBM Plex Sans for interface text and JetBrains Mono for machine-generated, positional, and digit-stable values. The theme uses weights 400 for body text, 500 for controls ([--vueda-font-weight-ui](/v3/reference/theming/tokens/typography.md#css-token-vueda-font-weight-ui)), and 600 for labels ([--vueda-font-weight-label](/v3/reference/theming/tokens/typography.md#css-token-vueda-font-weight-label)). The application loads the fonts, so that it can use fonts that it already loads for its own pages.

When no loaded face matches a family name, the browser uses a system font without an error. Text widths then change: column headers wrap, chips grow, and dense layouts no longer match the component reference.

A project generated from the VUEDA templates loads both families in `src/index.css`. The [Fontsource](https://fontsource.org/) variable packages `@fontsource-variable/ibm-plex-sans` and `@fontsource-variable/jetbrains-mono` register the families as `IBM Plex Sans Variable` and `JetBrains Mono Variable`, so the stylesheet puts those names first in each stack:

```css
/* src/index.css */
@import "tailwindcss";
@import "@vueda/theme/vueda-tailwind/base.css";
@import "@fontsource-variable/ibm-plex-sans";
@import "@fontsource-variable/jetbrains-mono";

:root {
    --vueda-font-sans: "IBM Plex Sans Variable", "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif;
    --vueda-font-mono: "JetBrains Mono Variable", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}
```

In another project, install both packages as dependencies and add the same lines. If you replace the default theme, remove the font imports and the `:root` rule along with the `base.css` import.

To use other fonts, load them and override the two stacks after the `base.css` import. Name each family exactly as its `@font-face` rule registers it:

```css
:root {
    --vueda-font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif;
    --vueda-font-mono: "JetBrains Mono Variable", ui-monospace, monospace;
}
```

A different font has different text widths, so screens no longer match the component reference exactly. Keep the default fonts when a close match to the reference matters more than the brand.

To confirm that a family loaded, run `await document.fonts.load('600 16px "IBM Plex Sans Variable"')` in the browser console, with your family name in place of `IBM Plex Sans Variable`. The result is an empty array when no loaded font face has that name.

## Port a shadcn-vue block

[shadcn-vue](https://www.shadcn-vue.com/) blocks, such as sidebar layouts and dashboard shells, do not install into a VUEDA project. A block imports components from `@/components/ui/...` paths and uses the `cn()` helper, and VUEDA provides neither. Most VUEDA components carry the shadcn-vue names (`Button`, `Dialog`, `Alert`, `Pagination`), so a block's markup needs few changes.

To port a block:

1. Copy the block's markup and script into a component in your project.
2. Replace each `@/components/ui/...` import with an import of the VUEDA component file. The Source section of the component's API page gives the file path under `client/lib/`; replace that prefix with `@vueda/`. For example, `Button` is `@vueda/controls/button/Button.vue`.
3. Check each component's props on its API page. For example, a VUEDA `Button` takes `tone` and `emphasis`, and a shadcn-vue `Button` takes `variant`.
4. Remove the `cn()` calls. Move each class that restyles a VUEDA component into a `themeOverride`, an `overrideTheme` call, or a token, as described in the sections above.

## Common pitfalls

**Using `setTheme` to change a value.** `setTheme` replaces every registered default, so a theme object that leaves out an already loaded component removes that component's default classes. A color, a size, or a duration belongs in a token, as in [Re-skin via tokens](/v3/guides/customize-vueda-appearance.md#re-skin-via-tokens).

**Using `themeOverride` for a system-wide change.** A `themeOverride` prop changes one component and its descendants. For a change everywhere the component appears, use `overrideTheme`, as in [Restyle one component system-wide](/v3/guides/customize-vueda-appearance.md#restyle-one-component-system-wide).

**Overriding a component's key for a family-wide change.** An override of `Button` does not reach `CalendarCellTrigger` or `PaginationItem`, which have their own theme keys. When the change applies to every button-shaped component, override the composition primitive that they share, such as `_ButtonBase` or `_ButtonGhost`.

**Drawing app chrome with a plain `border`.** VUEDA draws edges with the [Hairline](/v3/reference/glossary.md#hairline). Its width comes from the [`--vueda-hairline-width`](/v3/reference/theming/tokens/base.md#css-token-vueda-hairline-width) token. The token steps from 2px at a device pixel ratio of 1 down to 1px at a ratio of 2. That scale avoids color fringing on common office displays.

A Tailwind `border` or `border-b` stays at 1px, so a header drawn with it looks thinner than the VUEDA surfaces beside it. Use the matching utilities from `base.css` instead:

- `border-b-hairline` (or `-t`, `-l`, `-r`, `-x`, `-y`) for one edge.
- `border-hairline` for four real border sides.
- `hairline hairline-border` for a four-sided edge on an element with no other box-shadow.

**Naming a font that the page does not load.** The token stacks name families; they do not load them. A stack whose first family has no loaded face falls back to a system font without any warning. Load the fonts or override the stacks, as in [Load or replace the fonts](/v3/guides/customize-vueda-appearance.md#load-or-replace-the-fonts).

**Expecting `composes` to extend the default list.** A `composes` array in an override replaces the default array. To add a primitive, write the full list, as in [Restyle a visual family](/v3/guides/customize-vueda-appearance.md#restyle-a-visual-family).
