Expanding Search

A round search icon stretches into a full field when tapped and folds back when you leave it empty.

  • CSS
  • Forms & controls
  • Click
  • Added

AI prompt

Create an expanding search field in React and CSS. Collapsed, it is a 44px circle with a search icon button and an input of width 0. Clicking the icon sets an open state: the container width transitions from 44px to 240px over 360ms with cubic-bezier(0.16, 1, 0.3, 1), the input fades in, and focus moves to it once the transition starts. On blur with an empty value, or on Escape, collapse it again and return focus to the icon button. Keep the icon fixed on the left so it does not jump. Give the button aria-expanded and aria-controls. Under prefers-reduced-motion, open and close without the width transition. The placeholder reads "Search motions…".

Source

Demo.tsx
'use client'

import { useId, useRef, useState } from 'react'
import s from './demo.module.css'

export default function ExpandingSearch({ labels }: { labels: Record<string, string> }) {
  const [open, setOpen] = useState(false)
  const input = useRef<HTMLInputElement>(null)
  const button = useRef<HTMLButtonElement>(null)
  const id = useId()

  const close = (refocus: boolean) => {
    setOpen(false)
    if (refocus) button.current?.focus()
  }

  return (
    <div className={s.search} data-open={open || undefined}>
      <button
        ref={button}
        type="button"
        className={s.icon}
        aria-label={labels.open}
        aria-expanded={open}
        aria-controls={id}
        onClick={() => {
          setOpen(true)
          input.current?.focus()
        }}
      >
        <svg viewBox="0 0 24 24" aria-hidden="true">
          <circle cx="11" cy="11" r="6.5" />
          <path d="M16 16l4 4" />
        </svg>
      </button>
      <input
        ref={input}
        id={id}
        type="search"
        className={s.input}
        placeholder={labels.placeholder}
        tabIndex={open ? 0 : -1}
        onBlur={(e) => !e.currentTarget.value && close(false)}
        onKeyDown={(e) => e.key === 'Escape' && close(true)}
      />
    </div>
  )
}
demo.module.css
.search {
  display: flex;
  align-items: center;
  width: 44px;
  height: 44px;
  overflow: hidden;
  border: 1px solid var(--stage-rule);
  border-radius: 999px;
  background: var(--stage-surface);
  transition: width 360ms var(--ease-out), border-color var(--dur-short);
}

.search[data-open] { width: min(240px, 80vw); border-color: var(--stage-accent); }

.icon {
  display: grid;
  place-items: center;
  flex: none;
  width: 42px;
  height: 42px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--stage-ink);
  cursor: pointer;
}

.icon:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -4px; }

.icon svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}

.input {
  flex: 1;
  min-width: 0;
  height: 100%;
  padding: 0 14px 0 0;
  border: 0;
  outline: none;
  background: transparent;
  color: var(--stage-ink);
  font: 400 var(--text-sm) / 1 var(--font-body);
  opacity: 0;
  transition: opacity 200ms var(--ease-out);
}

.input::placeholder { color: var(--stage-muted); }
.search[data-open] .input { opacity: 1; transition-delay: 120ms; }

@media (prefers-reduced-motion: reduce) {
  .search, .input { transition: none; }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.