CSS Expandable Search Bar
A search bar that expands on focus with a smooth CSS transition.
An expandable search bar is a text field that occupies as little space as it can until someone means to use it, then widens. Two selectors do all of it. :focus matches the input itself and drives the width, and :focus-within matches the wrapper around it, which is how the border and background can change at the same moment without the input having to style its own parent.
There are three versions here. The base one grows from 120px to 240px and keeps the magnifier visible the whole time. The second starts as a bare circle with no field at all and unrolls when the icon is clicked. The third keeps the field a fixed size and instead drops a suggestion panel below it, which stays open while the reader tabs down into the results.
Click the field to expand it
HTML
<div class="search-wrap">
<span class="search-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
</span>
<input class="search-input" type="search" placeholder="Search...">
</div>
CSS
.search-wrap {
display: inline-flex;
align-items: center;
background: #1c1c1e;
border: 2px solid #2a2a2d;
border-radius: 999px;
padding: .5rem .75rem;
transition: border-color .25s, background .25s;
}
/* :focus-within reacts when the input inside is focused */
.search-wrap:focus-within {
background: #141415;
border-color: #b8ff57;
}
.search-icon {
display: flex;
color: #88888f;
flex-shrink: 0;
}
.search-input {
width: 120px;
padding: 0 .5rem;
border: none;
background: transparent;
outline: none;
color: #f0f0f0;
font-family: inherit;
font-size: .95rem;
transition: width .35s;
}
.search-input:focus {
width: 240px;
}
Other ways to build it
Collapsed to an icon
The field starts at width: 0 with no padding, so the control is a 40px circle holding a magnifier. The magnifier is a <label> bound to the input, not a decorative span, which is what makes one click both expand the field and put the cursor in it. Padding is transitioned alongside the width, because a padding that appears instantly makes the box jump before it glides.
HTML
<div class="mini-search">
<!-- a label, so the click lands focus in the field -->
<label class="mini-icon" for="q" aria-label="Search">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
</label>
<input class="mini-input" type="search" id="q" placeholder="Search the docs">
</div>
CSS
.mini-search {
display: inline-flex;
align-items: center;
background: #1c1c1e;
border: 2px solid #2a2a2d;
border-radius: 999px;
transition: border-color .25s;
}
.mini-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
flex-shrink: 0;
color: #88888f;
cursor: pointer;
}
/* appearance:none removes Safari's built-in search
decoration, which otherwise reserves width at zero */
.mini-input {
width: 0;
padding: 0;
border: none;
outline: none;
background: transparent;
color: #f0f0f0;
font: inherit;
font-size: .95rem;
appearance: none;
-webkit-appearance: none;
transition: width .35s ease, padding .35s ease;
}
.mini-search:focus-within {
border-color: #b8ff57;
}
.mini-search:focus-within .mini-icon {
color: #b8ff57;
}
.mini-search:focus-within .mini-input {
width: 190px;
padding-right: .9rem;
}
Suggestions that stay open while you tab through them
The panel is revealed by :focus-within on the wrapper rather than by hover on the field. That distinction is the whole point: the links inside the panel are focusable, they are descendants of the same wrapper, so focus moving onto one keeps the panel open. A hover trigger would close it the moment the pointer left the input. Click the field, then press Tab to walk down the list.
HTML
<div class="sg-wrap">
<div class="search-wrap">
<span class="search-icon">...</span>
<input class="search-input" type="search" placeholder="Search...">
</div>
<div class="sg-panel">
<span class="sg-title">Recent</span>
<a href="#">position sticky</a>
<a href="#">focus-within</a>
</div>
</div>
CSS
.sg-wrap {
position: relative;
}
/* hidden but still laid out, so opacity can transition */
.sg-panel {
position: absolute;
top: calc(100% + 6px);
left: 0;
right: 0;
display: flex;
flex-direction: column;
background: #141415;
border: 1px solid #2a2a2d;
border-radius: 8px;
opacity: 0;
visibility: hidden;
transform: translateY(-6px);
transition: opacity .2s, transform .2s, visibility .2s;
}
/* the links are inside the wrapper, so focusing one
keeps the panel open; :hover could not do this */
.sg-wrap:focus-within .sg-panel {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.sg-panel a {
padding: .5rem .9rem;
color: #f0f0f0;
font-size: .85rem;
text-decoration: none;
}
.sg-panel a:hover,
.sg-panel a:focus-visible {
background: rgba(184, 255, 87, 0.1);
color: #b8ff57;
}
How it works
The input starts at a narrow fixed width and transitions to a wider one on :focus. The wrapping container uses :focus-within to react when any child is focused, which is what lets the border-color and background change together without JavaScript.
:focus-within is the piece worth understanding, because it is what makes the whole pattern possible in CSS alone. It matches an element when that element or any of its descendants holds focus. Before it existed there was no way for a wrapper to react to its own input being focused, so this effect needed a focus and blur listener toggling a class. The selector replaced both listeners with one pseudo-class, and it stays matched while focus moves between children, which is exactly what a search box with suggestions needs.
Widths are animatable, which is why transition: width .35s works, but they are also expensive. Changing a width forces layout on the element and reflows whatever sits next to it in the flow. On a small control in a header nobody will ever measure the difference. Inside a long list, or on a control with heavy siblings, animating transform: scaleX() instead keeps the work on the compositor, at the cost of the text inside being stretched along with the box.
The collapse to an icon version has one detail that is easy to miss. The icon is a <label> pointing at the input's id, not a decorative <span>. Clicking a label moves focus to the control it labels, so the click that expands the field also puts the cursor in it. Without that, the reader clicks the magnifier, watches a field appear, and then has to click again to type in it.
A width of zero does not hide an input completely. It still has padding, borders, and in Safari a native appearance for type="search" including a clear button that reserves space. padding: 0 and border: none have to come off with the width, and appearance: none removes the platform decoration so the collapsed state is genuinely just the icon. Transition the padding alongside the width, or the box jumps at the start of the animation and then glides.
For the suggestion panel, the reveal is the same show and hide trick used on the hover dropdown: visibility: hidden with opacity: 0, both transitioned, so the panel can fade rather than being cut out of the layout. What differs is the trigger. Hover cannot survive the pointer leaving the trigger to reach the menu, while :focus-within holds as long as focus is anywhere inside the wrapper, including on a link in the panel itself.
CSS properties used
:focus-within- Matches an element while it or any descendant has focus. The wrapper uses it to change border and background when the input inside is focused.
:focus- Matches the focused element itself. Drives the width change on the input.
width- The animated property. Transitionable, but it triggers layout, so keep the element small or use a transform instead.
transition- Runs the width, padding, border and background changes over time. Listing several properties keeps their durations in step.
appearancenonestrips the platform styling that Safari applies toinput[type="search"], including the built-in clear button that otherwise reserves width.visibility- Paired with
opacityon the suggestion panel so the fade can be transitioned, whichdisplay: nonenever allows.
Browser support
| Feature | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
:focus-within | 60 | 52 | 10.1 | 79 |
:focus-visible | 86 | 4 | 15.4 | 86 |
transition | 4 | 5 | 5.1 | 12 |
appearance | 83 | 80 | 15.4 | 83 |
Figures come from Can I Use for the :focus-within and :focus-visible pseudo-classes, CSS transitions, and CSS appearance. :focus-within is the only selector here with a real floor, and a browser that does not know it simply drops the rule, leaving a search box that still works and still focuses, just without the border change. For older WebKit the platform search decoration also responds to -webkit-appearance: none and to input[type="search"]::-webkit-search-decoration, which is worth adding if the collapsed state has to be exactly icon width.
Accessibility notes
The field needs a name whether or not a visible label fits the design. A <label> with text is best; where the magnifier is the only affordance, put the label text in the label element and hide it visually with a clipping utility rather than deleting it. aria-label on the input is the fallback when there is genuinely no element to attach text to. A placeholder is not a label: it disappears the moment typing starts, and some screen readers do not announce it at all.
A control that is only reachable by clicking an icon has to survive a keyboard as well. Because the icon is a label bound to the input, Tab reaches the input directly and :focus-within fires from the keyboard exactly as it does from a click, so the field unrolls on Tab with no extra work. Check this by tabbing rather than assuming it: a decorative span in place of the label breaks it silently.
Do not remove the focus outline from the input without replacing it. The wrapper's accent border reads as a focus indicator to a sighted reader, but it is easy to lose against a busy header, so keep outline on the wrapper under :focus-visible where the design allows. The suggestion panel needs the same care: its links are focusable, and the panel stays open while they hold focus, so their focus ring has to be visible against the panel background rather than the page background.
If the suggestions are results rather than static links, the list changes while the reader types, and a purely visual reveal will not announce that. That is the point where a combobox pattern with aria-expanded and a live region belongs, and it needs script. CSS gets you the reveal, not the announcement.
What you can build with it
- Site headers. A magnifier that unrolls into a field keeps navigation links from being squeezed on tablet widths, which is where a full width search box usually causes the wrap.
- Data table toolbars. A filter field that grows when in use and shrinks back so the column headers keep their room.
- Documentation sidebars. Search sits above the tree of pages, and the suggestion panel overlays the links below it rather than pushing them down.
- Mobile navigation. Inside an off canvas panel, a search field that expands to the full panel width when focused, and gives the space back to the menu items when it is not.
- Admin dashboards. A global search that expands over the page chrome, with recent queries in a
:focus-withinpanel underneath.
Mistakes worth avoiding
- Using
:focuson the wrapper instead of:focus-within. A<div>is not focusable, so the rule never matches and the border never changes. This is the single most common reason the styling does not react. - Collapsing the input to
width: 0while leaving its horizontal padding and border in place. The collapsed control is still 20 or 30 pixels wide, which reads as a rendering fault next to the icon. - Making the magnifier a
<span>rather than a<label>bound to the input. The field expands on click but focus never moves into it, so the first thing the reader types goes nowhere. - Reaching for a suggestion panel opened on
:hover. The pointer has to cross the gap between the field and the panel, and unless the two touch exactly, the panel closes on the way. - Transitioning the width of a control that sits in a flex row with flexible siblings. Every frame reflows the neighbours, so the whole header appears to twitch while the field grows.
Frequently asked questions
How do I expand a search bar on focus with CSS only?
transition: width, then set a wider width under input:focus. Wrap it in a container and use :focus-within on that container for anything that has to change outside the input itself, such as the border color or the background.What is the difference between :focus and :focus-within?
:focus matches only the element that actually holds focus. :focus-within matches that element and every ancestor of it, so a wrapper can react to its own input being focused. It also stays matched while focus moves between children, which is what keeps a suggestion panel open.Why does my collapsed search input still show a rounded box in Safari?
input[type="search"], which includes a border treatment and a clear button that reserves width. appearance: none plus -webkit-appearance: none removes it, and older WebKit also needs ::-webkit-search-decoration cleared.Should I animate width or transform?
transform: scaleX() instead and accept that the contents stretch.Does an expandable search bar hurt accessibility?
<label>, give the field a real accessible name rather than relying on the placeholder, and keep a visible focus indicator. Done that way it is reachable by Tab and announces itself the same as any other search field.