Ir al contenido
Evaluar mi proyecto
Componentes · HerosDisponible

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.

search-hero.tsx140 líneas
Descargar
"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.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/hero-buscador

Ejemplo de uso de Hero con buscador

Después de copiar search-hero.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
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.

PropiedadTipoDescripción
title?stringOpcional.
description?stringOpcional.
suggestions?SearchSuggestion[]Opcional.
placeholders?string[]Frases que rotan en el placeholder cuando el campo está vacío.
onSelect?(suggestion: SearchSuggestion) => voidOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

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".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React · Next.js (quita la directiva use client fuera de Next.js) · Sin Tailwind

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.