Expandable Search Bar
A search icon that smoothly expands into a full text input on click.
- Category
- Components
- Added
- 2026-08-22
- Deps
- 2
- Updated
- 2026-09-01
Props
"use client";
import { useRef, useState } from "react";
import { motion } from "framer-motion";
import { Search, X } from "lucide-react";
export function ExpandableSearchBar({
placeholder = "Search...",
width = 240,
}: {
placeholder?: string;
width?: number;
}) {
const [open, setOpen] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
function handleOpen() {
setOpen(true);
requestAnimationFrame(() => inputRef.current?.focus());
}
return (
<motion.div
layout
transition={{ type: "spring", stiffness: 320, damping: 28 }}
className="flex h-11 items-center gap-2 rounded-full border border-neutral-200 px-3"
style={{ width: open ? width : 44 }}
>
{open ? (
<>
<Search size={16} className="shrink-0 text-neutral-400" />
<input
ref={inputRef}
type="text"
placeholder={placeholder}
className="w-full bg-transparent text-sm outline-none placeholder:text-neutral-400"
/>
<button aria-label="Close search" onClick={() => setOpen(false)} className="shrink-0 text-neutral-400">
<X size={16} />
</button>
</>
) : (
<button aria-label="Open search" onClick={handleOpen} className="flex h-full w-full items-center justify-center text-neutral-400">
<Search size={16} />
</button>
)}
</motion.div>
);
}
About this component
The expand animation comes from Framer Motion's layout prop on the wrapper interpolating between two real inline-style widths, 44px collapsed and the width prop when open, not manually authored width keyframes; layout handles measuring the before/after box and animating the delta on its own. The detail that will trip you up if you modify this: focusing the input on open is deferred with requestAnimationFrame rather than called directly in the click handler, because the input does not exist in the DOM yet at the moment open flips true, so a direct .focus() call would be a no-op against an unmounted element. Reach for this over a search bar that is always full-width when the collapsed icon-only state genuinely saves header space, a mobile nav bar or a dense toolbar; where search is a primary action, a permanently visible input serves users faster. The tradeoff for a real mounted/unmounted input over a decorative width animation on a hidden field: more correct, a visually collapsed input cannot be tabbed to, but the value resets every time it collapses and reopens.
Curator’s note
The expansion is a real input mounting into a real DOM element with Framer's `layout` prop animating the container's box between two real widths — not a decorative overlay animation layered on top of a permanently-full-width hidden input, which would look right but leave the input focusable/tabbable even while visually collapsed.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
Fuzzy Match Input
componentsSubsequence search that scores by run length and word starts, and highlights the hits.
Morphing Island Notch
componentsA status pill that resizes itself fluidly around whatever content it holds.
Draggable Range Slider
componentsA custom-styled slider with a drag handle and a value tooltip while dragging.