Skip to content

Expandable Search Bar

A search icon that smoothly expands into a full text input on click.

Freeminimalglass
Category
Components
Added
2026-08-22
Deps
2
Updated
2026-09-01
Preview

Props

240
Requires framer-motion, lucide-react
"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>
  );
}
Notes

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.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

Featured
New

Subsequence search that scores by run length and word starts, and highlights the hits.

searchfuzzyinput
Featured
New

A status pill that resizes itself fluidly around whatever content it holds.

islandnotchlayout

A custom-styled slider with a drag handle and a value tooltip while dragging.

sliderrangedrag