---
title: "Composing components"
description: "Reuse recipes with extend, or concatenate a parent result into a new base. Classes from both sides are kept."
canonical_url: "https://vinicunca.dev/unocss-variants/concepts/composing-components"
---
# 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](https://vinicunca.dev/unocss-variants/concepts/layers) prefix on the child.

```vue
<template>
  <uv-compose-buttons />
</template>
```

```vue
<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>
```

```ts
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.

```ts
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).

```ts
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
```

> [!NOTE]
> 
> 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.

```vue
<template>
  <uv-slot-card />
</template>
```

```ts
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.

```ts
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.


## Sitemap

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