Slots

Split a component into named parts, style them with variants, and share classes through compound slots.

Basic usage

Set slots to an object of part names. The recipe then returns functions, not a string. Always include the parts you render — on the published 1.5.0 build, slots: {} does not switch into slot mode (it still returns a string).

An implicit base slot is added when you pass a non-empty slots object, and base on the options object becomes that slot's classes.

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

Ketut Gabler
Full-stack developer
<script setup>
import { uv } from 'unocss-variants';

const card = uv({
  slots: {
    base: 'md:flex bg-background-muted rounded-xl p-8 md:p-0',
    avatar: 'w-24 h-24 md:w-48 md:h-auto md:rounded-none rounded-full mx-auto drop-shadow-lg',
    wrapper: 'flex-1 pt-6 md:p-8 text-center md:text-left space-y-4',
    description: 'text-md font-medium',
    infoWrapper: 'font-medium',
    name: 'text-sm color-sky-500 dark:color-sky-400',
    role: 'text-sm color-slate-700 dark:color-slate-500'
  }
});

const { base, avatar, wrapper, description, infoWrapper, name, role } = card();
</script>

<template>
  <figure :class="base()">
    <img
      src="/images/intro-avatar.png"
      alt=""
      :class="avatar()"
      height="512"
      width="385"
    >
    <div :class="wrapper()">
      <blockquote>
        <p :class="description()">
          UnoCSS Variants lets you reuse recipes instead of copying class strings, and keeps every variant typed.
        </p>
      </blockquote>
      <div :class="infoWrapper()">
        <div :class="name()">
          Ketut Gabler
        </div>
        <div :class="role()">
          Full-stack developer
        </div>
      </div>
    </div>
  </figure>
</template>

Slots with variants

A variant value can be a string (applied to the base slot) or a map of slot names.

Shoes

Nike Adapt BB 2.0

$279.97
$350
20% off
Select color:
Each slot function already includes the variant classes for that part. You do not pick slots yourself.
const item = uv({
  slots: {
    base: 'flex flex-col mb-4 sm:flex-row p-6 bg-white dark:bg-stone-900 drop-shadow-xl rounded-xl',
    buyButton: 'text-xs sm:text-sm px-4 h-10 rounded-lg shadow-lg uppercase font-semibold tracking-wider color-white active:opacity-80',
    addToBagButton: 'text-xs sm:text-sm px-4 h-10 rounded-lg uppercase font-semibold tracking-wider border-2 active:opacity-80'
    // ...
  },
  variants: {
    color: {
      primary: {
        buyButton: 'bg-blue-500 shadow-blue-500/50',
        addToBagButton: 'color-blue-500 border-blue-500'
      },
      secondary: {
        buyButton: 'bg-purple-500 shadow-purple-500/50',
        addToBagButton: 'color-purple-500 border-purple-500'
      }
    }
  }
});

const classes = item({ color: 'primary' });
classes.buyButton();

Call the recipe again when the selected variant changes (in Vue, wrap that call in computed).

Slots with compound variants

Pass a slot map as class / className on a compound variant to target individual parts.

Oops, something went wrong
Something went wrong saving your changes. Try again later.
Select severity:
Select variant:
const alert = uv({
  slots: {
    root: 'rounded py-3 px-5 mb-4',
    title: 'font-bold mb-1',
    message: ''
  },
  variants: {
    variant: {
      outlined: { root: 'border' },
      filled: { root: '' }
    },
    severity: {
      error: '',
      success: ''
    }
  },
  compoundVariants: [
    {
      variant: 'filled',
      severity: 'success',
      class: {
        root: 'bg-green-100 dark:bg-green-800',
        title: 'color-green-900 dark:color-green-50',
        message: 'color-green-700 dark:color-green-200'
      }
    }
    // other combinations...
  ],
  defaultVariants: {
    variant: 'filled',
    severity: 'success'
  }
});

Compound slots

compoundSlots applies the same classes to several slots. Omit extra variant keys to always apply the classes; add keys to apply them only when those variants match.

const pagination = uv({
  slots: {
    base: 'relative flex max-w-fit flex-wrap gap-1',
    item: 'data-[active="true"]:bg-blue-500 data-[active="true"]:color-white',
    prev: '',
    next: ''
  },
  variants: {
    size: {
      xs: {},
      sm: {},
      md: {}
    }
  },
  defaultVariants: {
    size: 'md'
  },
  compoundSlots: [
    {
      slots: ['item', 'prev', 'next'],
      class: [
        'flex',
        'items-center',
        'justify-center',
        'rounded-lg',
        'bg-neutral-200',
        'color-neutral-800'
      ]
    },
    {
      slots: ['item', 'prev', 'next'],
      size: 'md',
      class: 'h-9 w-9 text-base'
    }
  ]
});

Slot-level variant overrides

Each slot function accepts the same props as the root call. Use that to override a variant for one part only (for example a selected tab):

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

const card = uv({
  slots: {
    base: 'flex gap-2',
    tab: 'rounded'
  },
  variants: {
    color: {
      primary: {
        tab: 'color-blue-500 dark:color-blue-400'
      },
      secondary: {
        tab: 'color-purple-500 dark:color-purple-400'
      }
    },
    isSelected: {
      true: {
        tab: 'font-bold'
      }
    }
  }
});

const { base, tab } = card({ color: 'primary' });
</script>

<template>
  <div :class="base()">
    <button
      v-for="item in items"
      :key="item.id"
      :class="tab({ isSelected: item.isSelected })"
    >
      {{ item.label }}
    </button>
  </div>
</template>
tab();
// rounded color-blue-500 dark:color-blue-400

tab({ isSelected: true });
// rounded color-blue-500 dark:color-blue-400 font-bold

Pass class / className to the slot function, not to the root call. A root-level class on a slotted recipe is not applied to base() on the published 1.5.0 build:

const slots = card({ class: 'max-w-md' });
slots.base(); // still only "flex gap-2"
slots.base({ class: 'max-w-md' }); // "flex gap-2 max-w-md"