
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
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.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/chip.json$npx shadcn@latest add https://uipkge.dev/r/react/chip.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/chip.json$bunx shadcn@latest add https://uipkge.dev/r/react/chip.jsonnpx shadcn@latest add @uipkge-react/chipInstalls to:components/ui/chip/| Name | Type / Values | Default | Required |
|---|---|---|---|
variant | 'default''filled''outlined''outline''elevated''success''warning''destructive' | default | optional |
size | 'sm''default''lg' | default | optional |
wrap | 'true' | — | optional |
closable | boolean | — | optional |
onClose | () => void | — | optional |
'use client'
import * as React from 'react'
import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
import { chipVariants, type ChipVariants } from './chip.variants'
export interface ChipProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'onClose'>, ChipVariants {
closable?: boolean
onClose?: () => void
}
const STYLE_ID = 'chip-motion-styles'
const STYLE_CONTENT = `
@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;
}
}
`
const Chip = React.forwardRef<HTMLSpanElement, ChipProps>(
({ className, variant, size, wrap, closable, onClose, children, ...props }, ref) => {
const [leaving, setLeaving] = React.useState(false)
React.useLayoutEffect(() => {
if (typeof document === 'undefined') return
let el = document.getElementById(STYLE_ID) as HTMLStyleElement | null
if (!el) {
el = document.createElement('style')
el.id = STYLE_ID
document.head.appendChild(el)
}
if (el.textContent !== STYLE_CONTENT) el.textContent = STYLE_CONTENT
}, [])
function handleClose(e: React.MouseEvent) {
e.stopPropagation()
if (leaving) return
setLeaving(true)
const reduce = typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches
window.setTimeout(
() => {
onClose?.()
},
reduce ? 0 : 160,
)
}
return (
<span
ref={ref}
data-uipkge=""
data-slot="chip"
data-leaving={leaving || undefined}
className={cn(chipVariants({ variant, size, wrap }), 'chip-enter', leaving && 'chip-leave', className)}
{...props}
>
{children}
{closable ? (
<button
type="button"
aria-label="Remove item"
className="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"
onClick={handleClose}
>
<X className="size-3" aria-hidden="true" />
</button>
) : null}
</span>
)
},
)
Chip.displayName = 'Chip'
export interface ChipGroupRenderProps {
selected: string[]
multiple: boolean
filter: boolean
isSelected: (value: string) => boolean
toggle: (value: string) => void
}
export interface ChipGroupProps {
className?: string
selected?: string[]
multiple?: boolean
filter?: boolean
column?: boolean
mandatory?: boolean
max?: number
disabled?: boolean
onSelectedChange?: (value: string[]) => void
children?: (props: ChipGroupRenderProps) => React.ReactNode
}
const ChipGroup = React.forwardRef<HTMLDivElement, ChipGroupProps>(
(
{
className,
selected = [],
multiple = false,
filter = false,
column = false,
mandatory = false,
max,
disabled = false,
onSelectedChange,
children,
},
ref,
) => {
const isSelected = (value: string) => selected.includes(value)
const toggle = (value: string) => {
if (disabled) return
let newSelected: string[]
if (multiple) {
if (isSelected(value)) {
newSelected = selected.filter((v) => v !== value)
} else {
if (max && selected.length >= max) {
newSelected = [...selected.slice(1), value]
} else {
newSelected = [...selected, value]
}
}
} else {
if (isSelected(value) && !mandatory) {
newSelected = []
} else {
newSelected = [value]
}
}
onSelectedChange?.(newSelected)
}
return (
<div
ref={ref}
role="group"
className={cn('flex flex-wrap gap-2', column && 'flex-col', className)}
data-chip-group="true"
data-multiple={multiple || undefined}
data-filter={filter || undefined}
>
{children?.({ selected, multiple, filter, isSelected, toggle })}
</div>
)
},
)
ChipGroup.displayName = 'ChipGroup'
export { Chip, ChipGroup }
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>
export { Chip, ChipGroup, type ChipProps, type ChipGroupProps, type ChipGroupRenderProps } from './chip'
export { chipVariants, type ChipVariants } from './chip.variants'
Raw manifest:https://uipkge.dev/r/react/chip.json