Implementación de una barra de búsqueda adaptable para escritorio y móvil.
Vamos a ver cómo implementar una barra de búsqueda responsive con HTML, CSS y JavaScript, optimizada para diferentes tamaños de pantalla. El diseño se adapta de forma automática, ofreciendo una experiencia visual y de uso coherente tanto en escritorio como en móviles.
Comenzamos con una estructura HTML básica. Tenemos un contenedor que agrupa el botón de activación y los formularios de búsqueda para cada contexto, uno para el escritorio y otro para el móvil:
<div class="search-container" id="search-container"> <button class="search-toggle-button" id="search-toggle" aria-label="Open search bar"> <svg viewBox="0 0 24 24" width="24" height="24" role="presentation"> <path d="M10.25 2a8.25 8.25 0 0 1 6.34 13.53l5.69 5.69a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-5.69-5.69A8.25 8.25 0 1 1 10.25 2ZM3.5 10.25a6.75 6.75 0 1 0 13.5 0 6.75 6.75 0 0 0-13.5 0Z"> </path> </svg> </button>
<form action="/search" method="get" class="search-form search-form--desktop" id="search-form-desktop" role="search"> <div class="search-input-container"> <input type="text" name="q" class="search-input" placeholder="Search the blog…" aria-label="Search the blog"> <button type="submit" class="search-submit-button" aria-label="Submit search"> <svg viewBox="0 0 24 24" width="20" height="20" role="presentation"> <path d="M10.25 2a8.25 8.25 0 0 1 6.34 13.53l5.69 5.69a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-5.69-5.69A8.25 8.25 0 1 1 10.25 2ZM3.5 10.25a6.75 6.75 0 1 0 13.5 0 6.75 6.75 0 0 0-13.5 0Z"> </path> </svg> </button> </div> </form>
<form action="/search" method="get" class="search-form search-form--mobile" id="search-form-mobile" role="search"> <div class="search-input-container"> <span class="search-icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="20" height="20" role="presentation"> <path d="M10.25 2a8.25 8.25 0 0 1 6.34 13.53l5.69 5.69a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-5.69-5.69A8.25 8.25 0 1 1 10.25 2ZM3.5 10.25a6.75 6.75 0 1 0 13.5 0 6.75 6.75 0 0 0-13.5 0Z"> </path> </svg> </span> <input type="text" name="q" class="search-input" placeholder="Search the blog…" aria-label="Search the blog"> </div> <button type="submit" class="search-action-button"> Search </button> </form> </div>
Cada formulario está diseñado con clases específicas para mostrarse o ocultarse según el tamaño de pantalla.
Usamos media queries para mostrar u ocultar los formularios dependiendo del ancho de la ventana. El formulario de escritorio (.search-form--desktop
) se muestra solo en pantallas mayores a 768px, mientras que el formulario móvil (.search-form--mobile
) se activa en pantallas más pequeñas.
Además, se definen variables CSS que centralizan los colores, radios, padding y transiciones, lo que facilita mantener una consistencia visual sin repetir valores.
Ejemplo del comportamiento móvil:
@media (max-width: 768px) { .search-form--desktop { display: none !important; }
.search-container.search-active .search-form--mobile { display: flex; animation-name: slideDown; }
@keyframes slideDown { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }}
El input de búsqueda tiene un estilo limpio y accesible, con contorno al enfocarse, lo cual mejora la interacción con teclado.
El comportamiento interactivo está gestionado con JavaScript. Al pulsar el botón de búsqueda, se alterna el estado visible del formulario correspondiente. Además, se cierra automáticamente si se hace clic fuera del contenedor o si cambia el tamaño de la ventana, para evitar estados visuales inconsistentes.
searchToggle.addEventListener('click', (event) => { event.stopPropagation(); searchContainer.classList.toggle('search-active');
if (searchContainer.classList.contains('search-active')) { setTimeout(() => { const activeForm = window.innerWidth >= 768 ? desktopInput : mobileInput; activeForm.focus(); }, 50); }});
También se gestionan clics en el documento para cerrar la búsqueda si no se interactúa directamente con el componente, algo especialmente útil en interfaces móviles.
<!DOCTYPE html><html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Responsive Search Bar</title> <style> :root { --search-bg-desktop: #f8f9fa; --search-bg-mobile: #343a40; --search-border-radius: 6px; --search-padding: 0.75rem 1rem; --search-icon-color: #6c757d; --search-focus-outline: #79c0ff; --search-mobile-text-color: #f8f9fa; --search-mobile-button-border: #6c757d; --transition-speed: 0.3s; --search-svg-hover-color: #212529; }
body { margin: 8px; font-family: sans-serif; background-color: #e9ecef; display: flex; justify-content: center; align-items: flex-start; flex-direction: column; }
.search-container { position: relative; width: 100%; max-width: 600px; display: flex; justify-content: flex-end; }
.search-toggle-button { background: none; border: none; padding: 0.5rem; cursor: pointer; color: var(--search-icon-color); display: inline-flex; align-items: center; justify-content: center; border-radius: var(--search-border-radius); transition: opacity var(--transition-speed) ease, visibility var(--transition-speed) ease; position: absolute; top: 0; right: 0; z-index: 2; height: calc(24px + 2 * 0.75rem + 2px + 0.5rem); box-sizing: border-box; }
.search-toggle-button svg { display: block; fill: currentColor; transition: fill var(--transition-speed) ease; }
.search-toggle-button:hover svg { fill: var(--search-svg-hover-color); }
.search-container.search-active .search-toggle-button { opacity: 0; visibility: hidden; pointer-events: none; }
.search-form { display: none; position: absolute; top: 0; left: 0; width: 100%; z-index: 1; opacity: 0; visibility: hidden; transition: opacity var(--transition-speed) ease, visibility var(--transition-speed) ease; animation-duration: var(--transition-speed); animation-timing-function: ease-out; animation-fill-mode: forwards; }
.search-container.search-active .search-form { opacity: 1; visibility: visible; z-index: 3; }
.search-input-container { position: relative; display: flex; align-items: center; flex-grow: 1; }
.search-input { flex-grow: 1; padding: var(--search-padding); border: 1px solid #ced4da; border-radius: var(--search-border-radius); font-size: 1rem; transition: border-color var(--transition-speed) ease; appearance: none; -webkit-appearance: none; width: 100%; box-sizing: border-box; min-height: calc(24px + 2 * 0.75rem + 2px); }
.search-input::placeholder { color: #6c757d; opacity: 1; }
.search-input:focus { outline: 2px solid var(--search-focus-outline); outline-offset: 2px; border-color: var(--search-focus-outline); }
.search-icon { display: flex; align-items: center; justify-content: center; color: var(--search-icon-color); position: absolute; top: 0; bottom: 0; margin: auto 0; pointer-events: none; height: 20px; }
.search-icon svg { display: block; fill: currentColor; }
@media (min-width: 768px) { .search-form--mobile { display: none !important; }
.search-container.search-active .search-form--desktop { display: block; }
.search-form--desktop { background-color: var(--search-bg-desktop); border-radius: var(--search-border-radius); }
.search-form--desktop .search-input-container { padding: 0.25rem; background-color: var(--search-bg-desktop); border-radius: var(--search-border-radius); }
.search-form--desktop .search-input { padding-right: 3.5rem; border: none; background-color: transparent; }
.search-form--desktop .search-input:focus { outline: none; border: none; }
.search-form--desktop .search-input-container:has(.search-input:focus) { outline: 2px solid var(--search-focus-outline); outline-offset: 2px; }
.search-form--desktop .search-submit-button { background: none; border: none; padding: 0.5rem; cursor: pointer; position: absolute; right: 0.75rem; top: 0; bottom: 0; margin: auto 0; color: var(--search-icon-color); display: flex; align-items: center; justify-content: center; border-radius: var(--search-border-radius); height: calc(100% - 0.5rem); }
.search-form--desktop .search-submit-button svg { display: block; fill: currentColor; transition: fill var(--transition-speed) ease; }
.search-form--desktop .search-submit-button:hover svg { fill: var(--search-svg-hover-color); } }
@media (max-width: 768px) { .search-form--desktop { display: none !important; }
.search-container.search-active .search-form--mobile { display: flex; flex-direction: row; align-items: stretch; animation-name: slideDown; }
@keyframes slideDown { from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); } }
.search-form--mobile { background-color: var(--search-bg-mobile); border-radius: 0; padding: 0.5rem; box-sizing: border-box; }
.search-form--mobile .search-input-container { margin-bottom: 0; margin-right: 0.5rem; }
.search-form--mobile .search-input { padding-left: 3rem; background-color: #fff; border-color: #ced4da; color: #212529; border-radius: var(--search-border-radius); }
.search-form--mobile .search-input::placeholder { color: #6c757d; }
.search-form--mobile .search-input:focus { background-color: #fff; border-color: var(--search-focus-outline); color: #212529; outline: 2px solid var(--search-focus-outline); outline-offset: 2px; }
.search-form--mobile .search-icon { left: 0.75rem; color: var(--search-icon-color); }
.search-form--mobile .search-action-button { background-color: transparent; border: 1px solid var(--search-mobile-button-border); color: var(--search-mobile-text-color); padding: var(--search-padding); border-radius: var(--search-border-radius); cursor: pointer; font-size: 1rem; line-height: normal; text-align: center; transition: background-color var(--transition-speed) ease, border-color var(--transition-speed) ease; flex-shrink: 0; white-space: nowrap; display: inline-flex; align-items: center; box-sizing: border-box; }
.search-form--mobile .search-action-button:hover { background-color: rgba(255, 255, 255, 0.1); border-color: var(--search-mobile-text-color); } } </style></head>
<body> <p>Pulsa el icono para abrir el formulario de búsqueda. También puedes reducir el tamaño de la pantalla para ver la versión móvil. </p> <div class="search-container" id="search-container"> <button class="search-toggle-button" id="search-toggle" aria-label="Open search bar"> <svg viewBox="0 0 24 24" width="24" height="24" role="presentation"> <path d="M10.25 2a8.25 8.25 0 0 1 6.34 13.53l5.69 5.69a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-5.69-5.69A8.25 8.25 0 1 1 10.25 2ZM3.5 10.25a6.75 6.75 0 1 0 13.5 0 6.75 6.75 0 0 0-13.5 0Z"> </path> </svg> </button>
<form action="/search" method="get" class="search-form search-form--desktop" id="search-form-desktop" role="search"> <div class="search-input-container"> <input type="text" name="q" class="search-input" placeholder="Search the blog…" aria-label="Search the blog"> <button type="submit" class="search-submit-button" aria-label="Submit search"> <svg viewBox="0 0 24 24" width="20" height="20" role="presentation"> <path d="M10.25 2a8.25 8.25 0 0 1 6.34 13.53l5.69 5.69a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-5.69-5.69A8.25 8.25 0 1 1 10.25 2ZM3.5 10.25a6.75 6.75 0 1 0 13.5 0 6.75 6.75 0 0 0-13.5 0Z"> </path> </svg> </button> </div> </form>
<form action="/search" method="get" class="search-form search-form--mobile" id="search-form-mobile" role="search"> <div class="search-input-container"> <span class="search-icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="20" height="20" role="presentation"> <path d="M10.25 2a8.25 8.25 0 0 1 6.34 13.53l5.69 5.69a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-5.69-5.69A8.25 8.25 0 1 1 10.25 2ZM3.5 10.25a6.75 6.75 0 1 0 13.5 0 6.75 6.75 0 0 0-13.5 0Z"> </path> </svg> </span> <input type="text" name="q" class="search-input" placeholder="Search the blog…" aria-label="Search the blog"> </div> <button type="submit" class="search-action-button"> Search </button> </form> </div>
<script> const searchContainer = document.getElementById('search-container'); const searchToggle = document.getElementById('search-toggle'); const searchFormDesktop = document.getElementById('search-form-desktop'); const searchFormMobile = document.getElementById('search-form-mobile'); const desktopInput = searchFormDesktop.querySelector('.search-input'); const mobileInput = searchFormMobile.querySelector('.search-input');
searchToggle.addEventListener('click', (event) => { event.stopPropagation(); searchContainer.classList.toggle('search-active'); if (searchContainer.classList.contains('search-active')) { setTimeout(() => { if (window.getComputedStyle(searchFormDesktop).display !== 'none') { desktopInput.focus(); } else if (window.getComputedStyle(searchFormMobile).display !== 'none') { mobileInput.focus(); } }, 50); } });
document.addEventListener('click', (event) => { if (!searchContainer.contains(event.target) && searchContainer.classList.contains('search-active')) { searchContainer.classList.remove('search-active'); } });
searchFormDesktop.addEventListener('click', (event) => { event.stopPropagation(); }); searchFormMobile.addEventListener('click', (event) => { event.stopPropagation(); });
window.addEventListener('resize', () => { if (searchContainer.classList.contains('search-active')) { searchContainer.classList.remove('search-active'); } }); </script>
</body>
</html>
Esta implementación ofrece una solución práctica que puede integrarse sin dependencias externas. A diferencia de otros componentes más pesados o que requieren frameworks, el código es directo y adaptable a distintas necesidades. Puede personalizarse fácilmente para distintos estilos visuales o flujos de búsqueda según el proyecto.
Nos enfocamos en lograr una estructura clara, un diseño adaptable y una interacción mínima pero efectiva. El resultado es un componente reutilizable que mejora la accesibilidad y la experiencia de navegación.