---
title: "PillGroup"
description: "A pill-style single/multiple selection group supporting string and object items, quantity constraints, field mapping, and slot customization."
canonical_url: "https://nuxt.mhaibaraai.cn/en/docs/components/pill-group"
---
# PillGroup

> A pill-style single/multiple selection group supporting string and object items, quantity constraints, field mapping, and slot customization.

## Introduction

`MPillGroup` is a pill-style option group component supporting both single and multiple selection. It accepts string arrays or object arrays as items, maps business object fields via `labelKey`, `valueKey`, and `descriptionKey`, and supports quantity constraints, per-item semantic colors, active/inactive style differentiation, and slot customization.

## Usage

Pass a string array to generate single-select pills. The selected value is written directly to v-model:

```vue
<script setup lang="ts">
const value = ref("All")
</script>

<template>
  <MPillGroup :items='["All","Todo","In Progress","Done","Archived"]' />
</template>
```

### `items` Structured Items

Object `items` can render composite content via `label`, `description`, and `icon`, while preserving the original object as the value:

```vue
<script setup lang="ts">
const value = ref(null)
</script>

<template>
  <MPillGroup :items='[{"value":"free","label":"Free","description":"Basic · 1 user","icon":"i-lucide-gift"},{"value":"pro","label":"Pro","description":"Team · 10 users","icon":"i-lucide-zap"},{"value":"team","label":"Team","description":"Insights · 50 users","icon":"i-lucide-users"},{"value":"enterprise","label":"Enterprise","description":"Custom SLA · Unlimited","icon":"i-lucide-building"}]' />
</template>
```

### `multiple` Multiple Selection Mode

When `multiple` is enabled, `modelValue` becomes an array. Use `valueKey` to save only a stable field as the selected value:

```vue
<script setup lang="ts">
const value = ref([])
</script>

<template>
  <MPillGroup :items='[{"value":"free","label":"Free","icon":"i-lucide-gift"},{"value":"pro","label":"Pro","icon":"i-lucide-zap"},{"value":"team","label":"Team","icon":"i-lucide-users"},{"value":"enterprise","label":"Enterprise","icon":"i-lucide-building"}]' multiple value-key="value" />
</template>
```

### `deselectable` Single Deselect

`deselectable` allows clicking the current item again to clear the selection — useful for optional filter conditions (ignored in multiple mode):

```vue
<script setup lang="ts">
const value = ref(null)
</script>

<template>
  <MPillGroup :items='[{"value":"free","label":"Free","icon":"i-lucide-gift"},{"value":"pro","label":"Pro","icon":"i-lucide-zap"},{"value":"team","label":"Team","icon":"i-lucide-users"},{"value":"enterprise","label":"Enterprise","icon":"i-lucide-building"}]' deselectable />
</template>
```

### `orientation` Layout Direction

Setting `orientation` to `vertical` stacks items vertically — suited for longer descriptions or narrow containers:

```vue
<script setup lang="ts">
const value = ref(null)
</script>

<template>
  <MPillGroup :items='[{"value":"free","label":"Free","description":"Basic · 1 user","icon":"i-lucide-gift"},{"value":"pro","label":"Pro","description":"Team · 10 users","icon":"i-lucide-zap"},{"value":"team","label":"Team","description":"Insights · 50 users","icon":"i-lucide-users"},{"value":"enterprise","label":"Enterprise","description":"Custom SLA · Unlimited","icon":"i-lucide-building"}]' orientation="vertical" />
</template>
```

### `activeVariant` Active Style

`activeVariant` and `inactiveVariant` control the visual variant for selected and unselected states respectively:

```vue
<script setup lang="ts">
const value = ref(null)
</script>

<template>
  <MPillGroup :items='[{"value":"free","label":"Free","icon":"i-lucide-gift"},{"value":"pro","label":"Pro","icon":"i-lucide-zap"},{"value":"team","label":"Team","icon":"i-lucide-users"},{"value":"enterprise","label":"Enterprise","icon":"i-lucide-building"}]' active-variant="solid" inactive-variant="outline" />
</template>
```

### `size` Size

Use `size` to switch pill sizes. When placed inside `UFormField`, the size is automatically inherited from context:

```vue
<script setup lang="ts">
const value = ref(null)
</script>

<template>
  <MPillGroup :items='[{"value":"free","label":"Free","icon":"i-lucide-gift"},{"value":"pro","label":"Pro","icon":"i-lucide-zap"},{"value":"team","label":"Team","icon":"i-lucide-users"},{"value":"enterprise","label":"Enterprise","icon":"i-lucide-building"}]' size="md" />
</template>
```

### `color` Primary Color

Use `color` to set the component's primary color, making it easier to coordinate with external `UFieldGroup` or button groups:

```vue
<script setup lang="ts">
const value = ref("All")
</script>

<template>
  <MPillGroup :items='["All","Todo","In Progress","Done","Archived"]' color="primary" />
</template>
```

### `min` and `max` Quantity Constraints

`min` and `max` constrain the number of multiple selections: unselected items become grayed out at the max, and selected items cannot be deselected at the min:

```vue
<script setup lang="ts">
const value = ref(["frontend","design"])
</script>

<template>
  <MPillGroup :items='[{"value":"frontend","label":"Frontend","icon":"i-lucide-layout"},{"value":"backend","label":"Backend","icon":"i-lucide-server"},{"value":"devops","label":"DevOps","icon":"i-lucide-cloud"},{"value":"design","label":"Design","icon":"i-lucide-palette"},{"value":"data","label":"Data","icon":"i-lucide-database"}]' multiple value-key="value" :min="1" :max="3" />
</template>
```

### `labelKey` Display Field

`labelKey` specifies the field to read from the business object for display. Falls back to `label` if not set:

```vue
<script setup lang="ts">
const value = ref(null)
</script>

<template>
  <MPillGroup :items='[{"id":"u1","name":"Alice","handle":"@alice","email":"alice@team.dev"},{"id":"u2","name":"Bob","handle":"@bob","email":"bob@team.dev"},{"id":"u3","name":"Carol","handle":"@carol","email":"carol@team.dev"}]' label-key="name" />
</template>
```

### `valueKey` Value Field

`valueKey` specifies the field written back to `modelValue`, making it easy to save only a stable id rather than the entire object:

```vue
<script setup lang="ts">
const value = ref("u1")
</script>

<template>
  <MPillGroup :items='[{"id":"u1","name":"Alice","handle":"@alice","email":"alice@team.dev"},{"id":"u2","name":"Bob","handle":"@bob","email":"bob@team.dev"},{"id":"u3","name":"Carol","handle":"@carol","email":"carol@team.dev"}]' label-key="name" value-key="id" />
</template>
```

### `color` Per-Item Semantic Color

Each item can carry an independent `color`, applied per item at render time to express status differences:

```vue
<script setup lang="ts">
const value = ref("doing")
</script>

<template>
  <MPillGroup :items='[{"value":"todo","label":"Todo","icon":"i-lucide-circle","color":"neutral"},{"value":"doing","label":"In Progress","icon":"i-lucide-loader","color":"warning"},{"value":"done","label":"Done","icon":"i-lucide-check-circle","color":"success"},{"value":"block","label":"Blocked","icon":"i-lucide-octagon-x","color":"error"}]' value-key="value" />
</template>
```

### `disabled` Disabled

A per-item `disabled` prevents interaction on that item. The component-level `disabled` freezes the entire group while preserving the current value:

```vue
<script setup lang="ts">
const value = ref("pdf")
</script>

<template>
  <MPillGroup :items='[{"value":"pdf","label":"PDF","icon":"i-lucide-file-text"},{"value":"excel","label":"Excel (Coming Soon)","icon":"i-lucide-file-spreadsheet","disabled":true},{"value":"csv","label":"CSV","icon":"i-lucide-file"},{"value":"json","label":"JSON","icon":"i-lucide-braces"}]' value-key="value" :disabled="false" />
</template>
```

## Examples

### Slot-Based Item Customization

The `item-label`, `item-trailing`, and other slots can take over partial rendering of individual items. The selection indicator updates with the `selected` slot prop:

```vue [ComponentsPillGroupSlotsExample.vue]
<script setup lang="ts">
import type { PillItem } from '@movk/nuxt'

const items: PillItem[] = [
  { value: 'free', label: '免费版', icon: 'i-lucide-gift' },
  { value: 'pro', label: '专业版', icon: 'i-lucide-zap' },
  { value: 'enterprise', label: '企业版', icon: 'i-lucide-building' }
]
const value = ref<PillItem[]>([items[1]!])

function getItemLabel(item: PillItem): string {
  if (typeof item === 'object' && item) return item.label ?? String(item.value)
  return String(item)
}
</script>

<template>
  <MPillGroup v-model="value" :items="items" multiple>
    <template #item-label="{ item }">
      <span class="font-semibold">{{ getItemLabel(item) }}</span>
    </template>
    <template #item-trailing="{ selected }">
      <UIcon v-if="selected" name="i-lucide-check" class="size-3.5" />
    </template>
  </MPillGroup>
</template>
```

### Event Callbacks

Clicking an item fires `update:modelValue`, `change`, and `select` in sequence. The `select` payload contains `item`, `value`, `selected`, and `index`:

```vue [ComponentsPillGroupEventsExample.vue]
<script setup lang="ts">
import type { PillItem, PillSelectPayload } from '@movk/nuxt'

const items: PillItem[] = [
  { value: 'free', label: '免费版', icon: 'i-lucide-gift' },
  { value: 'pro', label: '专业版', icon: 'i-lucide-zap' },
  { value: 'enterprise', label: '企业版', icon: 'i-lucide-building' }
]
const value = ref<PillItem[]>([items[1]!])
const last = ref<string>('（点击选项查看事件）')

function onSelect(payload: PillSelectPayload<PillItem>) {
  const label = typeof payload.item === 'object' && payload.item
    ? payload.item.label ?? String(payload.item.value)
    : String(payload.item)
  last.value = `select: ${label} → selected=${payload.selected}, index=${payload.index}`
}
</script>

<template>
  <div class="flex flex-col gap-3">
    <MPillGroup
      v-model="value"
      :items="items"
      multiple
      @select="onSelect"
    />
    <p class="text-sm text-muted">
      {{ last }}
    </p>
  </div>
</template>
```

## API

### Props

```ts
/**
 * Props for the MPillGroup component
 */
interface MPillGroupProps {
  items?: T[] | undefined;
  valueKey?: VK | undefined;
  /**
   * @default 'label'
   */
  labelKey?: keyof Extract<NestedItem<T>, object> & string | DotPathKeys<Extract<NestedItem<T>, object>> | undefined;
  /**
   * @default 'description'
   */
  descriptionKey?: keyof Extract<NestedItem<T>, object> & string | DotPathKeys<Extract<NestedItem<T>, object>> | undefined;
  /**
   * 对象模式下判定 selected 的比较依据。string 走 `getPath` 比较（multiple=true 时启用 Map O(1)）；
   * function 自定义比较；不传则按 [valueKey, 'value', labelKey] 启发式回退。
   */
  by?: string | (a: T, b: T): boolean | undefined;
  /**
   * 排列方向
   * @default 'horizontal'
   */
  orientation?: "horizontal" | "vertical" | undefined;
  /**
   * 按钮尺寸
   * @default 'md'
   */
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  /**
   * 按钮颜色
   * @default 'primary'
   */
  color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "important" | "neutral" | undefined;
  /**
   * 选中态单项视觉变体
   * @default 'solid'
   */
  activeVariant?: "link" | "solid" | "outline" | "soft" | "subtle" | "ghost" | undefined;
  /**
   * 未选中态单项视觉变体
   * @default 'soft'
   */
  inactiveVariant?: "link" | "solid" | "outline" | "soft" | "subtle" | "ghost" | undefined;
  disabled?: boolean | undefined;
  /**
   * 是否可以多选
   */
  multiple?: M | undefined;
  /**
   * 单选：再次点击当前选中项清空（多选模式下被忽略）
   * @default false
   */
  deselectable?: boolean | undefined;
  /**
   * 多选：最多可选数量
   */
  max?: number | undefined;
  /**
   * 多选：最少需保留数量
   */
  min?: number | undefined;
  defaultValue?: GetModelValue<T, VK, M, undefined> | undefined;
  modelValue?: GetModelValue<T, VK, M, undefined> & GetModelValue<T, VK, M> | undefined;
  ui?: Record<string, C> & { root?: SlotClass; list?: SlotClass; item?: SlotClass; leading?: SlotClass; leadingIcon?: SlotClass; trailing?: SlotClass; trailingIcon?: SlotClass; itemWrapper?: SlotClass; itemLabel?: SlotClass; itemDescription?: SlotClass; } | undefined;
}
```

### Emits

```ts
/**
 * Emitted events for the MPillGroup component
 */
interface MPillGroupEmits {
  blur: (payload: [event: FocusEvent]) => void;
  focus: (payload: [event: FocusEvent]) => void;
  change: (payload: [value: GetModelValue<T, VK, M, undefined>]) => void;
  update:modelValue: (payload: [value: GetModelValue<T, VK, M, undefined>]) => void;
  select: (payload: [payload: PillSelectPayload<T>]) => void;
  update:modelValue: (payload: [value: GetModelValue<T, VK, M> | undefined]) => void;
}
```

### Slots

```ts
/**
 * Slots for the MPillGroup component
 */
interface MPillGroupSlots {
  leading(): any;
  default(): any;
  trailing(): any;
  item(): any;
  item-leading(): any;
  item-label(): any;
  item-description(): any;
  item-trailing(): any;
}
```

## Theme

<component-theme>



</component-theme>

## Changelog

See commit history for [src/runtime/components/PillGroup.vue](https://github.com/mhaibaraai/movk-nuxt/commits/main/src/runtime/components/PillGroup.vue).


## Sitemap

See the full [sitemap](https://nuxt.mhaibaraai.cn/sitemap.md) for all pages.
