Chip
chipuiCompact, 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.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/chip.json$npx shadcn@latest add https://uipkge.dev/r/lit/chip.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/chip.json$bunx shadcn@latest add https://uipkge.dev/r/lit/chip.jsonManifest:
https://uipkge.dev/r/lit/chip.jsonInstalls to:src/uip/components/chip/Examples
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
variant | 'default''filled''outlined''outline''elevated''success''warning''destructive' | default | optional |
size | 'sm''default''lg' | default | optional |
wrap | boolean | false | optional |
closable | boolean | false | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-chip': UipChip
'uip-chip-group': UipChipGroup
}
}npm dependencies
Includes
Used by
Files installed (2)
src/uip/components/chip/chip.ts6.1 kB
import { LitElement, css, html, nothing } from 'lit' import { X } from 'lucide' import { cn } from '../../lib/utils' import { tailwind } from '../../lib/styles' import { ThemeController } from '../../lib/theme' import { icon } from '../../lib/icon' import { chipVariants, type ChipVariants } from './chip.variants' type Variant = NonNullable<ChipVariants['variant']> type Size = NonNullable<ChipVariants['size']> /* * Enter/leave keyframes — React's injected `chip-motion-styles` verbatim. * Keyframes have no utility equivalent (same exception as skeleton's shimmer). */ const motion = css` @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; } } ` /** * <uip-chip> — the registry Chip. * * Class strings are React's `chipVariants` verbatim, except `[&>span]:min-w-0` * becomes `[&::slotted(span)]:min-w-0` on the <slot> (a light-DOM <span> isn't * a shadow-tree child). * * Events: `close` (no detail) — React's `onClose`, fired after the 160ms * leave animation when the built-in dismiss button is clicked. Like React, * the chip does not remove itself; the listener does. */ export class UipChip extends LitElement { // :host display can't be a utility class (the host has no template of its own). static styles = [tailwind, motion, css`:host { display: inline-flex; }`] static properties = { variant: { reflect: true }, size: { reflect: true }, wrap: { type: Boolean, reflect: true }, closable: { type: Boolean, reflect: true }, leaving: { state: true }, } variant: Variant = 'default' size: Size = 'default' wrap = false closable = false private leaving = false constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'chip') } willUpdate() { if (this.leaving) this.setAttribute('data-leaving', 'true') else this.removeAttribute('data-leaving') } private handleClose(e: MouseEvent) { e.stopPropagation() if (this.leaving) return this.leaving = true const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches setTimeout(() => this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true })), reduce ? 0 : 160) } render() { return html`<span part="base" data-slot="chip" data-leaving=${this.leaving ? 'true' : nothing} class=${cn( chipVariants({ variant: this.variant, size: this.size, wrap: this.wrap || undefined }), 'chip-enter', this.leaving && 'chip-leave', )} > <slot class="[&::slotted(span)]:min-w-0"></slot> ${this.closable ? html`<button 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=${this.handleClose} > ${icon(X, 'x', 'size-3')} </button>` : nothing} </span>` } } /** * <uip-chip-group> — the registry ChipGroup (`role="group"` flex wrapper). * * React's render-prop (`isSelected` / `toggle`) becomes the `isSelected(value)` * and `toggle(value)` methods with React's selection rules (multiple, max, * mandatory, disabled). `selected` is a string[] property. Unlike React's * controlled-only prop, `toggle` also updates `selected` before emitting. * * Events: `selected-change` (detail: { value: string[] }) — React's * `onSelectedChange`. */ export class UipChipGroup extends LitElement { static styles = [tailwind, css`:host { display: block; }`] static properties = { selected: { type: Array }, multiple: { type: Boolean, reflect: true }, filter: { type: Boolean, reflect: true }, column: { type: Boolean, reflect: true }, mandatory: { type: Boolean }, max: { type: Number }, disabled: { type: Boolean, reflect: true }, } selected: string[] = [] multiple = false filter = false column = false mandatory = false max?: number disabled = false constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-chip-group', 'true') } isSelected(value: string) { return this.selected.includes(value) } toggle(value: string) { if (this.disabled) return const selected = this.selected let next: string[] if (this.multiple) { if (this.isSelected(value)) next = selected.filter((v) => v !== value) else if (this.max && selected.length >= this.max) next = [...selected.slice(1), value] else next = [...selected, value] } else { next = this.isSelected(value) && !this.mandatory ? [] : [value] } this.selected = next this.dispatchEvent(new CustomEvent('selected-change', { detail: { value: next }, bubbles: true, composed: true })) } render() { return html`<div part="base" role="group" class=${cn('flex flex-wrap gap-2', this.column && 'flex-col')} data-chip-group="true" data-multiple=${this.multiple ? 'true' : nothing} data-filter=${this.filter ? 'true' : nothing} > <slot></slot> </div>` } } customElements.get('uip-chip') || customElements.define('uip-chip', UipChip) customElements.get('uip-chip-group') || customElements.define('uip-chip-group', UipChipGroup) declare global { interface HTMLElementTagNameMap { 'uip-chip': UipChip 'uip-chip-group': UipChipGroup } }src/uip/components/chip/chip.variants.ts2.1 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( // text-overflow/ellipsis cannot apply to a flex container's anonymous text, // so truncation is opt-in via a child span (`<Chip><span class="truncate"> // …</span></Chip>`); min-w-0 lets that span shrink inside the flex root. 'inline-flex items-center justify-center gap-1 rounded-full text-xs font-medium w-fit max-w-full whitespace-nowrap shrink-0 [&>span]:min-w-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', outline: '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', }, // Allow multi-line labels. rounded-lg + extra leading keeps a wrapped // chip readable instead of a cramped two-line stadium pill. wrap: { true: 'whitespace-normal h-auto rounded-lg py-1 leading-snug', }, }, defaultVariants: { variant: 'default', size: 'default', }, }, ) export type ChipVariants = VariantProps<typeof chipVariants>
Raw manifest:https://uipkge.dev/r/lit/chip.json