
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 React ->$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/chip.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/chip.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/chip.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/chip.jsonnpx shadcn-vue@latest add @uipkge/chipInstalls to:app/components/ui/chip/| Name | Type / Values | Default | Required |
|---|---|---|---|
variant | 'default''filled''outlined''outline''elevated''success''warning''destructive' | — | optional |
size | 'sm''default''lg' | — | optional |
wrapAllow the label to wrap onto multiple lines instead of clipping. | boolean | — | optional |
class | HTMLAttributes['class'] | — | optional |
closable | boolean | — | optional |
<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' | 'outline' | 'elevated' | 'success' | 'warning' | 'destructive'
size?: 'sm' | 'default' | 'lg'
/** Allow the label to wrap onto multiple lines instead of clipping. */
wrap?: boolean
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, wrap }), '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>
<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>
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 { 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