UIPackage
Menu

Framework

Change language

Boilerplate repo

Chip

chip ui
Boilerplate repo

Compact, removable tag — typically used inside `tags-input` or as a filter pill. Seven variants (`default`, `filled`, `outlined`, `elevated`, `success`, `warning`, `destructive`), three sizes, and an optional close button.

Also available for React ->

Installation

$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/chip.json
Named registry: npx shadcn-vue@latest add @uipkge/chip Installs to: app/components/ui/chip/

Examples

Loading interactive previews…

Props

Name Type / Values Default Required
variant
'default''filled''outlined''elevated''success''warning''destructive'
optional
size
'sm''default''lg'
optional
class HTMLAttributes['class'] optional
closable boolean optional

Files installed (4)

  • app/components/ui/chip/Chip.vue 2.1 kB
    <script setup lang="ts">
    import { ref } from 'vue'
    import type { HTMLAttributes } from 'vue'
    import { X } from 'lucide-vue-next'
    import { cn } from '@/lib/utils'
    import { chipVariants } from './chip.variants'
    
    // Inlined unions: SFC compiler can't extract runtime props from
    // `ChipVariants['variant'] | ['size']`.
    const props = defineProps<{
      variant?: 'default' | 'filled' | 'outlined' | 'elevated' | 'success' | 'warning' | 'destructive'
      size?: 'sm' | 'default' | 'lg'
      class?: HTMLAttributes['class']
      closable?: boolean
    }>()
    
    const emit = defineEmits<{
      close: []
    }>()
    
    const leaving = ref(false)
    
    function onClose() {
      if (leaving.value) return
      leaving.value = true
      const reduce =
        typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches
      window.setTimeout(
        () => {
          emit('close')
        },
        reduce ? 0 : 160,
      )
    }
    </script>
    
    <template>
      <span
        data-uipkge
        data-slot="chip"
        :data-leaving="leaving || undefined"
        :class="cn(chipVariants({ variant, size }), 'chip-enter', leaving && 'chip-leave', props.class)"
      >
        <slot />
        <button
          v-if="closable"
          type="button"
          aria-label="Remove item"
          class="focus-visible:ring-ring hover:bg-foreground/10 ml-1 inline-flex min-h-6 min-w-6 items-center justify-center rounded-full transition-transform duration-150 focus-visible:ring-1 focus-visible:outline-none active:scale-90"
          @click.stop="onClose"
        >
          <X class="size-3" aria-hidden="true" />
        </button>
      </span>
    </template>
    
    <style>
    @keyframes chip-enter {
      from {
        opacity: 0;
        transform: scale(0.88);
      }
      to {
        opacity: 1;
        transform: scale(1);
      }
    }
    
    @keyframes chip-leave {
      to {
        opacity: 0;
        transform: scale(0.88);
      }
    }
    
    [data-slot='chip'].chip-enter {
      animation: chip-enter 180ms cubic-bezier(0.22, 1.2, 0.36, 1) both;
    }
    
    [data-slot='chip'].chip-leave {
      animation: chip-leave 160ms ease-in both;
      pointer-events: none;
    }
    
    @media (prefers-reduced-motion: reduce) {
      [data-slot='chip'].chip-enter,
      [data-slot='chip'].chip-leave {
        animation: none !important;
      }
    }
    </style>
  • app/components/ui/chip/ChipGroup.vue 1.5 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    
    const props = withDefaults(
      defineProps<{
        class?: HTMLAttributes['class']
        selected?: string[]
        multiple?: boolean
        filter?: boolean
        column?: boolean
        mandatory?: boolean
        max?: number
        disabled?: boolean
      }>(),
      {
        selected: () => [],
        multiple: false,
        filter: false,
        column: false,
        mandatory: false,
        disabled: false,
      },
    )
    
    const emit = defineEmits<{
      'update:selected': [value: string[]]
    }>()
    
    function isSelected(value: string) {
      return props.selected.includes(value)
    }
    
    function toggle(value: string) {
      if (props.disabled) return
    
      let newSelected: string[]
    
      if (props.multiple) {
        if (isSelected(value)) {
          newSelected = props.selected.filter((v) => v !== value)
        } else {
          if (props.max && props.selected.length >= props.max) {
            newSelected = [...props.selected.slice(1), value]
          } else {
            newSelected = [...props.selected, value]
          }
        }
      } else {
        if (isSelected(value) && !props.mandatory) {
          newSelected = []
        } else {
          newSelected = [value]
        }
      }
    
      emit('update:selected', newSelected)
    }
    </script>
    
    <template>
      <div
        role="group"
        :class="['flex flex-wrap gap-2', column ? 'flex-col' : '', props.class]"
        :data-chip-group="true"
        :data-multiple="multiple || undefined"
        :data-filter="filter || undefined"
      >
        <slot :selected="selected" :multiple="multiple" :filter="filter" :is-selected="isSelected" :toggle="toggle" />
      </div>
    </template>
  • app/components/ui/chip/chip.variants.ts 1.5 kB
    import type { VariantProps } from 'class-variance-authority'
    import { cva } from 'class-variance-authority'
    
    /**
     * Variant definitions live in their own file (rather than the package
     * `index.ts`) so consuming Vue SFCs can import without creating a circular
     * dependency through the index. See card.variants.ts for the canonical
     * example + the SSR symptom that motivated the split.
     */
    
    export const chipVariants = cva(
      'inline-flex items-center justify-center gap-1 rounded-full text-xs font-medium w-fit whitespace-nowrap shrink-0 transition-colors duration-200 overflow-hidden focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
      {
        variants: {
          variant: {
            default: 'bg-muted text-muted-foreground hover:bg-muted/80',
            filled: 'bg-primary text-primary-foreground hover:bg-primary/90',
            outlined: 'border border-current bg-transparent hover:bg-accent',
            elevated: 'bg-primary/10 text-primary shadow-sm hover:bg-primary/20',
            success: 'bg-success/10 text-success dark:text-success hover:bg-success/20',
            warning: 'bg-warning/10 text-warning dark:text-warning hover:bg-warning/20',
            destructive: 'bg-destructive/10 text-destructive dark:text-destructive hover:bg-destructive/20',
          },
          size: {
            sm: 'h-6 px-2 text-xs',
            default: 'h-7 px-2.5 text-xs',
            lg: 'h-8 px-3 text-sm',
          },
        },
        defaultVariants: {
          variant: 'default',
          size: 'default',
        },
      },
    )
    
    export type ChipVariants = VariantProps<typeof chipVariants>
  • app/components/ui/chip/index.ts 0.3 kB
    export { default as Chip } from './Chip.vue'
    export { default as ChipGroup } from './ChipGroup.vue'
    
    // Re-export variant API from the sibling file (kept separate to avoid the
    // Component.vue <-> index.ts circular import that broke dev SSR for Card).
    export { chipVariants, type ChipVariants } from './chip.variants'

Raw manifest: https://uipkge.dev/r/vue/chip.json