Saltar al contenido
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
Primeros pasosWebReactSwiftUIJetpack ComposeFlutterReact NativePreajustesTu propio sistema de diseñoUsar Tanrei con agentes de IAMarcaTokens de diseñoSistemas de diseño y personalizaciónTheme StudioColorTipografíaIconosFormaElevaciónMovimientoEstados de interacciónComportamientoDiseño de páginaAccesibilidadTextosTokens de colorTokens de la escala tipográficaTokens de formaTokens de elevaciónTokens de movimientoTokens de estadoTokens de espacio y de medidaComponentesBotónBotón de acción principalBotón segmentadoGrupo de botonesBotón con menúBarra de herramientasBotón de copiarCasilla de verificaciónBotón de opciónInterruptorChipMenúControl deslizanteCampo de control deslizanteCalendarioDateTimePickerGrupo de camposGrupo de botones de opciónGrupo de casillas de verificaciónSelectCampo de selecciónBarra de menúsSubida de archivosSelector de archivos AvanzadoZona de soltar archivosEntrada de texto AvanzadoCampo de textoCampo de chipsCampoCuadro combinado AvanzadoCampo de cuadro combinadoEntrada numérica AvanzadoCampo numéricoEntrada de color AvanzadoCampo de colorEntrada de código de un solo uso AvanzadoCampo de código de un solo usoEntrada de texto incrustada AvanzadoCuadro de redacción de chatTarjetaDivisorCapa modal AvanzadoDiálogoListaFila de listaHoja inferiorHoja lateralCarruselBloque de códigoTablaPopoverPasosAcordeónLista de descripcionesFiguraEstructura de la aplicaciónCabecera de páginaVista de árbolLínea de tiempoVista previa de imágenesTirador de tamañoMarco redimensionableÁrea de desplazamientoInsigniaIndicador de progresoMedidorContador animadoIndicador de cargaSnackbarTooltipAtajo de tecladoTagCódigo en líneaCalloutAvatarEstado vacíoGrupo de tagsEsqueletoElemento de archivoMuestraTexto en marquesinaPestañasVista de pestañasBarra de la aplicaciónBarra de navegaciónPanel de navegaciónBúsquedaEnlacePaginaciónEnlaces anterior y siguienteIndicador de pasosRuta de navegaciónTabla de contenidoTarjeta de enlaceMenú FABEstadoCómo se construye TanreiCómo contribuirLicencia y atribución
InicioPrimeros pasosWeb

Web

Usa los tokens CSS y los Web Components de Tanrei en cualquier proyecto web.

content_copyVer en Markdown

@tanrei/web contiene dos cosas: una hoja de estilos con los tokens de diseño como propiedades personalizadas de CSS, y elementos personalizados basados en estándares, construidos con Lit. No requiere ningún framework.

Prelanzamiento

0.1.0-next.0 está publicado en npm con la etiqueta de distribución (dist-tag) next: hasta 0.1.0, instala @tanrei/web@next. Durante los prelanzamientos 0.x no se mantiene la compatibilidad con versiones anteriores: cualquier prelanzamiento puede romper cualquier cosa, sin un periodo de obsolescencia previo, y su changeset dice qué se rompió.

Instalación

Desde npm

npm install @tanrei/web

Desde el código fuente

Para usar el código fuente tal como está en main, antes de que se publique, necesitas Git, Node 22.22 o posterior y pnpm 12 (corepack enable te da la versión que fija el repositorio). Compila el paquete en un clon del repositorio, empaquétalo e instala el archivo empaquetado en tu proyecto:

git clone https://github.com/chnotchy/tanrei.git
cd tanrei
pnpm install
pnpm --filter @tanrei/web build
pnpm --filter @tanrei/web pack --pack-destination ../tanrei-packages
# en tu proyecto: la ruta a la carpeta en la que se escribieron los paquetes
npm install path/to/tanrei-packages/tanrei-web-*.tgz

El archivo empaquetado contiene lo mismo que publica un lanzamiento, así que todo lo que sigue funciona igual. Para usar un estado más reciente del código fuente, haz pull, compila y empaqueta de nuevo, y luego instala el archivo nuevo.

Uso

  1. Carga las hojas de estilo una sola vez, lo antes posible. Están envueltas en capas de cascada (tanrei.*), así que tus propios estilos siempre prevalecen.

    import '@tanrei/web/tanrei.css';

    tanrei.css lo tiene todo en un archivo: la base de la página, los tokens y las hojas de estilo de las partes que se dibujan sin un elemento (los estilos de texto, la tabla, el acordeón y las demás). Para cargar solo lo que usas, toma los tokens, la base si la quieres, y cada una de esas hojas de estilo cuando la necesites:

    import '@tanrei/web/tokens.css';
    import '@tanrei/web/base.css'; // la página con la superficie, el color de texto y la fuente de cuerpo del sistema, sin margen en el body

    La base da a la página la superficie del sistema, a tu propio texto el color on-surface y la fuente de cuerpo, y quita el margen que el navegador pone al body. Sin ella, la página conserva los valores predeterminados del navegador.

  2. Registra los componentes de tu sistema de diseño, o solo los elementos que uses.

    import '@tanrei/web';
    // con un preajuste, su entrada en su lugar: m3-like, m3-expressive-like o cupertino
    import '@tanrei/web/m3-like';
    // o bien, un elemento cada vez:
    import '@tanrei/web/components/button';

    @tanrei/web registra los componentes que incluye el sistema predeterminado y ningún otro, así que un producto construido sobre él no puede llegar a uno que su sistema no tiene. La entrada de un preajuste, @tanrei/web/<id>, registra lo que ese preajuste incluye, y @tanrei/web/all, todos los componentes del paquete. Un componente que solo incluye un preajuste, como el botón segmentado, se puede usar igualmente a propósito: inclúyelo en la capa de tu sistema e importa su propio módulo.

    Los especificadores @tanrei/web/… son exportaciones del paquete: los resuelve un empaquetador (Vite, webpack, esbuild). Sin él, un navegador no puede resolver un especificador desnudo en <link> ni en <script src>, así que sirve el directorio dist/ del paquete y apunta a los archivos: las hojas de estilos tal como están, y tanrei.bundle.js, que incluye Lit y todos los elementos:

    <link rel="stylesheet" href="/node_modules/@tanrei/web/dist/tanrei.css" />
    <script type="module" src="/node_modules/@tanrei/web/dist/tanrei.bundle.js"></script>
  3. Usa los elementos como cualquier otro HTML.

    <tanrei-button variant="filled">Desplegar</tanrei-button>
    <tanrei-button variant="outlined" size="small" disabled>Cancelar</tanrei-button>
  4. Da los textos de tu producto. Un componente no tiene textos propios: el texto de un tag de obligatoriedad, el nombre de un botón de cerrar y otros parecidos son de tu producto. Para empezar con un inglés llano en todos ellos, importa una vez el paquete de textos en inglés:

    import '@tanrei/web/words/en';

    Para cambiar cualquiera de ellos, o para dar los textos de otro idioma, llama a setWords() después. Un texto que das gana sobre el paquete, y un texto en un elemento gana sobre ambos.

    import { setWords } from '@tanrei/web';
    
    setWords({ requiredText: 'Mandatory' });
    setWords({ requiredText: 'Obligatorio', optionalText: 'Opcional', closeLabel: 'Cerrar' }, { lang: 'es' });

    Sin el paquete y sin tus textos, un componente sigue funcionando, y te dice en la consola cuál le falta. Consulta Textos.

En vivo

DesplegarCancelar

El manifiesto de los elementos

Los editores y los agentes de programación leen los elementos del paquete en su manifiesto de custom elements, @tanrei/web/custom-elements.json: cada elemento con sus atributos, propiedades, eventos, slots y partes, y nada que un producto no pueda alcanzar. @tanrei/web/custom-elements/<id>.json contiene solo los elementos que incluye el sistema <id>, así que una herramienta que trabaja en un sistema no ofrece nada más.

Tema y contraste

Los valores de los tokens usan light-dark(), de modo que siguen el color-scheme del elemento en el que se usan. Sin configurar nada, eso equivale al ajuste del sistema operativo, y prefers-contrast: more cambia al esquema de contraste alto.

Para tomar el control, define estos atributos:

<!-- Fuerza un tema para la página o para cualquier subárbol -->
<html data-tanrei-theme="dark">

<!-- Fuerza un nivel de contraste para la página: "medium" o "high" -->
<html data-tanrei-contrast="high">

Nota

Ambos atributos funcionan en <html> o en cualquier subárbol. La hoja de estilos generada declara el conjunto completo de tokens en :root, [data-tanrei-contrast], de modo que un data-tanrei-contrast en un elemento anidado vuelve a resolver, para ese nivel de contraste, los tokens de componente que hay dentro, y no solo los colores de sistema.

Conservar el tema que elige una persona

Si en tu producto se puede elegir un tema, guarda la elección y ponla en <html> antes de que la página se dibuje por primera vez: si la pone un script que se ejecuta después, la página muestra el tema del sistema durante un fotograma. @tanrei/web/theme-script hace las dos cosas. themeScript() es un script pequeño que se escribe en línea en <head>, antes de las hojas de estilo, y saveTheme() guarda una elección y la dibuja al momento.

import { loadTheme, saveTheme } from '@tanrei/web/theme-script';

saveTheme('dark'); // 'light', 'dark', o 'system' para volver a seguir al sistema operativo
loadTheme();       // lo guardado: 'light', 'dark' o 'system'

En Next.js (App Router), escríbelo en línea en el layout raíz. suppressHydrationWarning está ahí porque el script cambia un atributo de <html> antes de que React lo hidrate.

// app/layout.tsx
import { themeScript } from '@tanrei/web/theme-script';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <script dangerouslySetInnerHTML={{ __html: themeScript() }} />
      </head>
      <body>{children}</body>
    </html>
  );
}

En Astro, escríbelo en línea en el layout:

---
// src/layouts/Layout.astro
import { themeScript } from '@tanrei/web/theme-script';
---
<html lang="en">
  <head>
    <script is:inline set:html={themeScript()} />
  </head>
  <body><slot /></body>
</html>

Tokens en tu propio CSS

Cada token es una propiedad personalizada que lleva el nombre de su ruta: tanrei.sys.color.primary se convierte en --tanrei-sys-color-primary.

.panel {
  background: var(--tanrei-sys-color-surface-container-low);
  border-radius: var(--tanrei-sys-shape-corner-medium);
  box-shadow: var(--tanrei-sys-elevation-level1);
  font: var(--tanrei-sys-typescale-body-font);
}

Para poner un token desde un script, usa setTokens() de @tanrei/web: setTokens({ '--tanrei-sys-color-primary': '#0b57d0' }) en la raíz, o con { on: element } para una parte de la página. Un solo token puesto en la raíz con style.setProperty() hace que Safari vuelva a calcular los estilos de toda la página durante segundos; setTokens() lo escribe de forma que no ocurra.

Estilos de texto

Los estilos de texto se nombran por nivel (heading-2 es un h2, body un p, label el texto de un control), y cada uno es un conjunto de propiedades personalizadas: --tanrei-sys-typescale-<role>-font como forma abreviada, además de -font-family, -font-size, -font-weight, -line-height, -letter-spacing y -font-weight-emphasized. Dos hojas de estilos opcionales los aplican a los elementos por ti:

import '@tanrei/web/text.css';   // .tanrei-text-<role>, .tanrei-text-emphasized
import '@tanrei/web/prose.css';  // h1–h6, p, small, code, pre dentro de .tanrei-prose
<h2 class="tanrei-text-heading-2">Sección</h2>
<p class="tanrei-text-body">Texto corrido con <strong class="tanrei-text-emphasized">énfasis</strong>.</p>
<article class="tanrei-prose"><!-- Markdown ya convertido a HTML --></article>

.tanrei-text-emphasized dibuja el rol en el que se encuentra con su peso más el paso de énfasis de la familia; es un peso, no otro rol. Los dieciséis roles y su correspondencia con los elementos están en Tipografía.

Fuentes

La escala tipográfica pide Inter, Noto Sans JP, Noto Sans y Roboto Mono (--tanrei-ref-typeface-{brand,plain,mono} en tokens.css; tanrei build imprime la lista de cualquier sistema), los iconos piden Material Symbols Rounded, y todo recurre a las fuentes del sistema si faltan. Tanrei no incluye archivos de fuentes: cárgalos tú con exactamente esos nombres de familia, desde Google Fonts o desde los paquetes @fontsource-variable/* (que registran Inter Variable, etc.: declara tu propio @font-face con el nombre simple apuntando a sus archivos, como hace este sitio).

Iconos

import '@tanrei/web/icons.css';
<tanrei-button><span slot="icon" class="tanrei-icon" aria-hidden="true">add</span>Añadir</tanrei-button>

.tanrei-icon dibuja un nombre de Material Symbols con el peso de icono del sistema y toma su tamaño del lugar donde se encuentra. Nunca eliges entre contorno y relleno: consulta Iconos.

El archivo completo de Material Symbols Rounded contiene todos los glifos en todos los pesos, rellenos, grados y tamaños ópticos: 5,4 MB, que una página con una conexión lenta puede tardar medio minuto en recibir. Carga solo los glifos que dibuja tu producto. tanrei build muestra los nombres que los componentes de Tanrei pueden dibujar por sí mismos (un cheurón, una marca de verificación, la cruz de un botón de cerrar…) y los valores de eje con los que tu sistema dibuja los iconos; añade tus propios nombres a esa lista. Desde Google Fonts, pasa los nombres, en orden alfabético, en icon_names= y los ejes como rangos en family= (Material+Symbols+Rounded:opsz,wght,FILL,[email protected],300..500,0..1,0 para el sistema predeterminado). Si sirves el archivo tú mismo, recórtalo con un generador de subconjuntos que conserve las funciones de ligadura de la fuente (rlig, rclt) y los dos rellenos; este sitio dibuja sus iconos con un archivo así. En ambos casos, registra la fuente de los iconos con font-display: block: hasta que llega el archivo, un icono es invisible y nunca aparece su nombre en letras.

Compatibilidad con navegadores

Tanrei funciona en estas versiones y en las posteriores:

Navegador Versión
Chrome y Edge 125
Firefox 126
Safari, en macOS e iOS 17.5

Cada número es la versión que trajo la más reciente de las funciones sin las que Tanrei no puede dibujar, y ninguna tiene alternativa:

  • CSS. Cada color es un par de light-dark() (Chrome y Edge 123, Firefox 120, Safari 17.5). Los tamaños y los tamaños de letra se redondean con la función round() de CSS (Chrome y Edge 125, Firefox 118, Safari 15.4). Los componentes cambian de estilo según estados personalizados, :state() (Chrome y Edge 125, Firefox 126, Safari 17.4). Los menús, los tooltips y los snackbars se abren en la capa superior con la API Popover (Chrome y Edge 114, Firefox 125, Safari 17). Las capas de cascada que envuelven las hojas de estilo llegaron antes (Chrome y Edge 99, Firefox 97, Safari 15.4).
  • JavaScript. El paquete es ES2022, y su compilación falla si algún archivo que publica va más allá. Su sintaxis más reciente, los bloques estáticos de clase, funciona desde Chrome y Edge 94, Firefox 93 y Safari 16.4. Transpilar el paquete a un objetivo más antiguo no baja el mínimo: el CSS de arriba sigue siendo el mismo.

Algunas funciones más recientes se usan solo donde un navegador sin ellas sigue funcionando: sin @starting-style no se reproduce la animación de apertura, y sin corner-shape la esquina se queda redondeada.

Avisar al visitante de que su navegador es demasiado antiguo

Tanrei no lo hace por sí mismo: le corresponde al punto de entrada de tu producto. Comprueba primero las funciones, y carga la aplicación, y Tanrei con ella, solo cuando estén disponibles:

<p id="unsupported" hidden>Este sitio necesita Chrome o Edge 125, Firefox 126 o Safari 17.5, o una versión posterior.</p>
<script type="module">
  // light-dark() fija el mínimo de Safari; round(), el de Chrome y Edge; :state(), el de Firefox.
  const supported =
    CSS.supports('color', 'light-dark(red, red)') &&
    CSS.supports('width', 'round(1px, 1px)') &&
    CSS.supports('selector(:state(open))');
  if (supported) import('./app.js'); // tu aplicación, que importa @tanrei/web
  else document.getElementById('unsupported').hidden = false;
</script>

Mantén este script pequeño y sencillo: lo ejecuta el navegador de cada visitante, también los antiguos. Con un import estático, el navegador carga la aplicación antes de ejecutar este script, y uno antiguo que no pueda analizarla nunca llega a hacer la comprobación; import() la carga solo después.

Pruebas

En un navegador

Los elementos de Tanrei se apoyan en lo que implementan los navegadores: shadow roots, ElementInternals (el valor y la validez de un control de formulario, los estados personalizados), la API Popover, <dialog> y el diseño de la página. jsdom y happy-dom solo implementan una parte (happy-dom no tiene attachInternals() ni la API Popover), así que un elemento puede lanzar un error al renderizarse, dejar una promesa rechazada o renderizarse sin los tamaños, el orden de foco y la capa superior de los que depende. Tanrei no los admite y no incluye sustitutos para lo que les falta: un sustituto que evita que un elemento falle haría pasar una prueba con un comportamiento que no tiene ningún navegador.

Ejecuta en un navegador real todo lo que renderice un elemento de Tanrei:

Las pruebas de tu propia lógica que no renderizan ningún elemento de Tanrei pueden seguir en Node, jsdom o happy-dom. Las pruebas de Tanrei se ejecutan en Chromium, Firefox y WebKit, con axe-core en Playwright.

Encontrar un elemento

La mayoría de los elementos ponen su rol en un elemento nativo dentro de su shadow root (un <button>, un <input>, un <dialog>) y no en sí mismos, para que los navegadores y las herramientas de pruebas vean una semántica real; unos pocos, como una pestaña o un menú abierto, lo llevan ellos mismos. Los localizadores de Playwright buscan dentro de los shadow roots abiertos, así que busca un control por su rol y su nombre, como lo haría una persona:

await page.getByRole('button', { name: 'Guardar' }).click();
await expect(page.getByRole('switch', { name: 'Wi-Fi' })).toBeChecked();

Un clic en ese botón llega a los listeners del propio <tanrei-button>. Las consultas de Testing Library no buscan dentro de los shadow roots, así que no encuentran estos controles.

Un rol y su contenido en árboles distintos

Algunos elementos guardan un rol en su shadow root, mientras que lo que le pertenece es tu propio marcado, colocado en él mediante un slot: la lista de pestañas de <tanrei-tabs> y sus hijos <tanrei-tab>, el <dialog> de <tanrei-dialog> y sus acciones. En el DOM las pestañas no están dentro de la lista de pestañas, así que un localizador encadenado desde el rol no encuentra nada:

// No encuentra nada: las pestañas son hijas de <tanrei-tabs>, no de su lista de pestañas.
page.getByRole('tablist', { name: 'Secciones' }).getByRole('tab', { name: 'Facturación' });

Busca el elemento que contiene el rol con ese nombre y luego busca dentro de él:

const sections = page.locator('tanrei-tabs', { has: page.getByRole('tablist', { name: 'Secciones' }) });
await sections.getByRole('tab', { name: 'Facturación' }).click();

const confirm = page.locator('tanrei-dialog', { has: page.getByRole('dialog', { name: '¿Eliminar el archivo?' }) });
await confirm.getByRole('button', { name: 'Eliminar' }).click();

Un menú en la capa superior

Mientras está abierto, <tanrei-menu> es el menú, con el nombre de su ancla, pero no ocupa espacio en la página: su superficie se dibuja en la capa superior. Por eso, comprobar si el menú es visible falla incluso cuando está abierto. Comprueba que el menú está ahí y después comprueba sus elementos:

await page.getByRole('button', { name: 'Más' }).click();
const menu = page.getByRole('menu', { name: 'Más' });
await expect(menu).toBeAttached();
await expect(menu.getByRole('menuitem', { name: 'Cambiar nombre' })).toBeVisible();

Al cerrarse, el elemento deja de ser un menú y await expect(menu).toHaveCount(0) pasa.

Avisos

En una compilación de desarrollo, un elemento que se usa de una forma que su especificación no permite lo indica en la consola, con un mensaje que empieza por [tanrei] y termina con el identificador del comportamiento. Una prueba en el navegador puede fallar ante cualquiera de ellos: la guía para agentes de IA tiene un ejemplo con Playwright.