Skip to content
Suluu

SearchMorph

A compact search action that fluidly opens into a field. It shares the notification form's paper surface and spring, without a confirmation toast or results list — your application owns what happens with the query.

Preview

Click Search, then type

Installation

Registry — recommended

Copies the component into your configured shadcn UI directory and installs Motion.

npx shadcn@latest add https://suluu.site/r/search-morph.json

npm package

For centralized upgrades, install the optional ESM package and configure Tailwind source detection as shown below.

pnpm add suluu motion

Usage

Submission is callback-only. Connect onSubmit to your search handler. The field does not render results.

Registry
import { SearchMorph } from "@/components/ui/search-morph"

export function Find() {
  return (
    <SearchMorph
      onSubmit={(query) => runSearch(query)}
    />
  )
}
npm
// app/globals.css
@import "suluu/styles.css";
@source "../node_modules/suluu/dist";

// component.tsx
import { SearchMorph } from "suluu/search-morph"

export function Find() {
  return <SearchMorph onSubmit={(query) => runSearch(query)} />
}

Controlled state

Controlled
const [query, setQuery] = useState("")
const [open, setOpen] = useState(false)

<SearchMorph
  expanded={open}
  onExpandedChange={setOpen}
  onSubmit={(next) => runSearch(next)}
  onValueChange={setQuery}
  value={query}
/>

Pending state

Submitting swaps the action for a spinner. Left uncontrolled it settles after pendingDuration; pass pending to tie it to a real async search instead.

Pending
const [pending, setPending] = useState(false)

<SearchMorph
  onSubmit={async (query) => {
    setPending(true)
    await runSearch(query)
    setPending(false)
  }}
  pending={pending}
/>

In context

The search action can sit quietly in a content header, then make room for a query when needed.

Knowledge base

84 notes across 6 spaces

Launch notes

Updated this week

Research archive

Updated this week

Props

SearchMorph also accepts safe native form attributes and forwards its ref to the form element.

PropTypeDefaultDescription
labelstring"Search"CTA text in both states.
placeholderstring"Search"Search input placeholder.
classNamestringClass name applied to the form.
collapseOnBlurbooleantrueCollapse when focus leaves the widget.
disabledbooleanfalseDisable every interactive control.
valuestringControlled query value.
defaultValuestring""Initial uncontrolled query value.
onValueChange(value) => voidRuns when the input changes.
expandedbooleanControlled expansion state.
defaultExpandedbooleanfalseInitial uncontrolled expansion state.
onExpandedChange(expanded) => voidRuns for expansion requests.
motionIntensity"subtle" | "default" | "expressive""default"Animation character.
pendingbooleanControlled in-flight state. Swaps the action for a spinner.
pendingDurationnumber900Length of the built-in submit acknowledgement, in ms.
onSubmit(query, event) => voidRuns on submit. Empty queries are allowed.

Theming

Override these variables in your light and dark theme scopes. The registry installs the defaults automatically.

--suluu-search-backgroundContainer and input surface.
--suluu-search-foregroundPrimary text and icon.
--suluu-search-mutedPlaceholder text.
--suluu-search-hoverCollapsed CTA hover surface.
--suluu-search-accentExpanded submit button.
--suluu-search-accent-foregroundSubmit button content.
--suluu-search-ringKeyboard focus ring.
--suluu-search-shadowExpanded action button shadow.

Accessibility

SearchMorph is a native form with role="search". Expansion moves focus to the search field, Escape collapses and restores focus to the trigger, and reduced motion removes the morph. The clear button is labelled Clear search and hands focus back to the field; while a search is pending the action carries aria-busy alongside a polite live announcement.

↑↓ to navigate↵ to openesc to close