Component
Select
A white 40px field with the Shadow/S ring and an ink caret that opens the console's white menu.
npx -y github:anwarlemu/inkbox-brand-cli#release add selectUsage
Use to choose one value from a list of five or more (Any State for a phone number, Shared or Dedicated for an iMessage line). Give it a Label or an aria-label, a placeholder when nothing is chosen, and group long lists with SelectGroup and SelectLabel. Control it with value and onValueChange (values are non-empty strings), and give the root a name so a form submits the value with FormData.
Avoid
- A field edge instead of the Shadow/S ring
- A dark or purple list
- Checkmarks at the left of the items
- Animating items as they are highlighted
Exports: Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue. This component is a client component.
Dependencies: surface, radix-ui, @phosphor-icons/react.
Example
"use client";import * as React from "react";import { EnvelopeSimpleIcon, PhoneIcon, ChatCircleIcon } from "@phosphor-icons/react/ssr";import { Button } from "@/components/ui/button";import { Label } from "@/components/ui/label";import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue } from "@/components/ui/select";export function Preview() { const [state, setState] = React.useState("any"); const [saved, setSaved] = React.useState<string>(); return ( <form className="grid w-full max-w-114.5 gap-1" onSubmit={(event) => { event.preventDefault(); setSaved(String(new FormData(event.currentTarget).get("state"))); }} > <Label htmlFor="any-state">Phone number state</Label> <Select name="state" value={state} onValueChange={setState}> <SelectTrigger id="any-state"> <SelectValue placeholder="Choose a state" /> </SelectTrigger> <SelectContent> <SelectItem value="any">Any State</SelectItem> <SelectSeparator /> <SelectGroup> <SelectLabel>West</SelectLabel> <SelectItem value="ca">California</SelectItem> <SelectItem value="or">Oregon</SelectItem> <SelectItem value="wa">Washington</SelectItem> </SelectGroup> <SelectGroup> <SelectLabel>East</SelectLabel> <SelectItem value="ny">New York</SelectItem> <SelectItem value="ma">Massachusetts</SelectItem> <SelectItem value="fl" disabled> Florida (no numbers left) </SelectItem> </SelectGroup> </SelectContent> </Select> <div className="mt-3 flex items-center gap-3"> <Button type="submit">Save</Button> <p aria-live="polite" className="text-body text-fg-muted"> {saved ? `Saved: ${saved}` : ""} </p> </div> </form> );}export function WithIcons() { return ( <Select defaultValue="email"> <SelectTrigger aria-label="Channel" className="w-60"> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem value="email"> <EnvelopeSimpleIcon weight="fill" /> Email </SelectItem> <SelectItem value="phone"> <PhoneIcon weight="fill" /> Phone </SelectItem> <SelectItem value="imessage"> <ChatCircleIcon weight="fill" /> iMessage </SelectItem> </SelectContent> </Select> );}export function States() { return ( <div className="flex flex-wrap items-center gap-3"> <Select> <SelectTrigger aria-label="Line" size="sm" className="w-44"> <SelectValue placeholder="Choose a line" /> </SelectTrigger> <SelectContent> <SelectItem value="shared">Shared</SelectItem> <SelectItem value="dedicated">Dedicated</SelectItem> </SelectContent> </Select> <Select defaultValue="shared" disabled> <SelectTrigger aria-label="Disabled" className="w-44"> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem value="shared">Shared</SelectItem> </SelectContent> </Select> <Select> <SelectTrigger aria-label="Invalid" aria-invalid className="w-44"> <SelectValue placeholder="Required" /> </SelectTrigger> <SelectContent> <SelectItem value="edge">Edge</SelectItem> </SelectContent> </Select> </div> );}Source
"use client";import * as React from "react";import { Select as SelectPrimitive } from "radix-ui";import { CaretDownIcon, CaretUpIcon, CheckIcon } from "@phosphor-icons/react/ssr";import { usePortalContainer } from "@/lib/surface";import { cn } from "@/lib/utils";// The console's select (Figma 427:2715, Create Agent Identity: "Any State" under Phone Number, "Shared" under// iMessage): a white 40px field with 8px corners and the Shadow/S ring instead of an edge, the value in 14px ink 14px// in, a 16px bold caret 12px from the right; the placeholder is the light purple. `size="sm"` is 36px. The list is the// console's menu: white, 8px corners, the floating shadow, 4px inside; 36px rows with 8px corners, 14px ink, the// highlighted row on the lilac and a purple check at the right of the chosen one. It opens from the trigger (96// percent and a fade, 200ms) and closes faster.function Select(props: React.ComponentProps<typeof SelectPrimitive.Root>) { return <SelectPrimitive.Root data-slot="select" {...props} />;}function SelectGroup({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) { return <SelectPrimitive.Group data-slot="select-group" className={cn("scroll-my-1", className)} {...props} />;}function SelectValue(props: React.ComponentProps<typeof SelectPrimitive.Value>) { return <SelectPrimitive.Value data-slot="select-value" {...props} />;}function SelectTrigger({ className, size = "default", children, ...props }: React.ComponentProps<typeof SelectPrimitive.Trigger> & { size?: "sm" | "default" }) { return ( <SelectPrimitive.Trigger data-slot="select-trigger" data-size={size} className={cn( "flex w-full min-w-0 cursor-pointer items-center justify-between gap-2 rounded-control bg-surface pr-3 pl-3.5 font-sans text-body leading-tight whitespace-nowrap text-fg shadow-s outline-none transition-[box-shadow] duration-150 ease-out focus-visible:shadow-[var(--ds-shadow-s),var(--ds-shadow-focus)] disabled:cursor-not-allowed disabled:bg-subtle disabled:text-fg-muted aria-invalid:shadow-[0_0_0_1px_var(--ds-color-status-danger)] data-placeholder:text-fg-subtle data-[size=default]:h-10 data-[size=sm]:h-9 motion-reduce:transition-none *:data-[slot=select-value]:flex *:data-[slot=select-value]:min-w-0 *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 *:data-[slot=select-value]:truncate [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5.5", className, )} {...props} > {children} <SelectPrimitive.Icon asChild> <CaretDownIcon weight="bold" className="size-4 text-fg" /> </SelectPrimitive.Icon> </SelectPrimitive.Trigger> );}function SelectContent({ className, children, position = "popper", align = "center", ...props }: React.ComponentProps<typeof SelectPrimitive.Content>) { const container = usePortalContainer(); return ( <SelectPrimitive.Portal container={container}> <SelectPrimitive.Content data-slot="select-content" className={cn( "relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-control bg-surface text-fg shadow-floating data-[state=closed]:animate-pop-out data-[state=open]:animate-pop-in", position === "popper" && "w-full min-w-(--radix-select-trigger-width) data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className, )} position={position} align={align} {...props} > <SelectScrollUpButton /> <SelectPrimitive.Viewport data-position={position} className={cn("p-1", position === "popper" && "h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width) scroll-my-1")}> {children} </SelectPrimitive.Viewport> <SelectScrollDownButton /> </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-2 pb-1 text-body-sm leading-tight text-fg-label", className)} {...props} />;}function SelectItem({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>) { return ( <SelectPrimitive.Item data-slot="select-item" className={cn( "relative flex h-9 w-full cursor-default items-center gap-2.5 rounded-control pr-9 pl-2.5 font-sans text-body leading-tight text-fg outline-none select-none data-disabled:pointer-events-none data-disabled:text-fg-faint data-highlighted:bg-subtle [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5.5 [&_svg:not([class*='text-'])]:text-icon-secondary *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2.5", className, )} {...props} > <span data-slot="select-item-indicator" className="absolute right-2.5 flex size-4 items-center justify-center"> <SelectPrimitive.ItemIndicator> <CheckIcon weight="bold" className="size-4 text-fg-brand" /> </SelectPrimitive.ItemIndicator> </span> <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> </SelectPrimitive.Item> );}function SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>) { return <SelectPrimitive.Separator data-slot="select-separator" className={cn("pointer-events-none -mx-1 my-1 h-px bg-line-subtle", className)} {...props} />;}function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) { return ( <SelectPrimitive.ScrollUpButton data-slot="select-scroll-up-button" className={cn("flex h-6 cursor-default items-center justify-center bg-surface text-fg-muted", className)} {...props}> <CaretUpIcon weight="bold" className="size-3.5" /> </SelectPrimitive.ScrollUpButton> );}function SelectScrollDownButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) { return ( <SelectPrimitive.ScrollDownButton data-slot="select-scroll-down-button" className={cn("flex h-6 cursor-default items-center justify-center bg-surface text-fg-muted", className)} {...props}> <CaretDownIcon weight="bold" className="size-3.5" /> </SelectPrimitive.ScrollDownButton> );}export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue };Reference: Figma 427:2715: Any State and Shared in Create Agent Identity (Modal States); the list is extrapolated as the console's menu.