Componentes · Ola 3 · Formularios
Combobox
Elige una opción de una lista larga escribiendo para filtrarla. Tiene el aspecto de línea de Input; la lista es la superficie de Menu. Dentro de
un Field toma de él su etiqueta, su descripción y sus estados.
import { Combobox } from '@archeblack/ui';
Ejemplos
Dentro de un Field
Escribir filtra la lista sin distinguir mayúsculas ni tildes. El valor es el de la opción elegida, con bind:value.
Escribe «peru» o «espana»: no importan las tildes.
<script lang="ts">
import { Combobox, Field } from '@archeblack/ui';
const countries = [
{ value: 'ar', label: 'Argentina' },
{ value: 'bo', label: 'Bolivia' },
{ value: 'br', label: 'Brasil' },
{ value: 'cl', label: 'Chile' },
{ value: 'co', label: 'Colombia' },
{ value: 'es', label: 'España' },
{ value: 'mx', label: 'México' },
{ value: 'pe', label: 'Perú' },
{ value: 'uy', label: 'Uruguay' }
];
let country = $state('');
</script>
<div style:width="min(100%, 20rem)">
<Field label="País" hint="Escribe «peru» o «espana»: no importan las tildes.">
<Combobox items={countries} bind:value={country} placeholder="Busca un país" />
</Field>
</div> Grupos y botón para quitar
Grupos con etiqueta, una opción deshabilitada y clearable, que muestra una x mientras hay una elección.
París no está disponible en tu plan.
<script lang="ts">
import { Combobox, Field, type ComboboxItem } from '@archeblack/ui';
const zones: ComboboxItem[] = [
{
label: 'América',
options: [
{ value: 'America/Argentina/Buenos_Aires', label: 'Buenos Aires' },
{ value: 'America/Bogota', label: 'Bogotá' },
{ value: 'America/Mexico_City', label: 'Ciudad de México' },
{ value: 'America/Santiago', label: 'Santiago' }
]
},
{
label: 'Europa',
options: [
{ value: 'Europe/Lisbon', label: 'Lisboa' },
{ value: 'Europe/Madrid', label: 'Madrid' },
{ value: 'Europe/Paris', label: 'París', disabled: true }
]
}
];
let zone = $state('America/Argentina/Buenos_Aires');
</script>
<div style:width="min(100%, 20rem)">
<Field label="Zona horaria" hint="París no está disponible en tu plan.">
<Combobox items={zones} bind:value={zone} clearable placeholder="Busca una ciudad" />
</Field>
</div> Filtro propio
filter decide qué opciones coinciden; emptyText cambia el mensaje cuando no queda ninguna.
Escribe el código (usd) o el comienzo de una palabra (peso).
<script lang="ts">
import { Combobox, Field, type ComboboxFilter } from '@archeblack/ui';
const currencies = [
{ value: 'ARS', label: 'Peso argentino' },
{ value: 'BRL', label: 'Real brasileño' },
{ value: 'CLP', label: 'Peso chileno' },
{ value: 'EUR', label: 'Euro' },
{ value: 'MXN', label: 'Peso mexicano' },
{ value: 'USD', label: 'Dólar estadounidense' }
];
// Busca por el código (ARS) o por el comienzo de cualquier palabra del nombre.
const byCodeOrWord: ComboboxFilter = (option, query) => {
const text = query.trim().toLowerCase();
return (
option.value.toLowerCase().startsWith(text) ||
option.label
.toLowerCase()
.split(' ')
.some((word) => word.startsWith(text))
);
};
let currency = $state('');
</script>
<div style:width="min(100%, 20rem)">
<Field label="Moneda" hint="Escribe el código (usd) o el comienzo de una palabra (peso).">
<Combobox
items={currencies}
bind:value={currency}
filter={byCodeOrWord}
emptyText="Ninguna moneda coincide"
placeholder="Busca una moneda"
/>
</Field>
</div> Tallas
sm, md y lg, por alto, como Input y Select. Sin Field, el nombre va en aria-label.
<script lang="ts">
import { Combobox } from '@archeblack/ui';
const plans = [
{ value: 'free', label: 'Gratis' },
{ value: 'team', label: 'Equipo' },
{ value: 'business', label: 'Empresa' }
];
</script>
<div style:display="grid" style:gap="var(--arche-spacing-6)" style:width="min(100%, 20rem)">
<Combobox items={plans} size="sm" placeholder="Talla sm, 28 px" aria-label="Plan, talla sm" />
<Combobox items={plans} placeholder="Talla md, 36 px" aria-label="Plan, talla md" />
<Combobox items={plans} size="lg" placeholder="Talla lg, 44 px" aria-label="Plan, talla lg" />
</div> Estados
Inválido, con el mensaje del Field, y deshabilitado.
Elige quién responde por el proyecto.
Se fija al crear la cuenta.
<script lang="ts">
import { Combobox, Field } from '@archeblack/ui';
const owners = [
{ value: 'ana', label: 'Ana Duarte' },
{ value: 'bruno', label: 'Bruno Silva' },
{ value: 'carla', label: 'Carla Ortiz' }
];
let owner = $state('');
</script>
<div style:display="grid" style:gap="var(--arche-spacing-6)" style:width="min(100%, 20rem)">
<Field label="Responsable" error="Elige quién responde por el proyecto.">
<Combobox items={owners} bind:value={owner} placeholder="Busca una persona" />
</Field>
<Field label="Organización" hint="Se fija al crear la cuenta." disabled>
<Combobox items={[{ value: 'arche', label: 'Arche' }]} value="arche" />
</Field>
</div> En un formulario
Obligatorio, con el error al enviar y el foco en el primer campo inválido. Con name, el formulario envía el valor de la opción, no el texto.
<script lang="ts">
import { tick } from 'svelte';
import { Button, Combobox, Field, Input } from '@archeblack/ui';
const cities = [
{
label: 'Argentina',
options: [
{ value: 'bue', label: 'Buenos Aires' },
{ value: 'cor', label: 'Córdoba' },
{ value: 'mdz', label: 'Mendoza' },
{ value: 'ros', label: 'Rosario' }
]
},
{
label: 'Uruguay',
options: [
{ value: 'mvd', label: 'Montevideo' },
{ value: 'pde', label: 'Punta del Este' }
]
}
];
let name = $state('');
let city = $state('');
let submitted = $state(false);
let saved = $state(false);
// Los errores aparecen después del primer envío, no mientras la persona completa el formulario.
const errors = $derived({
name: name.trim() ? undefined : 'Escribe el nombre de la sede.',
city: city ? undefined : 'Elige una ciudad de la lista.'
});
const errorOf = (field: keyof typeof errors) => (submitted ? errors[field] : undefined);
async function submit(event: SubmitEvent) {
event.preventDefault();
const form = event.currentTarget as HTMLFormElement;
submitted = true;
saved = !Object.values(errors).some(Boolean);
if (!saved) {
// Espera a que los campos muestren su error y lleva el foco al primero.
await tick();
form.querySelector<HTMLElement>('[aria-invalid="true"]')?.focus();
}
}
</script>
<form
novalidate
onsubmit={submit}
style:display="grid"
style:gap="var(--arche-spacing-6)"
style:width="min(100%, 24rem)"
>
<p style:margin="0" style:color="var(--arche-color-text-muted)">
Los campos con * son obligatorios.
</p>
<Field label="Nombre de la sede" required error={errorOf('name')}>
<Input bind:value={name} autocomplete="off" />
</Field>
<Field label="Ciudad" required error={errorOf('city')}>
<Combobox items={cities} bind:value={city} name="city" placeholder="Busca una ciudad" />
</Field>
<div style:display="flex" style:align-items="center" style:gap="var(--arche-spacing-4)">
<Button type="submit" variant="primary">Crear sede</Button>
<span role="status" style:color="var(--arche-color-text-muted)">
{saved ? 'Sede lista para crear.' : ''}
</span>
</div>
</form> Props
| Prop | Descripción |
|---|---|
items ComboboxItem[] | Las opciones: { value, label, disabled? } sueltas o en grupos { label, options }. Cada value es único en toda la lista; label es lo que se ve, lo que filtra la búsqueda y lo que queda escrito al elegir. |
bind:value? string | Valor de la opción elegida, o '' si no hay ninguna. Borrar todo el texto del campo quita la elección. |
bind:open? boolean Por defecto false | Si la lista está abierta. |
onValueChange? (value: string) => void | Se llama con el valor nuevo, después de actualizar value y solo si el cambio se aceptó (un bind:value de función puede rechazarlo). |
onOpenChange? (open: boolean) => void | Se llama al abrir o cerrar la lista, después de actualizar open y solo si el cambio se aceptó. |
filter? (option: ComboboxOption, query: string) => boolean | Filtro propio. Recibe cada opción y el texto tal como se escribió (nunca vacío). Por defecto, la etiqueta contiene el texto sin distinguir mayúsculas ni tildes. |
emptyText? string Por defecto 'Sin resultados' | Mensaje de la lista cuando ninguna opción coincide. También se anuncia. |
clearable? boolean Por defecto false | Muestra un botón con una x para quitar la elección mientras hay una. |
clearLabel? string Por defecto 'Quitar la elección' | Nombre accesible del botón para quitar la elección. |
toggleLabel? string Por defecto 'Mostrar opciones' | Nombre accesible del chevron, que abre y cierra la lista. |
size? 'sm' | 'md' | 'lg' Por defecto 'md' | Talla por alto: sm 28 px (texto de 13 px), md 36 px y lg 44 px, como Input y Select. |
invalid? boolean Por defecto false | Marca el valor como inválido (aria-invalid). Dentro de un Field lo decide su error. |
name? string | Nombre del campo en el formulario. Se envía el value de la opción elegida (en un input oculto), no el texto. |
bind:ref? HTMLInputElement | null | El <input>, para enfocarlo desde el código. |
class? ClassValue | Clases del producto; se suman a arche-combobox, el contenedor que dibuja la línea. |
...rest HTMLInputAttributes | Cualquier otro atributo va al <input>: placeholder, required, disabled, aria-label, oninput, onkeydown… Sin type, role, list ni autocomplete: los decide el componente. |
Accesibilidad
- Sigue el patrón combobox con lista y autocompletado de APG: el foco se queda en el campo y la opción activa se señala con
aria-activedescendant. El campo tienerole="combobox",aria-autocomplete="list",aria-expandedy, abierto,aria-controlshacia la lista. - Teclado: escribir filtra y abre la lista; la flecha abajo (o Alt + flecha abajo) la abre y recorre las opciones; flecha arriba, Inicio, Fin, Re Pág y Av Pág también; Enter elige la opción resaltada; Escape cierra la lista y, cerrada, borra el campo y la elección; Tab cierra la lista y sigue.
- La opción resaltada con el teclado lleva un aro de foco hacia adentro, además del fondo; con el puntero, solo el fondo. La elegida lleva un check y
aria-selected="true"; las deshabilitadas se ven y se anuncian (aria-disabled="true"), pero no se eligen. - El chevron y el botón de quitar son botones con nombre (
toggleLabel,clearLabel) fuera del orden de tabulación, como en APG: el campo es una sola parada de Tab y con el teclado se hace lo mismo con las flechas y Escape. - Necesita un nombre: dentro de un Field lo da la etiqueta; suelto,
aria-label,aria-labelledbyo un<label for>propio. La lista toma el mismo nombre. En desarrollo, un Combobox suelto sin nombre avisa por consola. - Cuando ninguna opción coincide,
emptyTextse ve en la lista y se anuncia por una región de estado (role="status"). - Los grupos son
role="group"con su etiqueta como nombre. - La línea llega a 3:1 contra toda capa. El foco y la lista abierta la encienden; inválida y con foco, la línea toma el color de foco y el halo es de peligro. En colores forzados la línea se engrosa y la opción resaltada toma los colores de selección del sistema.
Qué evitar
- Un Combobox con cinco o seis opciones. Un Select o un grupo de Radio: se ven sin escribir.
- Usarlo como buscador libre, donde el texto escrito es el valor. Un Input
type="search". El Combobox solo acepta opciones de la lista. - Cambiar la página o enviar el formulario al elegir una opción. Aplica la elección con un botón: con el teclado se recorren las opciones una a una.
- Etiquetas repetidas o que solo se distinguen por el valor. Etiquetas únicas y completas («Córdoba, Argentina» y «Córdoba, España»): es lo que se lee y lo que queda escrito.