Asistentes de preferencias

Siete paquetes pequeños por framework, cada uno dueño de una tarea del usuario de principio a fin, más una barra que reúne cinco de ellos. Ya conociste theme-picker; aquí están locale-picker, text-size-picker y motion-picker (preferencias), share-picker y search-picker (acciones), date-time-picker (un valor de formulario) y picker-bar.

Qué vas a construir

Un panel de ajustes donde los usuarios eligen su idioma y el tamaño del texto: elecciones que se aplican a todo el documento, persisten entre visitas y te cuestan dos componentes y unas líneas de CSS. Unos 15 minutos.

Antes de empezar

Termina primero el tutorial del framework de tu stack. Los ejemplos de abajo son de Svelte; los asistentes de React, Vue, Angular, HTML, Nunjucks y Blazor coinciden contrato por contrato.

La forma que comparten

  • Un botón de icono sin cabeza que abre un cuadro de lista WAI-ARIA APG (o, en date-time-picker, un diálogo selector de fecha): sin <select>nativo, para que cada framework obtenga el mismo comportamiento de teclado y los mismos ganchos de estilo.
  • Un gancho de clase por paquete (.locale-picker, .text-size-picker, .share-picker, .date-time-picker), cero CSS distribuido.
  • Seguro para SSR: las escrituras en el DOM ocurren solo en el ciclo de vida mount/effect.
  • Limpio para i18n: toda cadena visible para el usuario es una prop.
  • Disponible para Svelte (canónico), React, Vue, Angular, elemento personalizado HTML, Nunjucks y Blazor, con el mismo contrato en cada uno.

theme-picker, locale-picker y text-size-picker son dueños cada uno de una preferencia del usuario de principio a fin (selección, aplicación al DOM, localStorage persistencia opcional mediante la prop storageKey ). share-picker y date-time-picker son distintos por naturaleza: share-picker es dueño de una acción (no aplica nada al documento y no persiste nada) y date-time-picker es dueño de un valor de formulario, porque una fecha en un formulario es un dato, no una preferencia.

locale-picker: idioma y dirección

Deja que el usuario elija una configuración regional y luego escribe lang y dir en la raíz del documento para que tu biblioteca i18n, tu CSS y las tecnologías de apoyo la sigan. RTL se detecta automáticamente para árabe, hebreo y otras escrituras de derecha a izquierda. Se centra solo en señalar: tu biblioteca i18n se encarga de la traducción.

<script>
  import LocalePicker from "@lilydesignsystem/svelte-locale-picker";

  let locale = $state("en-US");
</script>

<LocalePicker
  label="Language"
  locales={["en-US", "fr", "ar", "he", "ja"]}
  bind:value={locale}
  storageKey="my-app-locale"
/>

Las opciones se etiquetan a partir de una tabla integrada de nombres de configuración regional (se puede anular mediante localeLabels), cada opción lleva su propio atributo lang para que los lectores de pantalla pronuncien «Français» en francés, y los códigos con guion bajo como en_US se convierten sin pérdida a BCP 47 y de vuelta en-US. Conecta onChange (o el valor enlazado) a i18next, Paraglide, vue-i18n o lo que uses.

text-size-picker: tamaño controlado por el lector

Una victoria de accesibilidad que lleva minutos: el asistente establece data-text-size="{slug}" en la raíz del documento y tu CSS asigna cada valor a un tamaño.

<script>
  import TextSizePicker from "@lilydesignsystem/svelte-text-size-picker";
</script>

<TextSizePicker
  label="Text size"
  sizes={["small", "medium", "large", "x-large"]}
  storageKey="my-app-text-size"
/>
:root[data-text-size="small"]   { font-size: 87.5%; }
:root[data-text-size="medium"]  { font-size: 100%; }
:root[data-text-size="large"]   { font-size: 112.5%; }
:root[data-text-size="x-large"] { font-size: 125%; }

Dimensiona tu diseño en rem y toda la aplicación se escala con la elección del usuario. Las etiquetas se forman a partir de los slugs con mayúscula inicial («X Large») o se anulan con sizeLabels.

Comprueba tu trabajo: elige «Large» en el selector y el texto de la página crece. Recarga: sigue grande, porque la elección se persistió en localStorage. Cambia tu selector de idioma a árabe y dir="rtl" se establece en el documento automáticamente.

share-picker: envía esta página

Un botón de un solo glifo que abre la hoja de compartir nativa del dispositivo donde existe y, si no, recurre a una lista de destinos que tú proporcionas, además de una acción para copiar la URL. No incluye URL de redes sociales: qué redes pertenecen a tu producto es decisión tuya, no del sistema de diseño.

<script>
  import SharePicker from "@lilydesignsystem/svelte-share-picker";

  const targets = [
    { id: "mastodon", label: "Mastodon",
      href: (url, title) => `https://mastodon.social/share?text=${encodeURIComponent(title)}%20${encodeURIComponent(url)}` },
    { id: "email", label: "Email",
      href: (url, title) => `mailto:?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(url)}`, newTab: false },
  ];
</script>

<SharePicker
  label="Share this page"
  title="An article worth reading"
  {targets}
  copyLabel="Copy link"
  copiedLabel="Link copied"
/>

url tiene como valor predeterminado location.hrefasí que el caso común no necesita cableado. Fuerza una ruta con strategy="list" o strategy="native" cuando necesites un comportamiento coherente entre plataformas para pruebas o capturas de pantalla.

date-time-picker: una fecha, una hora o ambas

Un campo de texto en el que puedes escribir, más un botón de icono que abre un diálogo selector de fecha WAI-ARIA APG. Correcto para la configuración regional por construcción: los nombres de los meses, los nombres de los días, el primer día de la semana y el reloj de 12 o 24 horas provienen todos de Intl y el valor es siempre una cadena ISO ordenable y sin zona horaria: "2026-03-15" para mode="date", "09:30" para mode="time", "2026-03-15T09:30" para mode="datetime".

<script>
  import DateTimePicker from "@lilydesignsystem/svelte-date-time-picker";

  let appointment = $state("");
</script>

<DateTimePicker
  inputId="appointment"
  name="appointment"
  label="Choose an appointment date"
  locale="en-GB"
  bind:value={appointment}
  labels={{
    previousYear: "Previous year",
    previousMonth: "Previous month",
    nextMonth: "Next month",
    nextYear: "Next year",
    confirm: "OK",
    cancel: "Cancel",
  }}
/>

El valor ISO es idéntico a lo que envía <input type="date"> así que puedes cambiar el control nativo por este o viceversa sin tocar tu backend.

motion-picker: respeta el movimiento reducido

El asistente establece data-motion="{slug}" en la raíz del documento. Su primer valor proviene de la configuración prefers-reduced-motion del sistema operativo del visitante: siempre, sin una marca de adhesión voluntaria, porque el movimiento es una necesidad real de accesibilidad (WCAG 2.3.3). Tu CSS y tus scripts deciden qué se suprime.

<script>
  import MotionPicker from "@lilydesignsystem/svelte-motion-picker";
</script>

<MotionPicker
  label="Motion"
  motions={["no-preference", "reduce"]}
  storageKey="my-app-motion"
/>
:root[data-motion="reduce"] *,
:root[data-motion="reduce"] *::before,
:root[data-motion="reduce"] *::after {
  animation: none !important;
  transition: none !important;
  scroll-behavior: auto !important;
}

search-picker: un cuadro de búsqueda que no estorba

Un botón de icono que abre un campo de búsqueda. Pulsar Intro, o el botón ⏎ junto al campo, navega con una simple solicitud GET a /?<text> (el texto se recorta y se codifica como URI; una consulta vacía no lleva a ninguna parte). Sus tres nombres accesibles son obligatorios, porque el asistente no incluye inglés.

<script>
  import SearchPicker from "@lilydesignsystem/svelte-search-picker";
  import { goto } from "$app/navigation";
</script>

<SearchPicker
  label="Search this site"
  inputLabel="Search terms"
  submitLabel="Search"
  placeholder="Search…"
  action="/search"
  navigate={(href) => goto(href)}
/>

action cambia la ruta (/search?foo), y navigate mantiene la navegación dentro de la aplicación; sin él, el asistente llama a location.assign.

picker-bar: toda la fila del encabezado de una vez

picker-bar renderiza búsqueda, tema, configuración regional, tamaño de texto y compartir en una fila, con el conjunto completo de temas de referencia y la escala de texto de siete pasos ya cableados. Cada nombre accesible llega en un único objeto labels obligatorio.

<script>
  import PickerBar from "@lilydesignsystem/svelte-picker-bar";
</script>

<PickerBar
  labels={{
    search: "Search", searchInput: "Search terms", searchSubmit: "Search",
    theme: "Theme", locale: "Language", textSize: "Text size", share: "Share",
  }}
  themesUrl="/themes/"
  locales={["en", "fr", "es"]}
  shareTargets={targets}
/>

Alcanza cualquier selector envuelto con searchProps, themeProps, localeProps, textSizeProps y shareProps.

Las descripciones emergentes vienen integradas

Cada selector muestra su etiqueta en una descripción emergente cuando el puntero reposa sobre su botón o el botón tiene foco de teclado. La descripción permanece mientras el puntero se mueve hacia ella, Escape la cierra esté donde esté el foco, y nunca se muestra mientras la ventana emergente del selector está abierta. Lleva el mismo texto que el nombre accesible del botón, así que no hay nada que configurar ni traducir dos veces; los temas de referencia le dan estilo.

Un panel de ajustes en un solo lugar

Los cuatro asistentes de preferencias se componen de forma natural:

<fieldset class="fieldset">
  <legend>Preferences</legend>
  <ThemePicker    label="Theme"     themesUrl="/themes/" themes={themes} storageKey="pref-theme" />
  <LocalePicker   label="Language"  locales={locales}    storageKey="pref-locale" />
  <TextSizePicker label="Text size" sizes={sizes}        storageKey="pref-text-size" />
  <MotionPicker   label="Motion"    motions={motions}    storageKey="pref-motion" />
</fieldset>

Verificado: los fragmentos de locale-picker, text-size-picker, share-picker, date-time-picker y del panel combinado se compilaron individualmente contra los paquetes 0.1.1 publicados, mediante svelte/server (2026-08-29); los fragmentos de motion-picker, search-picker y picker-bar se renderizaron contra los componentes de los propios paquetes el 2026-10-06.

Dónde conseguirlos

# one directory per package in the monorepo, e.g.
git clone https://github.com/LilyDesignSystem/lily-design-system
cd lily-design-system/lily-design-system-svelte-theme-picker
# or lily-design-system-react-theme-picker, -vue- / -angular- / -html- / -web-components- / -nunjucks- / -blazor-

Cada paquete tiene su propia spec, pruebas asignadas a las cláusulas de la spec, ejemplos ejecutables (incluidos patrones de cookies para SSR) y una canalización de compilación y publicación. Los paquetes de Svelte son los canónicos; en caso de duda, prevalecen sus contratos.

Próximos pasos

¿Dudas por el camino? La página de ayuda está llena de respuestas, y si algún paso puede ser más claro, cuéntanoslo y con gusto mejoraremos el tutorial.