Chips
Chips help people enter information, make selections, filter content, or trigger actions
Usage
Chips help people enter information, make selections, filter content, or trigger actions. They’re best used to help users accomplish their current task faster and easier.
Instalation
Run the following command if is not already installed:
npm i @radix-ui/react-slot
Copy and paste the following code into your project.
'use client'
import * as React from 'react'
import { Presence } from '@radix-ui/react-presence'
import * as SelectPrimitive from '@radix-ui/react-select'
import { Slot, Slottable } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { createSafeContext } from '@/lib/createSafeContext'
import { cn } from '@/lib/utils'
const chipVariants = cva(
'group relative z-0 flex px-2 h-8 items-center justify-center overflow-hidden rounded-sm outline-none disabled:pointer-events-none disabled:border-onSurface/12',
{
variants: {
variant: {
assist: 'border text-onSurface',
elevated:
'transition-shadow border text-onSurface bg-surfaceContainerLow shadow-sm active:shadow-xs',
filter:
'transition-all border data-[selected]:border-0 text-onSurfaceVariant data-[selected]:border-secondaryContainer data-[selected]:bg-secondaryContainer data-[selected]:text-onSecondaryContainer',
input: 'border text-onSurfaceVariant',
suggestion:
'border text-onSurfaceVariant data-[selected]:border-secondaryContainer data-[selected]:bg-secondaryContainer data-[selected]:text-onSecondaryContainer',
},
},
defaultVariants: {
variant: 'assist',
},
}
)
const stateLayerVariants = cva(
'transition-opacity absolute inset-0 z-[-1] opacity-0 group-hover:opacity-8 group-focus:opacity-12 group-active:opacity-12',
{
variants: {
variant: {
assist: 'bg-onSurface',
elevated: 'bg-onSurface',
filter:
'bg-onSurfaceVariant group-data-[selected]:bg-onSecondaryContainer',
input:
'bg-onSurfaceVariant group-data-[selected]:bg-onSecondaryContainer',
suggestion:
'bg-onSurfaceVariant group-data-[selected]:bg-onSecondaryContainer',
},
},
defaultVariants: {
variant: 'assist',
},
}
)
const chipIconVariants = cva(
'h-full w-full block group-disabled:text-onSurface/38 group-data-[selected]:group-disabled:text-onSurface/38 [&>i]:text-[18px]',
{
variants: {
variant: {
assist: 'text-primary',
elevated: 'text-primary',
filter: 'group-data-[selected]:text-onSurfaceVariant',
input: '',
suggestion: '',
},
},
defaultVariants: {
variant: 'assist',
},
}
)
type ChipContextType = {
selected?: boolean
} & VariantProps<typeof chipVariants>
const [ChipProvider, useChipContext] = createSafeContext<ChipContextType>({
name: 'Chip',
})
interface ButtonChipProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof chipVariants> {
selected?: boolean
asChild?: boolean
}
const ChipRoot = React.forwardRef<HTMLButtonElement, ButtonChipProps>(
({ className, variant, children, selected, asChild, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<ChipProvider value={{ selected, variant }}>
<Comp
ref={ref}
data-selected={selected ? '' : undefined}
className={cn(chipVariants({ variant, className }))}
{...props}
>
<Slottable>{children}</Slottable>
<span className={stateLayerVariants({ variant })} />
</Comp>
</ChipProvider>
)
}
)
ChipRoot.displayName = 'Chip'
const ChipLabel = React.forwardRef<
HTMLSpanElement,
React.HTMLAttributes<HTMLSpanElement>
>(({ className, ...props }, ref) => {
return (
<span
ref={ref}
className={cn(
'truncate whitespace-nowrap px-2 text-label-lg text-current group-disabled:text-onSurface/38 group-data-[selected]:group-disabled:text-onSurface/38',
className
)}
{...props}
/>
)
})
ChipLabel.displayName = 'ChipLabel'
type ChipIconProps = {
perennial?: boolean
}
const ChipIcon = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & ChipIconProps
>(({ className, perennial, ...props }, ref) => {
const { selected, variant } = useChipContext()
const isVisible = perennial ?? selected ?? true
return (
<span
className={cn('h-[18px] w-0 transition-[width]', isVisible && 'w-[18px]')}
>
<Presence present={isVisible}>
<div
ref={ref}
className={cn(chipIconVariants({ variant, className }))}
{...props}
/>
</Presence>
</span>
)
})
ChipIcon.displayName = 'ChipIcon'
const Chip = Object.assign(ChipRoot, {
Label: ChipLabel,
Icon: ChipIcon,
})
/**
* Chip Select
*/
const ChipSelectRoot = SelectPrimitive.Root
const ChipSelectValue = SelectPrimitive.Value
const ChipSelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, asChild = true, ...props }, ref) => (
<SelectPrimitive.Trigger ref={ref} asChild={asChild} {...props}>
{children}
</SelectPrimitive.Trigger>
))
ChipSelectTrigger.displayName = 'ChipSelectTrigger'
const ChipSelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
'relative z-50 min-w-[8rem] overflow-hidden rounded-md bg-surfaceContainer text-onSurfaceVariant shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
position === 'popper' &&
'data-[side=bottom]:translate-y-0 data-[side=left]:-translate-x-0 data-[side=right]:translate-x-0 data-[side=top]:-translate-y-0',
className
)}
position={position}
{...props}
>
<SelectPrimitive.Viewport
className={cn(
'py-2',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
)}
>
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
ChipSelectContent.displayName = 'ChipSelectContent'
const ChipSelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
'group relative flex h-12 cursor-pointer select-none items-center gap-4 rounded-none px-3 py-2 text-onSurface outline-none transition-colors hover:bg-onSurface/8 focus:bg-onSurface/8 active:bg-onSurface/12 aria-selected:bg-onSurface/8 data-[disabled]:pointer-events-none data-[disabled]:text-onSurface/38',
className
)}
{...props}
>
{children}
</SelectPrimitive.Item>
))
ChipSelectItem.displayName = 'ChipSelectItem'
const ChipSelectItemText = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ItemText>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ItemText>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ItemText
ref={ref}
className={cn('text-body-md', className)}
{...props}
/>
))
const ChipSelect = Object.assign(ChipSelectRoot, {
Value: ChipSelectValue,
Trigger: ChipSelectTrigger,
Content: ChipSelectContent,
Item: ChipSelectItem,
ItemText: ChipSelectItemText,
})
export {
Chip,
ChipSelect,
ChipRoot,
ChipLabel,
ChipIcon,
ChipSelectRoot,
ChipSelectValue,
ChipSelectTrigger,
ChipSelectContent,
ChipSelectItem,
ChipSelectItemText,
chipVariants,
stateLayerVariants,
chipIconVariants,
}
export type { ButtonChipProps }
Update the import paths to match your project setup.
Examples
Assist chips
Assist chips represent smart or automated actions that can span multiple apps, such as opening a calendar event from the home screen.
import { Chip } from '@/components/ui/chip'
import { Icon } from '@/components/ui/icon'
export const AssistChip = () => {
return (
<div className="grid w-full grid-cols-1 sm:grid-cols-2">
{/* Outlined Chip when using on solid background */}
<div className="flex w-full items-center justify-center p-6">
<Chip>
<Chip.Icon>
<Icon symbol="calendar_today" />
</Chip.Icon>
<Chip.Label>Add to my itinerary</Chip.Label>
</Chip>
</div>
{/* Elevated Chip when using on image background */}
<div className="relative flex w-full items-center justify-center overflow-hidden rounded-md bg-[url(https://images.unsplash.com/photo-1574484184081-afea8a62f9c0?q=80&w=3081&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D)] bg-cover bg-center p-6">
<span className="bg-black/4 absolute inset-0 z-0 rounded-md backdrop-blur-[2px]" />
<Chip variant="elevated" className="relative z-[1]">
<Chip.Icon>
<Icon symbol="calendar_today" />
</Chip.Icon>
<Chip.Label>Add to my itinerary</Chip.Label>
</Chip>
</div>
</div>
)
}
import { Avatar } from '@/components/ui/avatar'
import { Chip } from '@/components/ui/chip'
export const AssistChipWithAvatar = () => {
return (
<Chip className="rounded-full pl-1">
<Avatar className="h-6">
<Avatar.Image src="https://github.com/jepricreations.png" />
<Avatar.Fallback className="text-label-sm">JC</Avatar.Fallback>
</Avatar>
<Chip.Label>Share with JC</Chip.Label>
</Chip>
)
}
Filter chips
Filter chips use tags or descriptive words to filter content. They can be a good alternative to segmented buttons or checkboxes when viewing a list or search results.
import { useState } from 'react'
import { Chip } from '@/components/ui/chip'
import { Icon } from '@/components/ui/icon'
export const FilterChip = () => {
const [selected, setSelected] = useState(false)
return (
<Chip
variant="filter"
selected={selected}
onClick={() => setSelected(!selected)}
>
<Chip.Icon
className={`animate-out zoom-out-0 group-data-[selected]:animate-in group-data-[selected]:zoom-in-50`}
>
<Icon symbol="check" />
</Chip.Icon>
<Chip.Label>Filter chip</Chip.Label>
</Chip>
)
}
Filter chip with menu
Filter chips can open a menu for more filtering options.
import { useState } from 'react'
import { Chip, ChipSelect } from '@/components/ui/chip'
import { Icon } from '@/components/ui/icon'
export const FilterChipWithMenu = () => {
const [selected, setSelected] = useState('running')
return (
<ChipSelect onValueChange={setSelected}>
<ChipSelect.Trigger>
<Chip variant="filter" selected>
<Chip.Icon>
<Icon symbol="check" />
</Chip.Icon>
<Chip.Label className="capitalize">{selected}</Chip.Label>
<Chip.Icon>
<Icon symbol="arrow_drop_down" />
</Chip.Icon>
</Chip>
</ChipSelect.Trigger>
<ChipSelect.Content align="start">
<ChipSelect.Item value="running">
<Icon symbol="directions_run" />
<ChipSelect.ItemText>Running</ChipSelect.ItemText>
</ChipSelect.Item>
<ChipSelect.Item value="walking">
<Icon symbol="directions_walk" />
<ChipSelect.ItemText>Walking</ChipSelect.ItemText>
</ChipSelect.Item>
<ChipSelect.Item value="hiking">
<Icon symbol="hiking" />
<ChipSelect.ItemText>Hiking</ChipSelect.ItemText>
</ChipSelect.Item>
<ChipSelect.Item value="cycling">
<Icon symbol="directions_bike" />
<ChipSelect.ItemText>Cycling</ChipSelect.ItemText>
</ChipSelect.Item>
</ChipSelect.Content>
</ChipSelect>
)
}
Input chips
Input chips represent discrete pieces of information entered by a user, such as Gmail contacts or filter options within a search field.
import { Chip } from '@/components/ui/chip'
import { Icon } from '@/components/ui/icon'
export const InputChip = () => {
const [selected, setSelected] = useState(true)
return (
<Chip
variant="input"
selected={selected}
onClick={() => setSelected(!selected)}
>
<Chip.Icon perennial>
<Icon symbol="calendar_month" />
</Chip.Icon>
<Chip.Label>Input chip</Chip.Label>
<Chip.Icon>
<Icon symbol="close" />
</Chip.Icon>
</Chip>
)
}
Suggestion chips
Suggestion chips help narrow a user’s intent by presenting dynamically generated suggestions, such as possible responses or search filters.
import { Chip } from '@/components/ui/chip'
export const SuggestionChip = () => {
const [selected, setSelected] = useState(true)
return (
<div className="flex gap-2">
<Chip
variant="suggestion"
selected={selected}
onClick={() => setSelected(!selected)}
>
<Chip.Label>Suggestion chip</Chip.Label>
</Chip>
<Chip asChild variant="suggestion">
<a href="#">
<Chip.Label>Link chip</Chip.Label>
</a>
</Chip>
</div>
)
}