Slots

Slots allows you to separate a component into multiple parts.

Basic Usage

You can add slots by using the slots key. There's no limit to how many slots you can add.

slot-example.vue
<script lang="ts" 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 text-sky-500 dark:text-sky-400',
    role: 'text-sm text-slate-700 dark:text-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 allows you to reduce repeated code in your
          project and make it more readable. They fixed the headache of
          building a design system with UnoCSS.
        </p>
      </blockquote>

      <figcaption :class="infoWrapper()">
        <div :class="name()">
          Ketut Gabler
        </div>
        <div :class="role()">
          Full-stack developer, Ngibur
        </div>
      </figcaption>
    </div>
  </figure>
</template>

Slots with variants

You can also change the entire component and its slots by using the variants. See more about variants here.

Shoes for example

Nike Adapt BB 2.0

$279.97
$350
20% off
Select color:
UnoCSS Variants will automatically select the correct variant for each slot.
slot-variants-example.vue
<script lang="ts" setup>
import { PRadioGroup } from '#components';
import { uv } from 'unocss-variants';
import { computed, ref } from 'vue';

const color = ref('primary' as const);
const size = ref('xs');

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',
    imageWrapper: 'mb-6 flex-none h-48 w-full relative z-10 sm:(mb-0 h-auto w-48) before:(rounded-xl bg-[#18000E] h-full w-full content-empty left-0 top-0 absolute from-[#010187] bg-gradient-to-r)',
    img: 'sm:scale-125 absolute z-10 top-2 sm:left-2 inset-0 w-full h-full object-cover rounded-lg',
    title: 'relative w-full flex-none mb-2 text-2xl font-semibold text-stone-900 dark:text-white',
    price: 'relative font-semibold text-xl dark:text-white',
    previousPrice: 'relative line-through font-bold text-neutral-500 ml-3',
    percentOff: 'relative font-bold text-green-500 ml-3',
    sizeButton: 'cursor-pointer select-none relative font-semibold rounded-full w-10 h-10 flex items-center justify-center active:opacity-80 dark:text-white peer-checked:text-white',
    buyButton: 'text-xs sm:text-sm px-4 h-10 rounded-lg shadow-lg uppercase font-semibold tracking-wider text-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',
        sizeButton: 'peer-checked:bg-blue',
        addToBagButton: 'text-blue-500 border-blue-500',
      },
      secondary: {
        buyButton: 'bg-purple-500 shadow-purple-500/50',
        sizeButton: 'peer-checked:bg-purple',
        addToBagButton: 'text-purple-500 border-purple-500',
      },
      success: {
        buyButton: 'bg-green-500 shadow-green-500/50',
        sizeButton: 'peer-checked:bg-green',
        addToBagButton: 'text-green-500 border-green-500',
      },
    },
  },
});

const variant = computed(() =>
  uv({
    extend: uv(item),
  })({
    color: color.value,
  }),
);
</script>

<template>
  <div>
    <div :class="variant.base()">
      <div :class="variant.imageWrapper()">
        <img
          alt="Shoes for example"
          :class="variant.img()"
          loading="lazy"
          src="/images/shoes-1.png"
        >
      </div>
      <div class="pl-4 flex-auto sm:pl-8">
        <div class="flex flex-wrap items-baseline relative">
          <h1 :class="variant.title()">
            Nike Adapt BB 2.0
          </h1>
          <div :class="variant.price()">
            $279.97
          </div>
          <div :class="variant.previousPrice()">
            $350
          </div>
          <div :class="variant.percentOff()">
            20% off
          </div>
        </div>
        <div class="my-4 flex items-baseline">
          <div class="text-sm font-medium flex space-x-3">
            <label
              v-for="itemSize in ['xs', 's', 'm', 'l', 'xl']"
              :key="itemSize"
            >
              <input
                v-model="size"
                :checked="size === itemSize"
                class="peer sr-only"
                name="size"
                :value="itemSize"
                type="radio"
              >
              <div :class="variant.sizeButton()">{{ itemSize.toUpperCase() }}</div>
            </label>
          </div>
        </div>
        <div class="flex space-x-4">
          <button :class="variant.buyButton()">
            Buy now
          </button>
          <button :class="variant.addToBagButton()">
            Add to bag
          </button>
        </div>
      </div>
    </div>

    <div class="flex flex-col gap-2">
      <span class="text-slate-800 font-semibold dark:text-slate-200">
        Select color:
      </span>
      <PRadioGroup
        v-model="color"
        :items="['primary', 'secondary', 'success']"
        :pohon="{
          label: 'capitalize',
        }"
      />
    </div>
  </div>
</template>

Slots with compound variants

Like variants, you can also define the styles for each slot when using compound variants. The slots can be passed to class or className as an object.

Oops, something went wrong
Something went wrong saving your changes. Try again later.
Select severity:
Select variant:
slot-compound-example.vue
<script lang="ts" setup>
import { PRadioGroup } from '#components';
import { uv } from 'unocss-variants';
import { computed, ref } from 'vue';

const severity = ref('success' as const);
const variant = ref('filled' as const);

const item = 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: 'outlined',
      severity: 'error',
      class: {
        root: 'border-red-700 dark:border-red-500',
        title: 'text-red-700 dark:text-red-500',
        message: 'text-red-600 dark:text-red-500',
      },
    },
    {
      variant: 'outlined',
      severity: 'success',
      class: {
        root: 'border-green-700 dark:border-green-500',
        title: 'text-green-700 dark:text-green-500',
        message: 'text-green-600 dark:text-green-500',
      },
    },
    {
      variant: 'filled',
      severity: 'error',
      class: {
        root: 'bg-red-100 dark:bg-red-800',
        title: 'text-red-900 dark:text-red-50',
        message: 'text-red-700 dark:text-red-200',
      },
    },
    {
      variant: 'filled',
      severity: 'success',
      class: {
        root: 'bg-green-100 dark:bg-green-800',
        title: 'text-green-900 dark:text-green-50',
        message: 'text-green-700 dark:text-green-200',
      },
    },
  ],
  defaultVariants: {
    variant: 'filled',
    severity: 'success',
  },
});

const alertUv = computed(() =>
  uv({
    extend: uv(item),
  })({
    severity: severity.value,
    variant: variant.value,
  }),
);
</script>

<template>
  <div :class="alertUv.root()">
    <div :class="alertUv.title()">
      Oops, something went wrong
    </div>
    <div :class="alertUv.message()">
      Something went wrong saving your changes. Try again later.
    </div>
  </div>

  <div class="flex flex-col gap-2">
    <span class="text-slate-800 font-semibold dark:text-slate-200">
      Select severity:
    </span>
    <PRadioGroup
      v-model="severity"
      :items="['success', 'error']"
      :pohon="{
        label: 'capitalize',
      }"
    />
    <span class="text-slate-800 font-semibold dark:text-slate-200">
      Select variant:
    </span>
    <PRadioGroup
      v-model="variant"
      :items="['filled', 'outlined']"
      :pohon="{
        label: 'capitalize',
      }"
    />
  </div>
</template>

Compound slots

Compound slots allow apply classes to multiple slots at once. This avoids having to repeat the same classes in multiple slots.

compound-slot-example.vue
<script lang="ts" setup>
import { uv } from 'unocss-variants';

const pagination = uv({
  slots: {
    base: 'relative flex max-w-fit flex-wrap gap-1',
    item: 'data-[active="true"]:bg-blue-500 data-[active="true"]:text-white',
    prev: '',
    next: '',
  },
  variants: {
    size: {
      xs: {},
      sm: {},
      md: {},
    },
  },
  defaultVariants: {
    size: 'md',
  },
  compoundSlots: [
    // if you don't specify any variant, it will always be applied
    {
      slots: ['item', 'prev', 'next'],
      class: [
        'flex',
        'flex-wrap',
        'truncate',
        'box-border',
        'outline-none',
        'items-center',
        'justify-center',
        'bg-neutral-200',
        'rounded-lg',
        'hover:bg-neutral-300',
        'active:bg-neutral-400',
        'text-neutral-800',
      ], // --> these classes will be applied to all slots
    },
    // if you specify a variant, it will only be applied if the variant is active
    {
      slots: ['item', 'prev', 'next'],
      size: 'xs',
      class: 'h-7 w-7 text-xs', // --> these classes will be applied to all slots if size is xs
    },
    {
      slots: ['item', 'prev', 'next'],
      size: 'sm',
      class: 'h-8 w-8 text-sm', // --> these classes will be applied to all slots if size is sm
    },
    {
      slots: ['item', 'prev', 'next'],
      size: 'md',
      class: 'h-9 w-9 text-base', // --> these classes will be applied to all slots if size is md
    },
  ],
});

const { base, item, prev, next } = pagination();
</script>

<template>
  <ul
    aria-label="pagination navigation"
    :class="base()"
  >
    <li>
      <button
        aria-label="Go to previous page"
        :class="prev()"
        data-disabled="true"
      >
        <svg
          fill="none"
          height="24"
          stroke="currentColor"
          strokeLinecap="round"
          strokeLinejoin="round"
          strokeWidth="2"
          width="24"
        >
          <path d="m15 18-6-6 6-6" />
        </svg>
      </button>
    </li>
    <li>
      <button
        aria-label="page 1"
        :class="item()"
      >
        1
      </button>
    </li>
    <li>
      <button
        aria-label="page 2"
        :class="item()"
      >
        2
      </button>
    </li>
    <li>
      <button
        aria-label="page 3"
        :class="item()"
        data-active="true"
      >
        3
      </button>
    </li>
    <li>
      <button
        aria-label="page 4"
        :class="item()"
      >
        4
      </button>
    </li>
    <li>
      <button
        aria-label="page 5"
        :class="item()"
      >
        5
      </button>
    </li>
    <li
      aria-hidden="true"
      :class="item()"
    >
      ...
    </li>
    <li>
      <button
        aria-label="page 10"
        :class="item()"
      >
        10
      </button>
    </li>
    <li>
      <button
        aria-label="Go to next page"
        :class="next()"
      >
        <svg
          fill="none"
          height="24"
          stroke="currentColor"
          strokeLinecap="round"
          strokeLinejoin="round"
          strokeWidth="2"
          width="24"
          xmlns="http://www.w3.org/2000/svg"
        >
          <path d="m9 18 6-6-6-6" />
        </svg>
      </button>
    </li>
  </ul>
</template>
Can I do the same with variants?
Yes, you can do the same with variants. However, compound slots allows you to write the classes only once and apply them to multiple slots. This is useful if you have a lot of slots and you want to apply the same classes to all of them.
For example, let's write the same component using variants:
import { uv } from "unocss-variants";

cosnt baseItem = [
  "flex",
  "flex-wrap",
  "truncate",
  "box-border",
  "outline-none",
  "items-center",
  "justify-center",
  "bg-neutral-100",
  "hover:bg-neutral-200",
  "active:bg-neutral-300",
  "text-neutral-500",
]

const pagination = uv({
  slots: {
    base: "flex flex-wrap relative gap-1 max-w-fit",
    item: [...baseItem, 'data-[active="true"]:bg-blue-500 data-[active="true"]:text-white'],
    prev: baseItem,
    next: baseItem,
  },
  variants: {
    size: {
      xs: {
        item: "w-7 h-7 text-xs",
        prev: "w-7 h-7 text-xs",
        next: "w-7 h-7 text-xs",
      },
      sm: {
        item: "w-8 h-8 text-sm",
        prev: "w-8 h-8 text-sm",
        next: "w-8 h-8 text-sm",
      },
      md: {
        item: "w-9 h-9 text-base",
        prev: "w-9 h-9 text-base",
        next: "w-9 h-9 text-base",
      },
    },
  },
  defaultVariants: {
    size: "md",
  },
});

Slot variant overrides

Some component libraries use class render props to allow customizing the class name of a component based on internal state. UnoCSS Variants supports slot level variant overrides to make it simple to override the selected variant(s) per slot as necessary.

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

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

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

<template>
  <Tabs :class="base()">
    <Tab
      v-for="item in items"
      :key="item.id"
      :class="tab({ isSelected: item.isSelected })"
      :id="item.id"
    >
      {{ item.label }}
    </Tab>
  </Tabs>
</template>