Catálogo · Todo el sitio
Búsqueda
Permite encontrar por su nombre una página, sección o elemento HTML del sitio, mediante un botón de la cabecera.
Encaja mejor en: cualquier sitio — cuando el sitio tiene más páginas de las que se pueden recorrer a simple vista
Ejemplo
El botón de búsqueda de una cabecera abre el mismo diálogo que aparece en la parte superior de esta página.
A second button for the search in this site’s header: it opens the same dialog. Without JavaScript it is a link to the A–Z index.
Problemas conocidos
La búsqueda todavía no se ha probado con un lector de pantalla. Ctrl+K abre esta búsqueda en lugar del cuadro de búsqueda web del navegador; usa la barra de direcciones para buscar en la web.
Con catalog.js, si pulsas Ctrl+K antes de que se haya cargado el script de búsqueda, este se carga y luego abre el diálogo; la espera puede ser algo más larga con una conexión lenta. Con un zoom del 400 %, se desplaza todo el diálogo, así que el campo puede quedar fuera de la vista mientras lees los resultados.
Código
Copiar el marcado
import "@luz-ds/catalog/components/search.css";
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@luz-ds/catalog@0.3.0/dist/components/search.min.css" integrity="sha384-lktvmtjXJkC+jS1MMF6Yy/v6A4rPYhVrr92UquHxNMnB5PN/SyACqXPtuRFN6FUr" crossorigin="anonymous">
Un <luz-search> contiene el enlace a tu índice, el botón que lo sustituye y el diálogo. Colócalo donde vaya la búsqueda, normalmente al final de la cabecera: el enlace y el botón comparten el mismo espacio, así que nada se desplaza cuando el botón toma el relevo, y el diálogo cerrado no ocupa espacio.
Todos los textos son tuyos: escríbelos en el idioma de la página.
<luz-search>
<a class="luz-search__fallback" href="/a-z/" data-luz-search-fallback>
<svg class="luz-search__trigger-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="6.5"/><path d="M16 16l4.5 4.5"/></svg>
<span class="luz-search__fallback-label">A–Z index</span>
</a>
<button class="luz-search__trigger luz-search__trigger--compact" type="button" data-luz-search-open
aria-haspopup="dialog" aria-keyshortcuts="Control+K Meta+K" hidden>
<svg class="luz-search__trigger-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="6.5"/><path d="M16 16l4.5 4.5"/></svg>
<span class="luz-search__trigger-label">Search docs…</span>
<kbd class="luz-search__trigger-key" aria-hidden="true">Ctrl K</kbd>
</button>
<dialog class="luz-modal luz-search" aria-labelledby="search-title"
data-luz-search-src="/search-index.json"
data-luz-label-results="{n} results"
data-luz-label-result="1 result"
data-luz-label-capped="{shown} of {n} results. Type more to narrow."
data-luz-label-none="No results for “{query}”. Check the spelling or try a shorter word."
data-luz-label-loading="Loading the search index…"
data-luz-label-error="Search is not available right now.">
<search class="luz-search__body">
<div class="luz-search__head">
<h2 class="luz-search__title" id="search-title"><label for="search-q">Search the documentation</label></h2>
<form method="dialog"><button class="luz-btn luz-btn--ghost luz-search__close" aria-label="Close search">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6 6 18"/></svg>
</button></form>
</div>
<div class="luz-search__field">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="6.5"/><path d="M16 16l4.5 4.5"/></svg>
<input class="luz-input" id="search-q" type="text" maxlength="100" autocomplete="off"
spellcheck="false" enterkeyhint="go" autofocus aria-describedby="search-hint">
</div>
<p class="luz-field__helper" id="search-hint">Results update as you type. Arrow keys move through them.</p>
<p class="luz-search__status" role="status"></p>
<div class="luz-search__results" data-luz-search-results>
<div hidden><h3>Pages</h3><ul data-luz-search-list="pages"></ul></div>
<div hidden><h3>Sections</h3><ul data-luz-search-list="sections"></ul></div>
<div hidden><h3>HTML elements</h3><ul data-luz-search-list="elements"></ul></div>
</div>
<p class="luz-search__foot">Can’t find it? <a href="/a-z/">Browse the A–Z index</a></p>
</search>
</dialog>
</luz-search>
<script type="module" src="https://cdn.jsdelivr.net/npm/@luz-ds/catalog@0.3.0/dist/catalog.min.js"></script>
Variantes y opciones
Todas las opciones se indican en el marcado: son clases o atributos.
.luz-search__trigger--compact: a 768 px o menos, el activador solo muestra la lupa, en un espacio de 44 por 44 px, igual que el enlace contiguo; sus nombres se mantienen, aunque recortados, no se eliminan. Añade a ambos un<svg class="luz-search__trigger-icon">.- Se puede usar más de un activador: todos los
[data-luz-search-open]de la página abren el diálogo, estén dentro del elemento o no (este sitio tiene uno en la cabecera y otro en la portada de la documentación). Los que estén fuera permanecen ocultos hasta que funcione la búsqueda y no ocupan espacio: colócalos donde su aparición no desplace nada, por ejemplo, al final de una sección. - Un elemento con
data-luz-search-show="idle"dentro de[data-luz-search-results]se muestra mientras el campo está vacío, por ejemplo, con una lista breve de páginas populares; uno condata-luz-search-show="none"se muestra cuando no hay coincidencias. - Omite
[data-luz-search-list]para no incluir ese grupo. - Los textos que escribe el script son los atributos
data-luz-label-*del diálogo:results,result,capped,none,loadingyerror, con{n},{shown}y{query}.data-luz-label-idlees el texto que muestra el estado mientras el campo está vacío, por ejemplo, «Escribe para buscar en la documentación». Si falta una etiqueta, no se escribe nada.
Cómo funciona
Carga catalog.min.js como módulo. No carga nada en páginas sin <luz-search>.
En una página que sí lo tenga, carga el archivo de búsqueda, search.min.js, cuando el lector pasa el puntero por primera vez, mueve el foco al elemento o pulsa Ctrl + K, o cuando el navegador está inactivo, lo que ocurra antes. Ese archivo define <luz-search>, un elemento personalizado en el DOM ligero (sin Shadow DOM), y cada instancia de la página se conecta por sí misma.
- El botón ocupa el mismo espacio que el enlace, así que nada se desplaza. Si el enlace tiene el foco del teclado o el puntero está pulsado en ese momento, el cambio espera a que el lector salga del enlace, lo siga o suelte el puntero: el foco nunca se mueve.
- El botón abre el
<dialog>nativo conshowModal(): el navegador mantiene el foco dentro, lo cierra con Escape y devuelve el foco a su origen. - La primera vez que se abre el diálogo, el script lee el índice.
- Si no se puede cargar
search.min.js, la página conserva el comportamiento sin JavaScript: el enlace funciona y el botón permanece oculto. - Si el diálogo no tiene campo,
role="status"o bloque[data-luz-search-results], la búsqueda no se conecta: el enlace permanece y el botón sigue oculto. - Si un router o framework añade un
<luz-search>después de cargar la página,catalog.min.jsno lo detecta: importa@luz-ds/catalog/search(osearch.min.js) a mano y se conectará al elemento cuando este se inserte. Aunque haya dos copias del archivo en una página, el elemento solo se define una vez.
El índice es un archivo JSON de la versión 1. Cada lista contiene [text, url, meta, lang]; meta (que se muestra atenuado después del texto) y lang son opcionales, y las URL son relativas al archivo:
{
"version": 1,
"pages": [["Button", "components/button/", "Core"]],
"sections": [["When to use", "components/button/#when", "Button › Design"]],
"elements": [["<table> element, Tables", "html/tables/#el-table"]]
}
Un resultado coincide cuando el texto y los metadatos contienen todas las palabras escritas, sin distinguir mayúsculas, minúsculas ni acentos; los que empiezan por esas palabras aparecen primero. Cada grupo muestra un máximo de 8 resultados y la línea de estado indica cuántos hay.
Qué acepta el componente para que un índice generado por un CMS no pueda dañar la página:
- El JSON solo puede proceder del mismo origen de la página, por http o https; si hay una redirección, la solicitud falla. Tu Content-Security-Policy necesita
connect-src 'self'; para usar un índice de otro sitio, aloja una copia en el tuyo. El navegador lo almacena en caché como cualquier archivo. - Los enlaces solo pueden ser URL
http:yhttps:; se omite cualquier otro elemento. El texto se escribe como texto, nunca como HTML. - Límites: 1 000 000 de caracteres JSON, 10 000 resultados, 300 caracteres por texto, 8 palabras por búsqueda y 100 caracteres en el campo. Se ignoran los campos desconocidos. Si el índice no es de la versión 1, se muestra el mensaje de error.
Sin JavaScript
El botón permanece oculto, no recibe el foco ni se lee; en su lugar se muestra el enlace. Crea una página que enumere todas las páginas por nombre, como un índice alfabético. El diálogo nunca se abre.
Teclado y ARIA
Todo funciona con el teclado mediante un atajo.
- Ctrl + K o ⌘ + K abre la búsqueda desde cualquier lugar de la página, excepto mientras escribes en un área de texto o una región editable, o mientras haya otro diálogo abierto. Si lo pulsas de nuevo, selecciona el texto del campo. Si lo pulsas antes de que se cargue
search.min.js, lo carga y luego abre el diálogo; en una página sin<luz-search>, el navegador conserva el atajo. - La flecha abajo pasa del campo al primer resultado; las flechas arriba y abajo recorren los resultados, incluso entre grupos; Inicio y Fin van al primero y al último. La flecha arriba en el primer resultado vuelve al campo.
- Enter en el campo abre el primer resultado; en un resultado, sigue el enlace. Si escribes mientras tienes un resultado enfocado, vuelves al campo.
- Escape o el botón de cierre (un icono × llamado Cerrar búsqueda mediante su
aria-label) cierra el diálogo, sea cual sea el contenido del campo, y devuelve el foco a su posición anterior. Por eso el campo estype="text": en unotype="search", Chrome y Safari usan la primera pulsación de Escape para borrar el texto. Si un resultado lleva a la misma página, el diálogo se cierra y el foco pasa al encabezado de esa sección. Si al abrirlo nada tenía el foco (Ctrl+K en una página recién cargada o un clic en un navegador que no enfoca botones), el script lo pasa al botón que abrió el diálogo o al propio botón de búsqueda tras Ctrl+K, para que la siguiente pulsación de Tab continúe desde la cabecera. - El encabezado da nombre al diálogo y etiqueta el campo. El recuento se anuncia cuando el lector deja de escribir (
role="status"). Los resultados son enlaces en listas bajo encabezados, no un combobox.
El contraste y el foco están en la pestaña Diseño.
Diseño
Cuándo usarlo
Usa la búsqueda en sitios con más de unas pocas páginas, donde el lector pueda buscar un componente, un ajuste o el título de un artículo por su nombre.
Cuándo no usarlo
No uses la búsqueda en lugar de la navegación: mantén el menú y una página de índice.
- La búsqueda requiere escribir y conocer el nombre, que quizá el lector desconozca.
- Solo encuentra coincidencias en títulos y encabezados: no puede buscar dentro de textos largos ni en varios sitios.
Qué hacer y qué no
Cómo escribir el contenido
Todo el texto está en el marcado, así que puedes redactarlo en el idioma de la página.
- El activador indica qué se busca: «Buscar en la documentación…». Mantén el texto breve en todos los idiomas («Buscar…») en lugar de recortarlo.
- Añade la página a los metadatos de los resultados de secciones: «Cuándo usarlo» con «Botón › Diseño», para que cada resultado se entienda por sí mismo y se encuentre por el nombre de la página.
- Indica qué probar a continuación en la etiqueta
none, que la línea de estado anuncia después de la consulta: «Comprueba la ortografía o prueba con una palabra más corta». - Haz que el enlace alternativo y la línea debajo de los resultados lleven a la misma página de índice.
Contraste y foco
El texto del botón usa el color de texto secundario sobre el color del campo, no el gris de un marcador de posición; los resultados aparecen sobre la superficie del diálogo.
| Parte | Color sobre el fondo | Claro | Oscuro | Alto contraste | Mínimo necesario |
|---|---|---|---|---|---|
Texto del activador | --luz-text-secondary on --luz-input-bg | 6.4:1 | 6.9:1 | 19.3:1 | 4.5:1 |
Texto del activador, al pasar el puntero | --luz-text-primary on --luz-input-bg | 16.5:1 | 14.0:1 | 21.0:1 | 4.5:1 |
Borde del activador | --luz-input-border on --luz-surface-page | 3.7:1 | 4.2:1 | 21.0:1 | 3:1 |
Título del resultado | --luz-brand-primary on --luz-surface | 5.9:1 | 7.2:1 | 14.1:1 | 4.5:1 |
Título del resultado, al pasar el puntero | --luz-brand-primary on --luz-surface-hover | 5.1:1 | 6.4:1 | 13.5:1 | 4.5:1 |
Encabezado del grupo, página del resultado, recuento e indicación | --luz-text-secondary on --luz-surface | 6.4:1 | 6.9:1 | 19.3:1 | 4.5:1 |
Página del resultado, al pasar el puntero | --luz-text-secondary on --luz-surface-hover | 5.6:1 | 6.2:1 | 18.4:1 | 4.5:1 |
Lupa del campo | --luz-text-secondary on --luz-input-bg | 6.4:1 | 6.9:1 | 19.3:1 | 3:1 |
Icono de cierre | --luz-text-secondary on --luz-surface | 6.4:1 | 6.9:1 | 19.3:1 | 3:1 |
Icono de cierre, al pasar el puntero | --luz-text-primary on --luz-surface-hover | 14.4:1 | 12.6:1 | 20.1:1 | 3:1 |
Anillo de foco | --luz-focus-ring on --luz-surface | 5.9:1 | 7.2:1 | 21.0:1 | 3:1 |
Calculado a partir de los valores de los tokens de cada modo y redondeado hacia abajo. El mínimo necesario es el de WCAG 2.2 AA: 4,5:1 para texto y 3:1 para anillos de foco, bordes e iconos.
- Foco: el activador tiene un contorno
--luz-focus-ringde 2 px, a 2 px de distancia; el resultado enfocado tiene el mismo anillo que cualquier enlace. - Bordes: en colores forzados, el diálogo conserva un borde real y la lupa y el icono de cierre se dibujan con el color de texto del sistema.
Tokens que usa
Los 23 tokens que lee components/search.css del catálogo, agrupados por familia. Cada familia enlaza a su página, con los valores de todos los modos. En Figma son variables con los mismos nombres: consulta Tokens en Figma.
- Color
--luz-divider,--luz-focus-ring,--luz-input-bg,--luz-input-border,--luz-surface,--luz-surface-hover,--luz-text-primary,--luz-text-secondary- Spacing and sizing
--luz-control-lg,--luz-control-sm,--luz-space-2xs,--luz-space-lg,--luz-space-md,--luz-space-sm,--luz-space-xs- Typography
--luz-font-heading,--luz-font-mono,--luz-font-sans,--luz-text-lg,--luz-text-sm,--luz-text-xs- Radius
--luz-radius-md,--luz-radius-sm
El teclado y ARIA están en la pestaña Código.