Skip to content

Chip

An indicator of a numeric value or a state.

Demo

This requires the following theme to be installed:

Component

Chip.vue
vue
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import theme from '@/ui/theme/chip'
import { Primitive, Slot } from 'reka-ui'
import { tv } from 'tailwind-variants'
import { computed } from 'vue'

const chip = tv(theme)

type ChipVariants = VariantProps<typeof chip>

export interface ChipProps {
  as?: any
  text?: string | number
  color?: ChipVariants['color']
  size?: ChipVariants['size']
  position?: ChipVariants['position']
  inset?: boolean
  standalone?: boolean
  class?: any
  ui?: Partial<typeof chip.slots>
}

export interface ChipEmits {
  (e: 'update:show', payload: boolean): void
}

export interface ChipSlots {
  default: (props?: object) => any
  content: (props?: object) => any
}
</script>

<script setup lang="ts">
const props = withDefaults(defineProps<ChipProps>(), {
  inset: false,
  standalone: false,
})
defineSlots<ChipSlots>()

const show = defineModel<boolean>('show', { default: true })

const ui = computed(() => chip({
  color: props.color,
  size: props.size,
  position: props.position,
  inset: props.inset,
  standalone: props.standalone,
}))
</script>

<template>
  <Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
    <Slot v-bind="$attrs">
      <slot />
    </Slot>

    <span v-if="show" :class="ui.base({ class: props.ui?.base })">
      <slot name="content">
        {{ text }}
      </slot>
    </span>
  </Primitive>
</template>
Chip.vue
vue
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import theme from '@/UI/Theme/chip'
import { Primitive, Slot } from 'reka-ui'
import { tv } from 'tailwind-variants'
import { computed } from 'vue'

const chip = tv(theme)

type ChipVariants = VariantProps<typeof chip>

export interface ChipProps {
  as?: any
  text?: string | number
  color?: ChipVariants['color']
  size?: ChipVariants['size']
  position?: ChipVariants['position']
  inset?: boolean
  standalone?: boolean
  class?: any
  ui?: Partial<typeof chip.slots>
}

export interface ChipEmits {
  (e: 'update:show', payload: boolean): void
}

export interface ChipSlots {
  default: (props?: object) => any
  content: (props?: object) => any
}
</script>

<script setup lang="ts">
const props = withDefaults(defineProps<ChipProps>(), {
  inset: false,
  standalone: false,
})
defineSlots<ChipSlots>()

const show = defineModel<boolean>('show', { default: true })

const ui = computed(() => chip({
  color: props.color,
  size: props.size,
  position: props.position,
  inset: props.inset,
  standalone: props.standalone,
}))
</script>

<template>
  <Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
    <Slot v-bind="$attrs">
      <slot />
    </Slot>

    <span v-if="show" :class="ui.base({ class: props.ui?.base })">
      <slot name="content">
        {{ text }}
      </slot>
    </span>
  </Primitive>
</template>

Theme

chip.ts
ts
export default {
  slots: {
    root: '',
    base: '',
  },
  variants: {
    color: {
      primary: '',
      secondary: '',
      success: '',
      info: '',
      warning: '',
      error: '',
      neutral: '',
    },
    size: {
      '3xs': 'h-[4px] min-w-[4px] text-[4px]',
      '2xs': 'h-[5px] min-w-[5px] text-[5px]',
      'xs': 'h-[6px] min-w-[6px] text-[6px]',
      'sm': 'h-[7px] min-w-[7px] text-[7px]',
      'md': 'h-[8px] min-w-[8px] text-[8px]',
      'lg': 'h-[9px] min-w-[9px] text-[9px]',
      'xl': 'h-[10px] min-w-[10px] text-[10px]',
      '2xl': 'h-[11px] min-w-[11px] text-[11px]',
      '3xl': 'h-[12px] min-w-[12px] text-[12px]',
    },
    position: {
      'top-right': 'top-0 right-0',
      'bottom-right': 'bottom-0 right-0',
      'top-left': 'top-0 left-0',
      'bottom-left': 'bottom-0 left-0',
    },
    inset: {
      false: '',
    },
    standalone: {
      false: '',
    },
  },
  compoundVariants: [],
  defaultVariants: {
    size: 'md',
    color: 'primary',
    position: 'top-right',
  } as const,
}
View Nuxt UI theme
chip.ts
ts
export default {
  slots: {
    root: 'relative inline-flex items-center justify-center shrink-0',
    base: 'rounded-full ring ring-bg flex items-center justify-center text-inverted font-medium whitespace-nowrap',
  },
  variants: {
    color: {
      primary: 'bg-primary',
      secondary: 'bg-secondary',
      success: 'bg-success',
      info: 'bg-info',
      warning: 'bg-warning',
      error: 'bg-error',
      neutral: 'bg-inverted',
    },
    size: {
      '3xs': 'h-[4px] min-w-[4px] text-[4px]',
      '2xs': 'h-[5px] min-w-[5px] text-[5px]',
      'xs': 'h-[6px] min-w-[6px] text-[6px]',
      'sm': 'h-[7px] min-w-[7px] text-[7px]',
      'md': 'h-[8px] min-w-[8px] text-[8px]',
      'lg': 'h-[9px] min-w-[9px] text-[9px]',
      'xl': 'h-[10px] min-w-[10px] text-[10px]',
      '2xl': 'h-[11px] min-w-[11px] text-[11px]',
      '3xl': 'h-[12px] min-w-[12px] text-[12px]',
    },
    position: {
      'top-right': 'top-0 right-0',
      'bottom-right': 'bottom-0 right-0',
      'top-left': 'top-0 left-0',
      'bottom-left': 'bottom-0 left-0',
    },
    inset: {
      false: '',
    },
    standalone: {
      false: 'absolute',
    },
  },
  compoundVariants: [{
    position: 'top-right',
    inset: false,
    class: '-translate-y-1/2 translate-x-1/2 transform',
  } as const, {
    position: 'bottom-right',
    inset: false,
    class: 'translate-y-1/2 translate-x-1/2 transform',
  } as const, {
    position: 'top-left',
    inset: false,
    class: '-translate-y-1/2 -translate-x-1/2 transform',
  } as const, {
    position: 'bottom-left',
    inset: false,
    class: 'translate-y-1/2 -translate-x-1/2 transform',
  } as const],
  defaultVariants: {
    size: 'md',
    color: 'primary',
    position: 'top-right',
  } as const,
}

Test

To test this component, you can use the following test file:

Chip.test.ts
ts
import type { RenderOptions } from '@testing-library/vue'
import Chip from '@/ui/components/Chip.vue'
import theme from '@/ui/theme/chip'
import { render, screen } from '@testing-library/vue'
import { describe, expect, it } from 'vitest'

describe('chip', () => {
  const sizes = Object.keys(theme.variants.size) as any
  const positions = Object.keys(theme.variants.position) as any

  it.each<[string, RenderOptions<typeof Chip>]>([
    // Props
    ['with text', { props: { text: 'Text' } }],
    ['with inset', { props: { inset: true } }],
    ...sizes.map((size: string) => [`with size ${size}`, { props: { size } }]),
    ...positions.map((position: string) => [`with position ${position}`, { props: { position } }]),
    ['with color neutral', { props: { color: 'neutral' } }],
    ['without show', { props: { show: false } }],
    ['with as', { props: { as: 'span' } }],
    ['with class', { props: { class: 'mx-auto' } }],
    ['with ui', { props: { ui: { base: 'text-(--ui-text-muted)' } } }],
    // Slots
    ['with default slot', { slots: { default: () => 'Default slot' } }],
    ['with content slot', { slots: { content: () => 'Content slot' } }],
  ])('renders %s correctly', (name, options) => {
    render(Chip, {
      attrs: {
        'data-testid': 'chip',
      },
      ...options,
    })

    expect(screen.getByTestId('chip')).toMatchSnapshot()
  })
})
Chip.test.ts
ts
import type { RenderOptions } from '@testing-library/vue'
import Chip from '@/UI/Components/Chip.vue'
import theme from '@/UI/Theme/chip'
import { render, screen } from '@testing-library/vue'
import { describe, expect, it } from 'vitest'

describe('chip', () => {
  const sizes = Object.keys(theme.variants.size) as any
  const positions = Object.keys(theme.variants.position) as any

  it.each<[string, RenderOptions<typeof Chip>]>([
    // Props
    ['with text', { props: { text: 'Text' } }],
    ['with inset', { props: { inset: true } }],
    ...sizes.map((size: string) => [`with size ${size}`, { props: { size } }]),
    ...positions.map((position: string) => [`with position ${position}`, { props: { position } }]),
    ['with color neutral', { props: { color: 'neutral' } }],
    ['without show', { props: { show: false } }],
    ['with as', { props: { as: 'span' } }],
    ['with class', { props: { class: 'mx-auto' } }],
    ['with ui', { props: { ui: { base: 'text-(--ui-text-muted)' } } }],
    // Slots
    ['with default slot', { slots: { default: () => 'Default slot' } }],
    ['with content slot', { slots: { content: () => 'Content slot' } }],
  ])('renders %s correctly', (name, options) => {
    render(Chip, {
      attrs: {
        'data-testid': 'chip',
      },
      ...options,
    })

    expect(screen.getByTestId('chip')).toMatchSnapshot()
  })
})

Contributors

barbapapazes

Changelog

c615c - feat: add custom eslint rule to disallow relative imports (#81) on 1/7/2025
18ff6 - feat: add chip component (#24) on 12/15/2024