Chip
An indicator of a numeric value or a state.
Demo
Related Theme
This requires the following theme to be installed:
Component
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>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
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
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:
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()
})
})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()
})
})