Composing components

Reuse recipes with extend, or concatenate a parent result into a new base. Classes from both sides are kept.

Using extend

extend merges the parent recipe into the child: base, slots, variants, defaultVariants, compoundVariants, and compoundSlots. TypeScript still autocompletes the combined keys.

You can extend one parent per recipe. Chain uv({ extend: child }) if you need a longer lineage.

Basic example

Parent classes are concatenated with child base. Nothing is dropped, so conflicting utilities (background, radius) need a layer prefix on the child.

<script setup>
import { uv } from 'unocss-variants';

const baseButton = uv({
  base: [
    'font-semibold',
    'dark:color-white',
    'py-1',
    'px-3',
    'rounded-full',
    'active:opacity-80',
    'bg-zinc-100',
    'hover:bg-zinc-200',
    'dark:bg-zinc-800',
    'dark:hover:bg-zinc-800'
  ]
});

const buyButton = uv({
  extend: baseButton,
  base: [
    'text-sm',
    'color-white',
    'rounded-lg',
    'shadow-lg',
    'uppercase',
    'tracking-wider',
    'uno-layer-uv:bg-blue-500',
    'uno-layer-uv:hover:bg-blue-600',
    'shadow-blue-500/50',
    'uno-layer-uv:dark:bg-blue-500',
    'uno-layer-uv:dark:hover:bg-blue-600'
  ]
});
</script>

<template>
  <div class="flex gap-3">
    <button :class="baseButton()">
      Button
    </button>
    <button :class="buyButton()">
      Buy button
    </button>
  </div>
</template>
buyButton();
// font-semibold dark:color-white py-1 px-3 rounded-full active:opacity-80 bg-zinc-100 hover:bg-zinc-200 dark:bg-zinc-800 dark:hover:bg-zinc-800 text-sm color-white rounded-lg shadow-lg uppercase tracking-wider uno-layer-uv:bg-blue-500 uno-layer-uv:hover:bg-blue-600 shadow-blue-500/50 uno-layer-uv:dark:bg-blue-500 uno-layer-uv:dark:hover:bg-blue-600

Extending variants

The child inherits parent axes and can add new ones. Matching keys merge: parent values stay, child values are added.

import { uv } from 'unocss-variants';

const baseButton = uv({
  base: 'font-semibold color-white rounded-full active:opacity-80',
  variants: {
    color: {
      primary: 'bg-blue-500 hover:bg-blue-700',
      secondary: 'bg-purple-500 hover:bg-purple-700',
      success: 'bg-green-500 hover:bg-green-700'
    },
    size: {
      small: 'py-0 px-2 text-xs',
      medium: 'py-1 px-3 text-sm',
      large: 'py-1.5 px-3 text-md'
    }
  }
});

const myButton = uv({
  extend: baseButton,
  variants: {
    isSquared: {
      true: 'rounded-sm'
    }
  }
});

myButton({ color: 'success', size: 'medium', isSquared: true });
// font-semibold color-white rounded-full active:opacity-80 rounded-sm bg-green-500 hover:bg-green-700 py-1 px-3 text-sm

defaultVariants and compoundVariants are inherited too. Child defaults override parent keys of the same name; compound arrays are concatenated (parent first).

const baseButton = uv({
  base: 'font-semibold color-white rounded-full active:opacity-80',
  variants: {
    color: {
      primary: 'bg-blue-500 hover:bg-blue-700',
      secondary: 'bg-purple-500 hover:bg-purple-700',
      success: 'bg-green-500 hover:bg-green-700'
    },
    size: {
      small: 'py-0 px-2 text-xs',
      medium: 'py-1 px-3 text-sm',
      large: 'py-1.5 px-3 text-md'
    }
  },
  defaultVariants: {
    color: 'primary',
    size: 'medium'
  },
  compoundVariants: [
    {
      color: 'primary',
      size: 'medium',
      className: 'rounded-sm'
    }
  ]
});

const myButton = uv({
  extend: baseButton
});

myButton();
// font-semibold color-white rounded-full active:opacity-80 bg-blue-500 hover:bg-blue-700 py-1 px-3 text-sm rounded-sm
Override inherited variants, defaultVariants, or compoundVariants by passing them on the child recipe.

Extending slots

Slots merge by key. The child can add parts or append classes on an inherited name.

UnoCSS Variants lets you reuse recipes instead of copying class strings, and keeps every variant typed.

Ketut Gabler
Full-stack developer
const card = uv({
  slots: {
    base: 'md:flex bg-background-muted rounded-xl p-8 md:p-0',
    title: 'text-sm font-medium'
  }
});

const mediaCard = uv({
  extend: card,
  slots: {
    media: 'mb-3 aspect-video rounded-lg',
    title: 'text-base font-semibold'
  }
});

const { base, media, title } = mediaCard();

mediaCard keeps base from card, adds media, and concatenates title classes.

Using the result string

You can feed parent() into a new base array. This is not type-safe — the child does not inherit variant keys.

import { uv } from 'unocss-variants';

const baseButton = uv({
  base: 'font-medium text-sm px-3 py-1 bg-blue-500 color-white rounded-full active:opacity-80'
});

const actionButton = uv({
  base: [baseButton(), 'bg-red-500', 'rounded-xs']
});

actionButton();
// font-medium text-sm px-3 py-1 bg-blue-500 color-white rounded-full active:opacity-80 bg-red-500 rounded-xs

Both backgrounds remain. Put uno-layer-uv: on the classes that must win, or prefer extend so variants stay typed.