Blog
How to Create an Expandable Search Bar with HTML, CSS and JavaScript
An expandable search bar keeps a page uncluttered by showing a search field only when a visitor needs it. In this tutorial, you’ll build a responsive, keyboard-friendly search control with HTML, CSS transitions, and a small JavaScript toggle.
Quick answer: Place a search button and input inside a form, animate the form width with CSS, and use JavaScript to toggle an open class and update accessible labels. The live example below also includes a clear button, Escape-to-close behavior, and a status message.
View Live DemoDownload Source ZIP
What you’ll build
The search starts as a round magnifying-glass button. Selecting it smoothly reveals a search field and clear control. The example is deliberately self-contained: the form demonstrates the interaction locally and does not send a query to a server.
Try the expandable search bar
Activate the magnifying glass, enter a query, and try clearing it or pressing Escape. On a real website, connect the form to your site’s search endpoint.
How the search interaction works
The form begins at the width of its icon button. When JavaScript adds the is-open class, CSS transitions the form to its full width and fades in the input. The toggle also updates aria-expanded and its accessible label so assistive technology can announce the current state.
Build it step by step
- Create a
formwithrole="search", a toggle button, a labeled search input, and a reset button. - Use a flexible layout and a fixed icon-button width. Animate the form’s width when it has the
is-openclass. - In JavaScript, toggle that class, move keyboard focus to the input, and return focus to the search button when closing.
- Handle the input, reset, and Escape key. Replace the local status message with a real site-search request when integrating it.
HTML structure
The input has a visible placeholder and a separate screen-reader label. The search button exposes its state through aria-expanded, while the feedback area uses role="status".
CSS animation and responsive behavior
The stylesheet uses width and opacity transitions and keeps the form within the available viewport width. Focus-visible outlines make keyboard interaction clear. A reduced-motion media query minimizes transitions for visitors who request less motion.
JavaScript controls
The script manages open and closed states, updates accessible labels, and handles clear, submit, and Escape actions. The sample submit handler prevents navigation and displays the entered query; wire it to your application’s search URL or API for production use.
Common issues
- The input cannot be focused: Check that the open state sets its
tabindexto0and does not leave an overlay above it. - The field is clipped: Set a maximum width based on the container and verify the mobile breakpoint.
- The icon button does nothing: Load the JavaScript after the form markup or defer the script until the document is ready.
Frequently asked questions
Does the search example query a website?
No. It demonstrates the interface locally. Point the form to your site’s search endpoint to return real results.
Can I build this without JavaScript?
A CSS-only version is possible with focus or checkbox techniques, but JavaScript gives you explicit open/close controls, Escape handling, and synchronized accessibility state.
Is the search bar mobile-friendly?
Yes. Its width is capped by the available container width, so it stays within narrow screens. Keep the surrounding layout flexible when embedding it.
Download the complete source
The ZIP contains a standalone HTML page, a separate stylesheet, and the JavaScript for the demo controls. Extract the files together and open index.html in a browser.
Select the magnifying glass to expand the search field.
Try opening the search field.
Select the magnifying glass to start a search.
The field will expand and receive focus.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<title>Expandable Search Bar Demo</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<main class="demo-card">
<p class="eyebrow">HTML · CSS · JavaScript</p>
<h1>Expandable Search Bar</h1>
<p class="intro">Select the search button to expand the field.</p>
<form class="search" role="search" autocomplete="off">
<button class="search__toggle" type="button" aria-label="Open search" aria-expanded="false" aria-controls="search-input">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="10.8" cy="10.8" r="6.8"></circle><path d="m16 16 5 5"></path></svg>
</button>
<label class="visually-hidden" for="search-input">Search this demo</label>
<input id="search-input" class="search__input" type="search" placeholder="Search this demo…" tabindex="-1">
<button class="search__clear" type="reset" aria-label="Clear search" tabindex="-1">×</button>
</form>
<p class="result" role="status" aria-live="polite">Try opening the search field.</p>
</main>
<script src="script.js"></script>
</body>
</html>
:root {
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #17213a;
background: #f3f6ff;
font-synthesis: none;
}
* { box-sizing: border-box; }
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
padding: 24px;
background: radial-gradient(circle at 15% 10%, #e0eaff, transparent 38%), #f5f7fc;
}
.demo-card {
width: min(100%, 620px);
padding: clamp(24px, 6vw, 48px);
border: 1px solid #dbe3f2;
border-radius: 24px;
background: #fff;
box-shadow: 0 24px 65px rgb(25 50 100 / 12%);
}
.eyebrow { margin: 0 0 10px; color: #4169cf; font-size: .78rem; font-weight: 750; letter-spacing: .12em; text-transform: uppercase; }
h1 { margin: 0; font-size: clamp(1.7rem, 5vw, 2.5rem); letter-spacing: -.04em; }
.intro { margin: 10px 0 28px; color: #5c6880; line-height: 1.6; }
.search {
--search-width: min(100%, 470px);
position: relative;
display: flex;
align-items: center;
width: 60px;
height: 60px;
overflow: hidden;
border: 1px solid #ced8ee;
border-radius: 999px;
background: #fff;
box-shadow: 0 8px 24px rgb(34 78 170 / 10%);
transition: width .35s cubic-bezier(.2,.75,.25,1), border-color .25s, box-shadow .25s;
}
.search.is-open, .search:focus-within {
width: var(--search-width);
border-color: #4d74e8;
box-shadow: 0 0 0 4px rgb(77 116 232 / 14%), 0 10px 28px rgb(34 78 170 / 12%);
}
.search__toggle {
z-index: 1;
flex: 0 0 58px;
width: 58px;
height: 58px;
display: grid;
place-items: center;
border: 0;
border-radius: 50%;
color: #315ac3;
background: transparent;
cursor: pointer;
}
.search__toggle:hover { background: #eef3ff; }
.search__toggle:focus-visible, .search__clear:focus-visible { outline: 3px solid #6c8bff; outline-offset: -4px; }
.search__toggle svg { width: 23px; height: 23px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
.search__input {
min-width: 0;
flex: 1;
height: 100%;
padding: 0 8px;
border: 0;
outline: 0;
color: #17213a;
background: transparent;
font: inherit;
opacity: 0;
transition: opacity .2s .08s;
}
.search.is-open .search__input, .search:focus-within .search__input { opacity: 1; }
.search__input::placeholder { color: #8490a7; }
.search__clear {
flex: 0 0 42px;
height: 42px;
margin-right: 8px;
border: 0;
border-radius: 50%;
color: #5e6c86;
background: #f0f3f9;
font-size: 1.4rem;
line-height: 1;
cursor: pointer;
opacity: 0;
}
.search.is-open .search__clear, .search:focus-within .search__clear { opacity: 1; }
.result { min-height: 1.5em; margin: 18px 0 0; color: #66738a; font-size: .92rem; }
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; } }
const form = document.querySelector('.search');
const toggleButton = document.querySelector('.search__toggle');
const searchInput = document.querySelector('#search-input');
const statusMessage = document.querySelector('.result');
function setSearchOpen(isOpen) {
form.classList.toggle('is-open', isOpen);
toggleButton.setAttribute('aria-expanded', String(isOpen));
toggleButton.setAttribute('aria-label', isOpen ? 'Close search' : 'Open search');
searchInput.tabIndex = isOpen ? 0 : -1;
form.querySelector('.search__clear').tabIndex = isOpen ? 0 : -1;
if (isOpen) searchInput.focus();
else toggleButton.focus();
}
toggleButton.addEventListener('click', () => {
setSearchOpen(!form.classList.contains('is-open'));
});
searchInput.addEventListener('input', () => {
statusMessage.textContent = searchInput.value.trim()
? 'Demo query: ' + searchInput.value.trim()
: 'Type a word to see the demo query.';
});
form.addEventListener('submit', (event) => {
event.preventDefault();
statusMessage.textContent = searchInput.value.trim()
? 'Search submitted: ' + searchInput.value.trim()
: 'Enter a search term first.';
});
form.addEventListener('reset', () => {
requestAnimationFrame(() => {
statusMessage.textContent = 'Search cleared.';
searchInput.focus();
});
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && form.classList.contains('is-open')) {
searchInput.value = '';
statusMessage.textContent = 'Search closed.';
setSearchOpen(false);
}
});