Hero con buscador
Un campo de búsqueda grande al centro con sugerencias que se filtran.
Encuentra lo que buscas, sin buscar.
Escribe lo que necesitas y salta directo a la página, el documento o la respuesta.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
/**
* Hero con buscador: un campo grande al centro con sugerencias que se filtran al escribir, atajo de teclado (/) y un placeholder que cambia.
* Pasa tus resultados en `suggestions`; al elegir uno se llama a `onSelect` o se navega a su `href`.
*
* <SearchHero
* title="Encuentra lo que buscas, sin buscar."
* suggestions={[{ label: "Plantillas de propuesta", href: "/plantillas" }, { label: "Integraciones", href: "/integraciones" }]}
* />
*/
import { useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
export type SearchSuggestion = { label: string; hint?: string; href: string };
export type SearchHeroProps = {
title?: string;
description?: string;
suggestions?: SearchSuggestion[];
/** Frases que rotan en el placeholder cuando el campo está vacío. */
placeholders?: string[];
onSelect?: (suggestion: SearchSuggestion) => void;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gchs-root { --gchs-accent: #ff5a1f; position: relative; display: grid; place-items: center; box-sizing: border-box; min-height: 640px; padding: clamp(40px, 7vw, 96px) clamp(20px, 5vw, 64px); background: radial-gradient(60% 60% at 50% 0%, #fff 0%, transparent 70%), #e9edf4; color: #151a26; text-align: center; font-family: inherit; }
.gchs-inner { width: min(100%, 720px); }
.gchs-title { margin: 0; font-size: clamp(36px, 5.6vw, 72px); font-weight: 700; line-height: 1.04; letter-spacing: -.04em; text-wrap: balance; }
.gchs-desc { margin: 18px auto 0; max-width: 46ch; color: #50586b; font-size: clamp(16px, 1.4vw, 19px); line-height: 1.6; }
.gchs-box { position: relative; margin-top: 34px; text-align: left; }
.gchs-field { display: flex; align-items: center; gap: 14px; min-height: 66px; padding: 0 18px 0 22px; border: 1.5px solid transparent; border-radius: 20px; background: #fff; box-shadow: 0 1px 2px rgba(20, 30, 60, .06), 0 22px 44px -22px rgba(20, 30, 60, .35); transition: border-color .2s, box-shadow .3s; }
.gchs-field:focus-within { border-color: var(--gchs-accent); box-shadow: 0 0 0 5px rgba(255, 90, 31, .14), 0 22px 44px -22px rgba(20, 30, 60, .4); }
.gchs-field > svg { flex: none; width: 24px; height: 24px; color: #6b7388; }
.gchs-input { flex: 1; min-width: 0; padding: 0; border: 0; outline: none; background: transparent; color: inherit; font-family: inherit; font-size: clamp(17px, 1.8vw, 21px); }
.gchs-input::placeholder { color: #8a92a5; }
.gchs-kbd { flex: none; padding: 4px 9px; border: 1px solid #d5dae6; border-bottom-width: 2px; border-radius: 7px; color: #6b7388; font-family: inherit; font-size: 13px; font-weight: 600; }
.gchs-list { position: absolute; z-index: 2; top: calc(100% + 10px); left: 0; right: 0; margin: 0; padding: 8px; overflow: hidden; border-radius: 18px; background: #fff; box-shadow: 0 30px 60px -20px rgba(20, 30, 60, .45), 0 0 0 1px rgba(20, 30, 60, .06); list-style: none; animation: gchs-in .25s cubic-bezier(.16, 1, .3, 1); }
.gchs-item a { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 13px 14px; border-radius: 12px; color: inherit; font-size: 16px; font-weight: 600; text-decoration: none; }
.gchs-item a small { color: #8a92a5; font-size: 13px; font-weight: 500; }
.gchs-item a:hover, .gchs-item a:focus-visible, .gchs-item[data-active="true"] a { background: #fff1ea; outline: none; }
.gchs-empty { padding: 16px 14px; color: #6b7388; font-size: 15px; }
.gchs-chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-top: 22px; }
.gchs-chip { padding: 8px 16px; border: 1px solid #cfd5e3; border-radius: 999px; background: rgba(255, 255, 255, .6); color: #39425a; font-family: inherit; font-size: 14px; font-weight: 600; cursor: pointer; transition: background-color .2s, border-color .2s, transform .25s cubic-bezier(.16, 1, .3, 1); }
.gchs-chip:hover { border-color: var(--gchs-accent); background: #fff; transform: translateY(-2px); }
.gchs-chip:focus-visible { outline: 2px solid var(--gchs-accent); outline-offset: 2px; }
@keyframes gchs-in { from { opacity: 0; transform: translateY(-6px); } }
@media (prefers-reduced-motion: reduce) { .gchs-list { animation: none; } .gchs-chip, .gchs-field { transition: none; } }
`;
const DEFAULT_SUGGESTIONS: SearchSuggestion[] = [
{ label: "Plantillas de propuesta", hint: "Documentos", href: "#plantillas" },
{ label: "Guía de integraciones", hint: "Guías", href: "#integraciones" },
{ label: "Precios y planes", hint: "Cuenta", href: "#precios" },
{ label: "Estado del servicio", hint: "Soporte", href: "#estado" },
{ label: "Contactar a ventas", hint: "Soporte", href: "#contacto" },
];
const DEFAULT_PLACEHOLDERS = ["Busca plantillas…", "Busca integraciones…", "Busca guías…", "Busca precios…"];
export function SearchHero({
title = "Encuentra lo que buscas, sin buscar.",
description = "Escribe lo que necesitas y salta directo a la página, el documento o la respuesta.",
suggestions = DEFAULT_SUGGESTIONS,
placeholders = DEFAULT_PLACEHOLDERS,
onSelect,
className,
style,
}: SearchHeroProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [query, setQuery] = useState("");
const [open, setOpen] = useState(false);
const [tick, setTick] = useState(0);
const results = useMemo(() => {
const needle = query.trim().toLocaleLowerCase("es");
return needle ? suggestions.filter((item) => item.label.toLocaleLowerCase("es").includes(needle)) : suggestions;
}, [query, suggestions]);
useEffect(() => {
const timer = window.setInterval(() => setTick((value) => value + 1), 2600);
return () => window.clearInterval(timer);
}, []);
useEffect(() => {
function onKey(event: KeyboardEvent) {
const typing = (event.target as HTMLElement | null)?.closest("input, textarea, [contenteditable]");
if (event.key === "/" && !typing) { event.preventDefault(); inputRef.current?.focus(); }
if (event.key === "Escape") { setOpen(false); inputRef.current?.blur(); }
}
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);
return (
<>
<style>{CSS}</style>
<section className={["gchs-root", className].filter(Boolean).join(" ")} style={style}>
<div className="gchs-inner">
<h1 className="gchs-title">{title}</h1>
<p className="gchs-desc">{description}</p>
<div className="gchs-box" onBlur={(event) => { if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false); }}>
<div className="gchs-field">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" /></svg>
<input
ref={inputRef}
className="gchs-input"
type="search"
aria-label="Buscar"
autoComplete="off"
value={query}
placeholder={placeholders[tick % placeholders.length]}
onChange={(event) => { setQuery(event.target.value); setOpen(true); }}
onFocus={() => setOpen(true)}
/>
<kbd className="gchs-kbd" aria-hidden="true">/</kbd>
</div>
{open ? (
<ul className="gchs-list" aria-label="Sugerencias">
{results.length ? results.map((item) => (
<li key={item.href} className="gchs-item">
<a href={item.href} onClick={() => onSelect?.(item)}>{item.label}{item.hint ? <small>{item.hint}</small> : null}</a>
</li>
)) : <li className="gchs-empty">Sin resultados para «{query}»</li>}
</ul>
) : null}
</div>
<div className="gchs-chips">
{suggestions.slice(0, 4).map((item) => (
<button key={item.href} type="button" className="gchs-chip" onClick={() => { setQuery(item.label); setOpen(true); inputRef.current?.focus(); }}>{item.label}</button>
))}
</div>
</div>
</section>
</>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/hero-buscadorEjemplo de uso de Hero con buscador
Después de copiar search-hero.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { SearchHero } from "./search-hero";
export default function Ejemplo() {
return <SearchHero title="Ejemplo" />;
}Propiedades de SearchHero
SearchHero acepta 7 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
title? | string | Opcional. |
description? | string | Opcional. |
suggestions? | SearchSuggestion[] | Opcional. |
placeholders? | string[] | Frases que rotan en el placeholder cuando el campo está vacío. |
onSelect? | (suggestion: SearchSuggestion) => void | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 140 líneas (8 KB) y define 1 animación con @keyframes. Esto es lo que hace Hero con buscador por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
- Expone su estado a tecnologías de apoyo con atributos aria-*.
- Responde al teclado, no solo al ratón.
- Es un componente de cliente de Next.js: incluye la directiva "use client".
Acerca del producto
Un hero centrado en un buscador: campo grande con placeholder que cambia, atajo de teclado con la tecla /, lista de sugerencias que se filtra al escribir y chips para probar. Pasa tus propias sugerencias y enlaces.
Incluye
Una sección completa en un solo archivo React + TypeScript, con los estilos dentro. No usa Tailwind ni bibliotecas externas.
- Filtrado de sugerencias mientras escribes
- Atajo / para enfocar y Escape para cerrar
- Placeholder rotativo
- onSelect opcional para tu analítica
- Navegación por teclado con Tab
Cómo usarlo
Pasa suggestions con { label, hint, href } y, si quieres, placeholders y onSelect.