ComponentProduct
Select
Single-choice menu with toolbar, field, and chip trigger variants.
$ npx context-ds add selectUsage
Use it for
Filters (ghost), form values (field), card footers (chip).
Avoid
- Native selects
- More than about 20 options (use a command list)
Dependencies: surface, @radix-ui/react-select, lucide-react, class-variance-authority
Example
examples/select.tsx
import { Calendar } from "lucide-react";import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";export function Product() { return ( <div className="flex flex-wrap items-start gap-6"> <Select defaultValue="30d"> <SelectTrigger aria-label="Range"> <Calendar strokeWidth={1.5} /> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem value="7d">Last 7 days</SelectItem> <SelectItem value="30d">Last 30 days</SelectItem> <SelectItem value="90d">Last 90 days</SelectItem> </SelectContent> </Select> <div className="w-64"> <Select defaultValue="markdown"> <SelectTrigger appearance="field" aria-label="Format"> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem value="markdown" description="Clean text for models">Markdown</SelectItem> <SelectItem value="html" description="The page as rendered">HTML</SelectItem> </SelectContent> </Select> </div> <Select defaultValue="all"> <SelectTrigger appearance="chip" aria-label="Status"> <SelectValue /> </SelectTrigger> <SelectContent className="min-w-40"> <SelectItem value="all">All statuses</SelectItem> <SelectItem value="active">Active</SelectItem> <SelectItem value="paused">Paused</SelectItem> </SelectContent> </Select> </div> );}Source
components/ui/select.tsx
"use client";import * as React from "react";import * as SelectPrimitive from "@radix-ui/react-select";import { Check, ChevronDown, ChevronsUpDown, ChevronUp } from "lucide-react";import { cva, type VariantProps } from "class-variance-authority";import { usePortalContainer } from "@/lib/surface";import { cn } from "@/lib/utils";// The dashboard's single-choice dropdown (SelectMenu): a quiet bordered button in toolbars (`ghost`), a full field beside// inputs (`field`, `compact` for dense panels), or a small gray chip in a card's footer (`chip`); the list is the same// menu panel growing from its trigger's corner, the selected row black with a blue check.const triggerVariants = cva( "flex items-center gap-1.5 whitespace-nowrap outline-none transition-colors duration-150 ease-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-3.5 [&_svg]:shrink-0 [&_svg]:text-fg-subtle", { variants: { appearance: { ghost: "h-8 rounded-control border border-line px-3 text-body-sm text-fg-muted hover:border-line-strong hover:text-fg data-[state=open]:border-line-strong data-[state=open]:text-fg", field: "w-full rounded-control border border-line bg-surface text-fg hover:border-line-strong data-[state=open]:border-brand data-placeholder:text-fg-subtle [&>span]:flex-1 [&>span]:text-left", chip: "h-7 rounded-[calc(var(--ds-radius-control)-0.125rem)] bg-wash-hover px-2.5 text-caption text-fg-muted hover:bg-wash-pill hover:text-fg data-[state=open]:bg-wash-pill data-[state=open]:text-fg", }, // A shorter field, for dense panels. compact: { true: "", false: "" }, }, compoundVariants: [ { appearance: "field", compact: false, className: "h-10 px-3.5 text-body-sm" }, { appearance: "field", compact: true, className: "h-7 px-2.5 text-caption" }, ], defaultVariants: { appearance: "ghost", compact: false }, },);function Select(props: React.ComponentProps<typeof SelectPrimitive.Root>) { return <SelectPrimitive.Root data-slot="select" {...props} />;}function SelectGroup(props: React.ComponentProps<typeof SelectPrimitive.Group>) { return <SelectPrimitive.Group data-slot="select-group" {...props} />;}function SelectValue(props: React.ComponentProps<typeof SelectPrimitive.Value>) { return <SelectPrimitive.Value data-slot="select-value" {...props} />;}function SelectTrigger({ className, appearance, compact, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Trigger> & VariantProps<typeof triggerVariants>) { return ( <SelectPrimitive.Trigger data-slot="select-trigger" className={cn(triggerVariants({ appearance, compact }), className)} {...props}> {children} <SelectPrimitive.Icon asChild> <ChevronsUpDown strokeWidth={1.5} /> </SelectPrimitive.Icon> </SelectPrimitive.Trigger> );}function SelectContent({ className, children, position = "popper", ...props }: React.ComponentProps<typeof SelectPrimitive.Content>) { return ( <SelectPrimitive.Portal container={usePortalContainer()}> <SelectPrimitive.Content data-slot="select-content" position={position} sideOffset={6} className={cn( "relative z-50 max-h-80 min-w-52 origin-(--radix-select-content-transform-origin) animate-pop-in overflow-hidden rounded-shell bg-surface p-(--ds-space-frame) shadow-floating", position === "popper" && "w-full min-w-(--radix-select-trigger-width)", className, )} {...props} > <SelectPrimitive.ScrollUpButton className="flex h-6 items-center justify-center text-fg-subtle"> <ChevronUp className="size-3.5" /> </SelectPrimitive.ScrollUpButton> <SelectPrimitive.Viewport className="flex flex-col gap-0.5">{children}</SelectPrimitive.Viewport> <SelectPrimitive.ScrollDownButton className="flex h-6 items-center justify-center text-fg-subtle"> <ChevronDown className="size-3.5" /> </SelectPrimitive.ScrollDownButton> </SelectPrimitive.Content> </SelectPrimitive.Portal> );}function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) { return <SelectPrimitive.Label data-slot="select-label" className={cn("px-2.5 pt-1 pb-1.5 text-caption text-fg-subtle", className)} {...props} />;}function SelectItem({ className, children, description, ...props }: React.ComponentProps<typeof SelectPrimitive.Item> & { description?: React.ReactNode }) { return ( <SelectPrimitive.Item data-slot="select-item" className={cn( "relative flex w-full cursor-default items-start gap-2 rounded-control py-1.5 pr-8 pl-2.5 text-body-sm leading-5 text-fg-muted outline-none select-none transition-colors duration-150 data-[disabled]:pointer-events-none data-[disabled]:opacity-40 data-[highlighted]:bg-wash-hover data-[state=checked]:text-fg", className, )} {...props} > <span className="min-w-0 flex-1"> <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> {description && <span className="mt-0.5 block text-caption text-fg-subtle">{description}</span>} </span> <SelectPrimitive.ItemIndicator className="absolute top-2 right-2.5 text-brand"> <Check className="size-3.5" strokeWidth={1.75} /> </SelectPrimitive.ItemIndicator> </SelectPrimitive.Item> );}function SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>) { return <SelectPrimitive.Separator data-slot="select-separator" className={cn("-mx-1.5 my-1 h-px bg-line-subtle", className)} {...props} />;}export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue };Reference: context-dashboard c0ddf7a src/components/ui/menu.tsx.