---
title: "Customize components"
description: "Learn how to customize Pohon UI components with the UnoCSS Variants API for advanced, flexible, and maintainable styling."
canonical_url: "https://vinicunca.dev/pohon/getting-started/theme/components"
---
# Customize components

> Learn how to customize Pohon UI components with the UnoCSS Variants API for advanced, flexible, and maintainable styling.

## UnoCSS Variants

Pohon UI components are styled using the [UnoCSS Variants](https://vinicunca.dev/unocss-variants/) API, which provides a powerful way to create variants and manage component styles.

### Slots

Components can have multiple `slots`, each representing a distinct HTML element or section within the component. These slots allow for flexible content insertion and styling.

Let's take the [Card](https://vinicunca.dev/pohon/components/card) component as an example which has multiple slots:

```ts [src/theme/card.ts]
export default {
  slots: {
    root: 'rounded-lg overflow-hidden',
    header: 'p-4 sm:px-6',
    title: 'color-text-highlighted font-semibold',
    description: 'mt-1 color-text-muted text-sm',
    body: 'p-4 sm:p-6',
    footer: 'p-4 sm:px-6'
  }
};
```

```vue [src/runtime/components/Card.vue]
<template>
  <Primitive
    :as="props.as"
    data-slot="root"
    :class="ui.root({ class: [props.ui?.root, props.class] })"
  >
    <div
      data-slot="header"
      :class="ui.header({ class: props.ui?.header })"
    >
      <slot name="header" />
    </div>

    <div
      data-slot="body"
      :class="ui.body({ class: props.ui?.body })"
    >
      <slot />
    </div>

    <div
      data-slot="footer"
      :class="ui.footer({ class: props.ui?.footer })"
    >
      <slot name="footer" />
    </div>
  </Primitive>
</template>
```

> [!NOTE]
> 
> The snippets above are abridged. The real components also carry `v-if` guards on each slot and the theme defines `variant` styles on top of the slots.

Some components don't have slots, they are just composed of a single root element. In this case, the theme only defines the `base` slot like the [Container](https://vinicunca.dev/pohon/components/container) component for example:

```ts [src/theme/container.ts]
export default {
  base: 'w-full max-w-(--ui-container) mx-auto px-4 sm:px-6 lg:px-8'
};
```

```vue [src/runtime/components/Container.vue]
<template>
  <Primitive
    :as="props.as"
    :class="ui({ class: [props.ui?.base, props.class] })"
  >
    <slot />
  </Primitive>
</template>
```

> [!NOTE]
> 
> Components without slots still have a [`ui` prop](#ui-prop). It exposes a single `base` key instead of one key per slot. [Link](https://vinicunca.dev/pohon/components/link) and [Icon](https://vinicunca.dev/pohon/components/icon) are the exceptions: they have no `ui` prop, so use the [`class` prop](#class-prop) to override their styles.

### Variants

Components support `variants`, which dynamically adjust the styles of different `slots` based on component props.

For example, the [Avatar](https://vinicunca.dev/pohon/components/avatar) component uses a `size` variant to control its appearance:

```ts [src/theme/avatar.ts]
export default {
  slots: {
    root: 'inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-background-elevated',
    image: 'h-full w-full rounded-[inherit] object-cover'
  },
  variants: {
    size: {
      sm: {
        root: 'size-7 text-sm'
      },
      md: {
        root: 'size-8 text-base'
      },
      lg: {
        root: 'size-9 text-lg'
      }
    }
  },
  defaultVariants: {
    size: 'md'
  }
};
```

This way, the `size` prop will apply the corresponding styles to the `root` slot:

```vue
<template>
  <PAvatar src="https://github.com/nuxt.png" size="lg" />
</template>
```

### Default Variants

The `defaultVariants` property sets the default value for each variant when no prop is passed.

For example, the [Avatar](https://vinicunca.dev/pohon/components/avatar) component has its default size set to `md`:

```ts [src/theme/avatar.ts]
export default {
  slots: {
    root: 'inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-background-elevated',
    image: 'h-full w-full rounded-[inherit] object-cover'
  },
  variants: {
    size: {
      sm: {
        root: 'size-7 text-sm'
      },
      md: {
        root: 'size-8 text-base'
      },
      lg: {
        root: 'size-9 text-lg'
      }
    }
  },
  defaultVariants: {
    size: 'md'
  }
};
```

**Nuxt:**

> [!TIP]
> See: /pohon/getting-started/installation/nuxt#themedefaultvariants
> 
> You can use the `theme.defaultVariants` option in your `nuxt.config.ts` to override the default `size` and `color` of every component that uses `md` and `primary`.

**Vue:**

> [!TIP]
> See: /pohon/getting-started/installation/vue#themedefaultvariants
> 
> You can use the `theme.defaultVariants` option in your `vite.config.ts` to override the default `size` and `color` of every component that uses `md` and `primary`.

### Compound Variants

Some components use the `compoundVariants` property to apply classes when multiple variant conditions are met at the same time.

For example, the [Button](https://vinicunca.dev/pohon/components/button) component uses the `compoundVariants` property to apply classes for a specific `color` and `variant` combination:

```ts [src/theme/button.ts]
import type { ModuleOptions } from '../module';

export default (options: Required<ModuleOptions>) => ({
  slots: {
    base: ['rounded-md font-medium inline-flex items-center disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75', options.theme.transitions && 'transition-colors']
  },
  variants: {
    color: {
      ...Object.fromEntries((options.theme.colors || []).map((color: string) => [color, ''])),
      neutral: ''
    },
    variant: {
      solid: '',
      outline: '',
      soft: '',
      subtle: '',
      ghost: '',
      link: ''
    }
  },
  compoundVariants: [
    ...(options.theme.colors || []).map((color: string) => ({
      color,
      variant: 'outline',
      class: `ring ring-inset ring-${color}/50 color-${color} hover:bg-${color}/10 active:bg-${color}/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent outline-${color}/25 focus-visible:outline-3 focus-visible:ring-${color}`
    })),
    {
      color: 'neutral',
      variant: 'outline',
      class: 'ring ring-inset ring-border-accented color-text bg-background hover:bg-background-elevated active:bg-background-elevated disabled:bg-background aria-disabled:bg-background outline-border-inverted/25 focus-visible:outline-3 focus-visible:ring-border-inverted'
    }
  ],
  defaultVariants: {
    color: 'primary',
    variant: 'solid'
  }
});
```

## Customize theme

You have multiple ways to customize the appearance of Pohon UI components, you can do it for all components at once or on a per-component basis.

> [!NOTE]
> 
> Use the component `ui` prop to override its slot classes. The generated theme shows the available slots and variants.

> [!TIP]
> 
> You can explore the theme for each component in two ways:
> 
> - Check the `Theme` section in the documentation of each individual component.
> - Browse the source code directly in the GitHub repository at [`src/theme`](https://github.com/vinicunca/pohon/tree/main/src/theme).

### Class conflicts

Pohon UI doesn't merge classes. When your class sets the same property as a default class, for example `font-bold` and `font-medium`, both stay on the element. The CSS cascade decides which one applies, not the order of the classes in the `class` attribute.

To make your class win, put it in a [CSS layer](https://unocss.dev/config/layers) that comes after the component defaults. Declare the layers in your `uno.config.ts`. The `pohon` layer is used by the Pohon UI theme itself, and `p-variant` is the layer for your overrides:

```ts [uno.config.ts]
import { presetVinicunca } from '@vinicunca/unocss-preset';
import { pohonColors } from 'pohon-ui/unocss';
import { defineConfig } from 'unocss';

export default defineConfig({
  layers: {
    'pohon': 100,
    'p-variant': 200
  },
  outputToCssLayers: {
    allLayers: true
  },
  presets: [
    presetVinicunca()
  ],
  theme: { colors: pohonColors },
  variants: [
    // The Pohon UI theme writes `pohon:` for `uno-layer-pohon:`
    (matcher) => {
      if (matcher.startsWith('pohon:')) {
        return {
          matcher: matcher.replace('pohon:', 'uno-layer-pohon:')
        };
      }
    }
  ]
});
```

Then prefix each class that must win with `uno-layer-p-variant:`:

```vue
<template>
  <PButton
    class="uno-layer-p-variant:font-bold"
    label="Button"
  />
</template>
```

Only prefix the classes that conflict with a default. A class that sets a property the component doesn't set, like `rotate-90` on an icon, works without it.

> [!TIP]
> 
> To write `p-variant:font-bold` instead, add a shorthand next to the `pohon:` one:
> 
> ```ts [uno.config.ts]
> export default defineConfig({
>   variants: [
>     (matcher) => {
>       for (const layer of ['pohon', 'p-variant']) {
>         if (matcher.startsWith(`${layer}:`)) {
>           return {
>             matcher: matcher.replace(`${layer}:`, `uno-layer-${layer}:`)
>           };
>         }
>       }
>     }
>   ]
> });
> ```

> [!NOTE]
> 
> With `outputToCssLayers`, every UnoCSS layer becomes a native `@layer`, so the layer order decides the winner even when the default class has a higher specificity, like `hover:` or `dark:` classes. CSS you write outside of any `@layer` in your `main.css` still wins over all of them.

To replace all the classes of a slot instead of overriding them one by one, set the slot to a function. See [Global config](#global-config) and the [`ui` prop](#ui-prop).

### Global config

**Nuxt:**

You can override the theme of components globally inside your `app.config.ts` by using the exact same structure as the theme object.

**Vue:**

You can override the theme of components globally inside your `vite.config.ts` by using the exact same structure as the theme object.

You can customize the [`slots`](#slots), [`variants`](#variants), [`compoundVariants`](#compound-variants) and [`defaultVariants`](#default-variants) of a component to change the default theme of a component:

**Nuxt:**

```ts [app/app.config.ts]
export default defineAppConfig({
  ui: {
    button: {
      slots: {
        base: 'uno-layer-p-variant:font-bold'
      },
      variants: {
        size: {
          md: {
            leadingIcon: 'uno-layer-p-variant:size-4'
          }
        }
      },
      compoundVariants: [
        {
          color: 'neutral',
          variant: 'outline',
          class: 'uno-layer-p-variant:ring-border uno-layer-p-variant:hover:bg-background-accented'
        }
      ],
      defaultVariants: {
        color: 'neutral',
        variant: 'outline'
      }
    }
  }
});
```

**Vue:**

```ts [vite.config.ts]
import vue from '@vitejs/plugin-vue';
import ui from 'pohon-ui/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        button: {
          slots: {
            base: 'uno-layer-p-variant:font-bold'
          },
          variants: {
            size: {
              md: {
                leadingIcon: 'uno-layer-p-variant:size-4'
              }
            }
          },
          compoundVariants: [
            {
              color: 'neutral',
              variant: 'outline',
              class: 'uno-layer-p-variant:ring-border uno-layer-p-variant:hover:bg-background-accented'
            }
          ],
          defaultVariants: {
            color: 'neutral',
            variant: 'outline'
          }
        }
      }
    })
  ]
});
```

> [!NOTE]
> 
> In this example, `font-bold` wins over the default font weight on all buttons, `size-4` wins over the `size-5` class on the leading icon when `size="md"` and `ring-border hover:bg-background-accented` win over `ring-border-accented hover:bg-background-elevated` when `color="neutral"` and `variant="outline"`. They win because of the `uno-layer-p-variant:` prefix, see [Class conflicts](#class-conflicts). The buttons now default to `color="neutral"` and `variant="outline"`.

By default, these classes are added next to the component's defaults. You can also set a slot to a function to replace them instead. It receives the slot's default classes as its argument, so you can reuse part of them. Classes from `variants` and `compoundVariants` still apply on top, exactly like when you pass a string.

**Nuxt:**

```ts [app/app.config.ts]
export default defineAppConfig({
  ui: {
    button: {
      slots: {
        label: () => 'text-base font-bold'
      }
    }
  }
});
```

**Vue:**

```ts [vite.config.ts]
import vue from '@vitejs/plugin-vue';
import ui from 'pohon-ui/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        button: {
          slots: {
            label: () => 'text-base font-bold'
          }
        }
      }
    })
  ]
});
```

**Nuxt:**

> [!WARNING]
> 
> Set a slot to a function in one place only. Nuxt merges the `app.config.ts` of every [layer](https://nuxt.com/docs/getting-started/layers) with `defuFn`, which calls your function with whatever value the key already has lower down and keeps only the result. Pohon UI then reads a plain string, so its classes are added next to the defaults instead of replacing them.

**Nuxt:**

> [!TIP]
> See: /pohon/getting-started/installation/nuxt#themeunstyled
> 
> To remove the default classes from all components at once, use the `theme.unstyled` option in your `nuxt.config.ts`.

**Vue:**

> [!TIP]
> See: /pohon/getting-started/installation/vue#themeunstyled
> 
> To remove the default classes from all components at once, use the `theme.unstyled` option in your `vite.config.ts`.

### Theme component

The [Theme](https://vinicunca.dev/pohon/components/theme) component overrides slots and prop defaults for all of its descendants, without affecting the rest of the app. Like the global config, its classes are added next to the defaults, so a class that conflicts with a default needs a [layer](#class-conflicts). Slots accept the same function form to replace their classes instead. It is resolved at the same point as the [`ui` prop](#ui-prop), so it behaves the same way. A component's own `ui` prop replaces the Theme component's classes for the same slot.

```vue [ThemeUiExample.vue]
<template>
  <PTheme
    :ui="{
      button: {
        base: 'uno-layer-p-variant:rounded-full',
      },
    }"
  >
    <div class="flex gap-2 items-center">
      <PButton
        label="Button"
        color="neutral"
      />
      <PButton
        label="Button"
        color="neutral"
        variant="outline"
      />
      <PButton
        label="Button"
        color="neutral"
        variant="subtle"
      />
    </div>
  </PTheme>
</template>
```

### `ui` prop

You can also override a component's **slots** using the `ui` prop. Its classes are added next to the classes from the global config and the resolved `variants`, so a class that conflicts with them needs a [layer](#class-conflicts).

```vue
<template>
  <PButton trailing-icon="i-lucide-chevron-right" size="md" color="neutral" variant="outline" :ui="{
  trailingIcon: 'rotate-90 uno-layer-p-variant:size-3'
}">
    Button
  </PButton>
</template>
```

> [!NOTE]
> 
> In this example, `size-3` wins over the `size-5` class the `md` size variant applies to the `trailingIcon` slot. `rotate-90` needs no prefix because nothing else sets a rotation on the icon.

A slot value can also be a function to replace its classes instead of adding to them:

```vue
<template>
  <PButton
    :ui="{ label: () => 'text-base font-bold' }"
    label="Button"
  />
</template>
```

> [!NOTE]
> 
> The `ui` prop is resolved after the `variants`, so unlike in the global config, a function here replaces the slot's **resolved** classes. Its argument contains what the `variants` and `compoundVariants` matched. A `class` prop on the same slot is still added next to the returned classes.

### `class` prop

Use the `class` prop to override the classes of the `root` or `base` slot. Like the `ui` prop, its classes are added next to the classes from the global config and the resolved `variants`, so a class that conflicts with them needs a [layer](#class-conflicts).

```vue
<template>
  <PButton class="uno-layer-p-variant:font-bold uno-layer-p-variant:rounded-full">
    Button
  </PButton>
</template>
```

> [!NOTE]
> 
> In this example, both classes need the prefix because the button already sets a font weight and a border radius.


## Sitemap

See the full [sitemap](https://vinicunca.dev/sitemap.md) for all pages.
