Components

Base Components

Chip Menu

A select control that looks like a chip (badge) and opens a dropdown menu. Built from Badge and DropdownMenu.

Example

The chip menu shows the current selection (or a placeholder) in a Badge and opens a dropdown when clicked.

Installation

Install the following dependencies:

npm install lucide-react

Create a chip-menu.tsx file and paste the following code into it.

"use client";import * as React from "react";import { ChevronDownIcon, ChevronLeftIcon, PlusCircleIcon } from "lucide-react";import { Badge } from "@/components/ui/badge";import {  DropdownMenu,  DropdownMenuContent,  DropdownMenuItem,  DropdownMenuTrigger,} from "@/components/ui/dropdown-menu";import { cn } from "@/lib/utils";export interface ChipMenuOption {  value: string;  label: React.ReactNode;  disabled?: boolean;}export interface ChipMenuProps {  /** Currently selected value (must match an option's value). */  value: string | null;  /** Callback when selection changes. */  onValueChange: (value: string) => void;  /** Menu options. */  options: ChipMenuOption[];  /** Placeholder when no value is selected. */  placeholder?: string;  /** Badge variant. */  variant?: React.ComponentProps<typeof Badge>["variant"];  /** Optional class for the trigger badge. */  className?: string;  /** Optional class for the dropdown content. */  contentClassName?: string;  /** Whether the chip is disabled. */  disabled?: boolean;}function ChipMenu({  value,  onValueChange,  options,  placeholder = "Select…",  variant = "default",  className,  contentClassName,  disabled = false,}: ChipMenuProps) {  const selected = options.find((o) => o.value === value);  const label = selected ? selected.label : "";  const [open, setOpen] = React.useState(false);  const handleReset = (e: React.MouseEvent) => {    e.preventDefault();    e.stopPropagation();    onValueChange("");  };  return (    <DropdownMenu open={open} onOpenChange={setOpen}>      {label ? (        <Badge          variant={variant}          className={cn(            "py-[4px] cursor-pointer gap-1 pr-1 transition-opacity hover:opacity-90 data-[state=open]:opacity-90",            !selected && "text-muted-foreground",            className          )}        >          <span            className="px-[2px] text-chip-menu-placeholder"            onClick={handleReset}          >            <PlusCircleIcon className={cn("size-3 opacity-70 transition-transform", label && "rotate-45")} />{" "}          </span>          {placeholder && (            <span className="text-chip-menu-placeholder">{placeholder}</span>          )}          <span className="px-[6px] text-chip-menu-placeholder">|</span>          <DropdownMenuTrigger            disabled={disabled}            className="inline-flex items-center gap-1 h-auto border-0 bg-transparent p-0 outline-none focus:ring-0 focus-visible:ring-0"          >            <span className="text-primary">{label}</span>            <ChevronDownIcon className="size-3 opacity-70" />          </DropdownMenuTrigger>        </Badge>      ) : (        <DropdownMenuTrigger          disabled={disabled}          className="inline-flex h-auto border-0 bg-transparent p-0 outline-none focus:ring-0 focus-visible:ring-0"        >          <Badge            variant={variant}            className={cn(              "py-[4px] cursor-pointer gap-1 pr-1 transition-opacity hover:opacity-90 data-[state=open]:opacity-90",              !selected && "text-muted-foreground",              className            )}          >            <span className="px-[2px] text-chip-menu-placeholder">              <PlusCircleIcon className="size-3 opacity-70" />{" "}            </span>            {placeholder && (              <span className="text-chip-menu-placeholder">{placeholder}</span>            )}            <span className="text-primary">{label}</span>            <ChevronDownIcon className="size-3 opacity-70" />          </Badge>        </DropdownMenuTrigger>      )}      <DropdownMenuContent className={contentClassName} align="start">        {options.map((opt) => (          <DropdownMenuItem            key={opt.value}            disabled={opt.disabled}            onSelect={() => onValueChange(opt.value)}          >            {opt.label}          </DropdownMenuItem>        ))}      </DropdownMenuContent>    </DropdownMenu>  );}export { ChipMenu };

Check the import paths to ensure they match your project setup.

The chip menu uses the Badge and DropdownMenu components. Ensure those are installed if you copy the source.

Usage

Control the selected value with value and onValueChange. Pass options as an array of { value, label, disabled? }.
import { ChipMenu } from "@/components/ui/chip-menu";

const [status, setStatus] = useState<string | null>(null);

<ChipMenu
  value={status}
  onValueChange={setStatus}
  options={[
    { value: "new", label: "New" },
    { value: "active", label: "Active" },
    { value: "done", label: "Done" },
  ]}
  placeholder="Status"
  variant="default"
/>

Examples

With placeholder

When no value is selected, the chip shows the placeholder in muted text.

Variants

Use the variant prop to match Badge variants (default, secondary, outline, etc.).
Secondary|

API Reference

ChipMenu

value*string | null
Default: -

Currently selected value (must match an option's value).

onValueChange*(value: string) => void
Default: -

Callback when selection changes.

options*ChipMenuOption[]
Default: -

Menu options.

placeholderstring
Default: Select…

Placeholder when no value is selected.

variant"link" | "default" | "secondary" | "destructive" | "outline" | "ghost" | "info" | "positive" | "negative" | "warning" | "urgent" | null
Default: default

Badge variant.

classNamestring
Default: -

Optional class for the trigger badge.

contentClassNamestring
Default: -

Optional class for the dropdown content.

disabledboolean
Default: false

Whether the chip is disabled.

PropTypeDefaultDescription
value*string | null-Currently selected value (must match an option's value).
onValueChange*(value: string) => void-Callback when selection changes.
options*ChipMenuOption[]-Menu options.
placeholderstringSelect…Placeholder when no value is selected.
variant"link" | "default" | "secondary" | "destructive" | "outline" | "ghost" | "info" | "positive" | "negative" | "warning" | "urgent" | nulldefaultBadge variant.
classNamestring-Optional class for the trigger badge.
contentClassNamestring-Optional class for the dropdown content.
disabledbooleanfalseWhether the chip is disabled.
PropTypeDescription
valuestring | nullCurrently selected option value.
onValueChange(value: string) => voidCalled when the user picks an option.
optionsChipMenuOption[]{ value, label, disabled? }[].
placeholderstringShown when no value is selected. Default: "Select…".
variantBadge variantBadge style. Default: "default".
classNamestringOptional class for the trigger badge.
contentClassNamestringOptional class for the dropdown content.
disabledbooleanDisables the chip and menu.

On this page