Search Expand
An icon button that expands into a search input with a smooth animation.
Overview
The SearchExpand component renders as a compact icon button that smoothly expands into a search input when clicked. The icon from the button becomes the leading icon in the input. Ideal for toolbars and headers where space is constrained.
The expanded state uses the same composition pattern as InputWrapper — pass an Input and optional trailing elements (clear buttons, submit buttons, etc.) as children.
Usage
import { Input, SearchExpand } from "@tilt-legal/cubitt-components/primitives";<SearchExpand>
<Input placeholder="Search..." size="md" variant="default" />
</SearchExpand>For search surfaces, prefer useSearch as the state layer and let SearchExpand stay presentation-only:
import { Button, Input, SearchExpand } from "@tilt-legal/cubitt-components/primitives";
import { useSearch } from "@tilt-legal/cubitt-components/utilities/hooks";
import { Xmark } from "@tilt-legal/cubitt-icons/ui/outline";
function UrlSyncedSearch() {
const search = useSearch({
debounceMs: 100,
paramName: "q",
paramDebounce: 300,
});
return (
<SearchExpand expandedWidth="20rem">
<Input placeholder="Search" size="md" variant="default" {...search.inputProps} />
<Button
className="-me-0.5 size-5"
disabled={search.isEmpty}
mode="icon"
onClick={search.clear}
size="sm"
variant="link"
>
{!search.isEmpty && <Xmark />}
</Button>
</SearchExpand>
);
}Examples
Sizes
All three sizes match Button and Input height tokens exactly (sm=28px, md=34px, lg=40px). Pass the matching size to both SearchExpand and Input.
import { Input, SearchExpand } from "@tilt-legal/cubitt-components/primitives";
<div className="flex items-center gap-4">
<SearchExpand size="sm">
<Input placeholder="Small" size="sm" variant="default" />
</SearchExpand>
<SearchExpand size="md">
<Input placeholder="Medium" size="md" variant="default" />
</SearchExpand>
<SearchExpand size="lg">
<Input placeholder="Large" size="lg" variant="default" />
</SearchExpand>
</div>Button Variants
The collapsed button supports ghost, outline, and secondary variants.
import { Input, SearchExpand } from "@tilt-legal/cubitt-components/primitives";
<div className="flex items-center gap-4">
<SearchExpand variant="ghost">
<Input placeholder="Ghost" size="md" variant="default" />
</SearchExpand>
<SearchExpand variant="outline">
<Input placeholder="Outline" size="md" variant="default" />
</SearchExpand>
<SearchExpand variant="secondary">
<Input placeholder="Secondary" size="md" variant="default" />
</SearchExpand>
</div>Custom Icon
Pass any Cubitt icon via the icon prop. It appears in both the collapsed button and expanded input.
import { Input, SearchExpand } from "@tilt-legal/cubitt-components/primitives";
import { SearchExpand } from "@tilt-legal/cubitt-components/primitives";
import {
Filter } from "@tilt-legal/cubitt-icons/ui/outline";
<SearchExpand icon={Filter}>
<Input placeholder="Filter items..." size="md" variant="default" />
</SearchExpand>Custom Width
Control how wide the input expands with expandedWidth.
import { Input } from "@tilt-legal/cubitt-components/primitives";
<SearchExpand expandedWidth="12rem">
<Input placeholder="Narrow (12rem)" size="md" variant="default" />
</SearchExpand>
<SearchExpand expandedWidth="24rem">
<Input placeholder="Wide (24rem)" size="md" variant="default" />
</SearchExpand>Clearable
Add a clear button as a trailing child — same pattern as InputWrapper.
import { Button, Input, SearchExpand } from "@tilt-legal/cubitt-components/primitives";
import { useSearch } from "@tilt-legal/cubitt-components/utilities/hooks";
import { Xmark } from "@tilt-legal/cubitt-icons/ui/outline";
function ClearableSearch() {
const search = useSearch({ debounceMs: 0 });
return (
<SearchExpand expandedWidth="20rem">
<Input
placeholder="Type to search..."
size="md"
variant="default"
{...search.inputProps}
/>
<Button
className="-me-0.5 size-5"
disabled={search.isEmpty}
mode="icon"
onClick={search.clear}
size="sm"
variant="link"
>
{!search.isEmpty && <Xmark />}
</Button>
</SearchExpand>
);
}Controlled
Use expanded and onExpandedChange for full control over the expand/collapse state.
import { Input } from "@tilt-legal/cubitt-components/primitives";
import { SearchExpand } from "@tilt-legal/cubitt-components/primitives";
import { useSearch } from "@tilt-legal/cubitt-components/utilities/hooks";
import { useState } from "react";
function ControlledSearch() {
const [expanded, setExpanded] = useState(false);
const search = useSearch({ debounceMs: 0 });
return (
<SearchExpand
expanded={expanded}
onExpandedChange={setExpanded}
expandedWidth="20rem"
>
<Input
placeholder="Controlled search..."
size="md"
variant="default"
{...search.inputProps}
/>
</SearchExpand>
);
}URL State
SearchExpand with URL state synchronization owned by useSearch.
import { Input, SearchExpand } from "@tilt-legal/cubitt-components/primitives";
import { useSearch } from "@tilt-legal/cubitt-components/utilities/hooks";
function UrlStateSearchExpand() {
const search = useSearch({
debounceMs: 0,
paramName: "q",
});
return (
<SearchExpand expandedWidth="20rem">
<Input
placeholder="Type to update URL"
size="md"
variant="default"
{...search.inputProps}
/>
</SearchExpand>
);
}Keyboard & Focus
- Click or Enter/Space on the collapsed button expands the input and auto-focuses it.
- Escape collapses the input and returns focus to the button.
- Blur when the input is empty collapses it automatically (configurable via
collapseOnBlurWhenEmpty). - Blur when the input has a value keeps it expanded.
API Reference
SearchExpand
The outer container that handles the expand/collapse animation and button state. Pass Input and optional trailing elements as children.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Content shown when expanded (Input + optional trailing elements) |
icon | CubittIcon | Magnifier | Icon shown in both collapsed and expanded states |
size | "sm" | "md" | "lg" | "md" | Matches Button/Input height tokens |
expandedWidth | string | number | "16rem" | Width when expanded |
variant | "ghost" | "outline" | "secondary" | "secondary" | Button variant for the collapsed state |
expanded | boolean | — | Controlled expanded state |
onExpandedChange | (expanded: boolean) => void | — | Callback when expanded state changes |
collapseOnBlurWhenEmpty | boolean | true | Collapse on blur when input is empty |
collapseOnEscape | boolean | true | Collapse when Escape is pressed |
label | string | "Search" | Accessible label for the button and input |
className | string | — | Additional className on the outer container |
reduceMotion | boolean | false | Disable animations (respects prefers-reduced-motion by default) |
disabled | boolean | false | Disable the component |
Children
SearchExpand uses the same composition pattern as InputWrapper. Pass an Input as the primary child and optional trailing elements:
<SearchExpand>
<Input placeholder="Search..." size="md" variant="default" />
<Button mode="icon" size="sm" variant="link" onClick={onClear}>
<Xmark />
</Button>
</SearchExpand>Input props such as placeholder, size, and variant are set directly on the Input child. For search state, prefer useSearch and pass search.inputProps into the composed input. If useSearch owns URL sync, do not also pass paramName to the Input child.