Morph select
A select whose trigger grows into the list, with a gliding highlight and type-ahead.
inputsnew
Preview
Morph select preview
Live · keyboard ready
Deploy target
When to use
- 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.
When not to use
- 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.
shadcn CLI
Also installs (1)
- manicat-foundation
Manual install(+)
registry.jsonjson
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "morph-select",
"type": "registry:ui",
"title": "Morph select",
"description": "A select whose trigger grows into the list, with a gliding highlight and type-ahead.",
"categories": [
"inputs"
],
"dependencies": [
"lucide-react",
"motion"
],
"registryDependencies": [
"https://manicat.viraza.net/r/manicat-foundation.json"
],
"files": [
{
"path": "registry/components/morph-select/morph-select.tsx",
"type": "registry:component",
"target": "@components/manicat/morph-select/morph-select.tsx"
},
{
"path": "registry/components/morph-select/morph-select.module.css",
"type": "registry:component",
"target": "@components/manicat/morph-select/morph-select.module.css"
}
],
"docs": "Docs and live preview: https://uiarc.dev/components/morph-select",
"meta": {
"tier": "free",
"kind": "component",
"docs": "https://manicat.viraza.net/components/morph-select",
"markdown": "https://uiarc.dev/components/morph-select/markdown",
"tags": [
"inputs",
"new"
]
}
}Usage
Install the component, then copy this into your app. Change the content and the props to fit your screen.
example.tsxtsx
"use client";
import { useState } from "react";
import { MorphSelect } from "@/components/manicat/morph-select/morph-select";
export function MorphSelectExample() {
const [value, setValue] = useState<string | null>("bill");
return (
<MorphSelect
label="Deploy target"
value={value}
onValueChange={(next) => setValue(next)}
items={[
{ label: "Staging", options: [{ value: "preview", label: "Preview", meta: "3 min" }, { value: "bill", label: "Bill", meta: "now" }] },
{ label: "Production", options: [{ value: "eu", label: "Europe", meta: "1 region" }, { value: "us", label: "United States", meta: "2 regions" }] },
]}
/>
);
}
Examples
Real screens rather than isolated props. Switch to Code to copy a whole example into your project.
Issue properties Compact pickers in a ticket header; each trigger grows into its list and the chosen label settles back in.
Issue properties
Compact pickers in a ticket header; each trigger grows into its list and the chosen label settles back in. preview
Live · keyboard ready
MAN-482
Checkout button stalls on Safari 17
Status
Assignee
issue-properties-compact-pickers-in-a-ticket-header-each-trigger-grows-into-its-list-and-the-chosen-label-settles-back-in-.tsxtsx
"use client";
import { useState } from "react";
import { MorphSelect } from "@/components/manicat/morph-select/morph-select";
const statuses = [
{ value: "backlog", label: "Backlog" },
{ value: "todo", label: "To do" },
{ value: "progress", label: "In progress" },
{ value: "review", label: "In review" },
{ value: "done", label: "Done" },
];
const people = [
{ value: "ada", label: "Ada Lovelace", meta: "Design" },
{ value: "linus", label: "Linus Park", meta: "Engineering" },
{ value: "grace", label: "Grace Mbeki", meta: "Support" },
];
export function MorphSelectIssueExample() {
const [status, setStatus] = useState<string | null>("progress");
const [assignee, setAssignee] = useState<string | null>("ada");
return (
<div className="grid w-full max-w-md gap-4 rounded-[var(--radius-panel)] border border-(color:--border) bg-(--surface) p-5">
<div>
<p className="text-xs text-(color:--text-muted)">MAN-482</p>
<h4 className="text-base font-medium text-(color:--foreground)">Checkout button stalls on Safari 17</h4>
</div>
<div className="flex flex-wrap gap-3">
<MorphSelect label="Status" value={status} onValueChange={setStatus} items={statuses} />
<MorphSelect label="Assignee" value={assignee} onValueChange={setAssignee} items={people} />
</div>
</div>
);
}
Timezone field Grouped options with an offset on the right and search keywords, so typing “est” finds New York.
Timezone field
Grouped options with an offset on the right and search keywords, so typing “est” finds New York. preview
Live · keyboard ready
Timezone
Digest emails are sent at 8:00 in Europe/Paris.
timezone-field-grouped-options-with-an-offset-on-the-right-and-search-keywords-so-typing-est-finds-new-york-.tsxtsx
"use client";
import { useState } from "react";
import { MorphSelect } from "@/components/manicat/morph-select/morph-select";
const timezones = [
{
label: "Europe",
options: [
{ value: "Europe/London", label: "London", meta: "UTC+0", keywords: "uk gmt britain" },
{ value: "Europe/Paris", label: "Paris", meta: "UTC+1", keywords: "france cet" },
{ value: "Europe/Berlin", label: "Berlin", meta: "UTC+1", keywords: "germany cet" },
],
},
{
label: "Americas",
options: [
{ value: "America/New_York", label: "New York", meta: "UTC-5", keywords: "est usa" },
{ value: "America/Chicago", label: "Chicago", meta: "UTC-6", keywords: "cst usa" },
{ value: "America/Los_Angeles", label: "Los Angeles", meta: "UTC-8", keywords: "pst usa california" },
],
},
];
export function MorphSelectTimezoneExample() {
const [timezone, setTimezone] = useState<string | null>("Europe/Paris");
return (
<div className="grid w-full max-w-xs gap-2">
<MorphSelect label="Timezone" value={timezone} onValueChange={setTimezone} items={timezones} searchable />
<p className="text-sm text-(color:--text-muted)">Digest emails are sent at 8:00 in {timezone ?? "your timezone"}.</p>
</div>
);
}
Keyboard interactions
| Keys | Action |
|---|---|
| Enter / Space / ArrowDown / ArrowUp | Opens the list on the selected option; Enter or Space picks the active option when open. |
| ArrowDown / ArrowUp | Moves the active option, skipping disabled ones. |
| PageDown / PageUp | Moves eight options at a time. |
| Home / End | 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. |
Accessibility
- 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.
Motion
- 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.
Responsive behavior
- 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.
Performance
- 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
- 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.