Customize components
UnoCSS Variants
Pohon UI components are styled using the 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 component as an example which has multiple slots:
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'
}
};
<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>
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 component for example:
export default {
base: 'w-full max-w-(--ui-container) mx-auto px-4 sm:px-6 lg:px-8'
};
<template>
<Primitive
:as="props.as"
:class="ui({ class: [props.ui?.base, props.class] })"
>
<slot />
</Primitive>
</template>
ui prop. It exposes a single base key instead of one key per slot. Link and Icon are the exceptions: they have no ui prop, so use the 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 component uses a size variant to control its appearance:
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:
<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 component has its default size set to md:
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'
}
};
theme.defaultVariants option in your nuxt.config.ts to override the default size and color of every component that uses md and primary.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 component uses the compoundVariants property to apply classes for a specific color and variant combination:
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.
ui prop to override its slot classes. The generated theme shows the available slots and variants.- Check the
Themesection in the documentation of each individual component. - Browse the source code directly in the GitHub repository at
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 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:
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::
<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.
p-variant:font-bold instead, add a shorthand next to the pohon: one:export default defineConfig({
variants: [
(matcher) => {
for (const layer of ['pohon', 'p-variant']) {
if (matcher.startsWith(`${layer}:`)) {
return {
matcher: matcher.replace(`${layer}:`, `uno-layer-${layer}:`)
};
}
}
}
]
});
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 and the ui prop.
Global config
You can override the theme of components globally inside your app.config.ts by using the exact same structure as the theme object.
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, variants, compoundVariants and defaultVariants of a component to change the default theme of a component:
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'
}
}
}
});
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'
}
}
}
})
]
});
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. 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.
export default defineAppConfig({
ui: {
button: {
slots: {
label: () => 'text-base font-bold'
}
}
}
});
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'
}
}
}
})
]
});
app.config.ts of every layer 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.theme.unstyled option in your nuxt.config.ts.theme.unstyled option in your vite.config.ts.Theme component
The 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. Slots accept the same function form to replace their classes instead. It is resolved at the same point as the ui prop, so it behaves the same way. A component's own ui prop replaces the Theme component's classes for the same slot.
<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.
<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>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:
<template>
<PButton
:ui="{ label: () => 'text-base font-bold' }"
label="Button"
/>
</template>
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.
<template>
<PButton class="uno-layer-p-variant:font-bold uno-layer-p-variant:rounded-full">Button</PButton>
</template>