Morph select

A select whose trigger grows into the list, with a gliding highlight and type-ahead.

pnpm dlx shadcn@latest add @uiarc/morph-select
Live · keyboard ready

Checkout button stalls on Safari 17

Tapping Pay leaves the spinner running after the card is approved. Reproduced on iOS and macOS.

Status
Assignee
Priority
  1. Emma Collins opened this issue
  • Form fields and property panels where one value is picked from a known list.
  • Grouped lists like time zones, assignees, or projects, with optional icons and meta.
  • Compact toolbars where the picker should look like a pill until opened.
  • Use combobox when users type free text or the list is very long and remote.
  • Use multi-select when more than one value can be chosen.
  • Use dropdown-menu for actions rather than a stored value.

Installation

Add Morph select with the shadcn CLI, or copy the source by hand.

pnpm dlx shadcn@latest add @uiarc/morph-select

Adds the component and its local dependencies, and installs motion, lucide-react. First time? Add the @uiarc registry to components.json, or use the full URL:

example.tsx
import { useState } from "react";import { MorphSelect } from "@/components/arc/morph-select/morph-select"; export function TimezoneField() {  const [zone, setZone] = useState<string | null>("europe/zurich");  return (    <MorphSelect      label="Time zone"      name="timezone"      value={zone}      onValueChange={setZone}      items={[        { label: "Europe", options: [          { value: "europe/london", label: "London", meta: "UTC+0" },          { value: "europe/zurich", label: "Zurich", meta: "UTC+1", keywords: "switzerland" },        ] },        { label: "Americas", options: [          { value: "america/new_york", label: "New York", meta: "UTC−5" },          { value: "america/los_angeles", label: "Los Angeles", meta: "UTC−8" },        ] },      ]}    />  );}

MorphSelect

A select whose trigger grows into the option list, with groups, type-ahead, optional search, and a label that flies back into the trigger.

PropTypeDefaultDescription
labelRequiredstring–Visible label, also the accessible name of the trigger and the list.
hideLabelbooleanfalseKeeps the label for assistive technology only.
itemsRequiredMorphSelectItem[]–Options { value, label, icon?, meta?, keywords?, disabled? } or groups { label, options }, in display order.
valuestring | null–Controlled selected value.
defaultValuestring | nullnullUncontrolled starting value.
onValueChange(value: string, option: MorphSelectOption) => void–Called with the new value and its option.
placeholderstring"Select"Trigger text with no selection.
searchableboolean | "auto""auto"Shows a search field in place of the trigger when open. auto turns it on above eight options.
searchPlaceholderstring–Search field placeholder. Defaults to the selected label, then "Search".
panelWidthnumber272Width of the open surface in px. Never narrower than the trigger.
maxListHeightnumber296Tallest the option list gets before it scrolls, in px.
align"start" | "end""start"Which edge of the trigger stays put while the surface grows.
namestring–Adds a hidden input for native form submission.
disabledbooleanfalseDisables the trigger.
classNamestring–Extra class on the root.
EnterorSpaceorArrowDownorArrowUp
Opens the list on the selected option; Enter or Space picks the active option when open.
ArrowDownorArrowUp
Moves the active option, skipping disabled ones.
PageDownorPageUp
Moves eight options at a time.
HomeorEnd
Moves to the first or last option; when closed, opens on it.
Alt+ArrowUp
Picks the active option and closes.
Printable characters
Type-ahead jumps to a matching label, or starts a search when search is on.
Escape
Closes and returns focus to the trigger.
Tab
Closes and moves focus on without picking.
  • The trigger is a role="combobox" with aria-haspopup="listbox", aria-expanded, and aria-activedescendant pointing at the active option.
  • With search on, the input takes over as the combobox with aria-autocomplete="list", and the trigger turns inert.
  • Options are role="option" with aria-selected and aria-disabled; groups are role="group" labelled by their heading.
  • A polite status announces the number of search results or No matches.
  • The trigger surface grows into the list on a physical spring with slight bounce and folds back without overshoot; the corner radius morphs with it.
  • One highlight glides between options; the chosen label lifts out of the list and flies into the trigger while the trigger width springs.
  • Values in the trigger roll up or down depending on whether the new option is later or earlier in the list.
  • Reduced motion jumps the size, removes the fly and roll, and uses short fades.
  • The closed trigger sizes to its label up to min(20rem, 100vw - 2rem); the open list is max(trigger width, min(panelWidth, 100vw - 2rem)).
  • Set align="end" for triggers near the right edge so the surface grows leftward.
  • Hover highlight follows the mouse only; on touch a tap picks directly, and hover styles apply only on fine pointers.
  • A hidden measuring copy and a ResizeObserver size the trigger and list; the surface animates width, height, and radius through motion values.
  • All options render; there is no virtualization, so keep lists to a few hundred items.
  • Search filters in memory on label, meta, and keywords.

Notes for AI

Give your coding assistant the Markdown reference instead of screenshots.

  • Use for compact property pickers and form fields with up to a few hundred options, including avatars or icons.
  • Add keywords to options so search matches synonyms; meta shows trailing detail like counts or offsets.
  • Pass name to submit with a native form; the hidden input carries the value.
  • Use combobox when people mostly type free text, and multi-select for several values.

The full library index for assistants is at /llms.txt.