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.

Link chip
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>
  )
}