Componentes · Ola 3 · Formularios
Date picker
Escribe una fecha por partes o elígela en un calendario. Tiene el aspecto de línea de Input; el calendario se abre en una superficie como
la de Popover, con la semana desde el lunes y en español por defecto. El valor es un DateValue de @internationalized/date.
@archeblack/ui/date reexporta esa biblioteca entera: los ejemplos
crean y comparan fechas importando de ahí, sin sumar otro paquete a las dependencias del producto.
Es la misma que usan DatePicker y Bits UI, así que los valores son compatibles.
import { DatePicker } from '@archeblack/ui';
import { CalendarDate, parseDate } from '@archeblack/ui/date';
Ejemplos
Dentro de un Field
Los segmentos se escriben o se cambian con las flechas; el botón abre el calendario. El valor, con bind:value.
Escribe día, mes y año, o elige en el calendario.
<script lang="ts">
import type { DateValue } from '@archeblack/ui/date';
import { DatePicker, Field } from '@archeblack/ui';
let delivery = $state<DateValue | undefined>();
</script>
<div style:width="min(100%, 20rem)">
<Field label="Fecha de entrega" hint="Escribe día, mes y año, o elige en el calendario.">
<DatePicker bind:value={delivery} />
</Field>
</div> Rango y días no disponibles
min y max limitan las fechas; isDateUnavailable tacha los fines de semana.
De hoy a 60 días, de lunes a viernes.
<script lang="ts">
import { getLocalTimeZone, isWeekend, today, type DateValue } from '@archeblack/ui/date';
import { DatePicker, Field } from '@archeblack/ui';
const start = today(getLocalTimeZone());
const end = start.add({ days: 60 });
let booking = $state<DateValue | undefined>();
</script>
<div style:width="min(100%, 20rem)">
<Field label="Día del turno" hint="De hoy a 60 días, de lunes a viernes.">
<DatePicker
bind:value={booking}
min={start}
max={end}
isDateUnavailable={(date) => isWeekend(date, 'es')}
/>
</Field>
</div> Otro idioma
locale cambia el orden de los segmentos, los meses y los días; weekStartsOn el primer día de la semana. Los textos de los botones también se traducen.
Month, day and year; the week starts on Sunday.
<script lang="ts">
import { CalendarDate, type DateValue } from '@archeblack/ui/date';
import { DatePicker, Field } from '@archeblack/ui';
let launch = $state<DateValue | undefined>(new CalendarDate(2026, 11, 3));
</script>
<div style:width="min(100%, 20rem)">
<Field label="Launch date" hint="Month, day and year; the week starts on Sunday.">
<DatePicker
bind:value={launch}
locale="en-US"
weekStartsOn={0}
triggerLabel="Choose date"
previousLabel="Previous month"
nextLabel="Next month"
emptyLabel="empty"
/>
</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 { DatePicker } from '@archeblack/ui';
</script>
<div style:display="grid" style:gap="var(--arche-spacing-6)" style:width="min(100%, 20rem)">
<DatePicker size="sm" aria-label="Fecha, talla sm" />
<DatePicker aria-label="Fecha, talla md" />
<DatePicker size="lg" aria-label="Fecha, talla lg" />
</div> Estados
Inválido, con el mensaje del Field, y deshabilitado.
Elige la fecha de vencimiento de la factura.
Se fija al crear la cuenta.
<script lang="ts">
import { CalendarDate, type DateValue } from '@archeblack/ui/date';
import { DatePicker, Field } from '@archeblack/ui';
let due = $state<DateValue | undefined>();
</script>
<div style:display="grid" style:gap="var(--arche-spacing-6)" style:width="min(100%, 20rem)">
<Field label="Vencimiento" error="Elige la fecha de vencimiento de la factura.">
<DatePicker bind:value={due} />
</Field>
<Field label="Alta de la cuenta" hint="Se fija al crear la cuenta." disabled>
<DatePicker value={new CalendarDate(2024, 3, 14)} />
</Field>
</div> En un formulario
Obligatorio y desde mañana, con el error al enviar y el foco en el primer campo inválido. Con name, el formulario envía la fecha en ISO 8601.
<script lang="ts">
import { tick } from 'svelte';
import { DateFormatter, getLocalTimeZone, today, type DateValue } from '@archeblack/ui/date';
import { Button, DatePicker, Field, Input } from '@archeblack/ui';
const zone = getLocalTimeZone();
const firstDay = today(zone).add({ days: 1 });
const longDate = new DateFormatter('es', { dateStyle: 'long' });
let title = $state('');
let date = $state<DateValue | undefined>();
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({
title: title.trim() ? undefined : 'Escribe un título para la reunión.',
date: !date
? 'Elige una fecha.'
: date.compare(firstDay) < 0
? 'Elige una fecha a partir de mañana.'
: undefined
});
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. En el campo de
// fecha, el primer segmento.
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="Título" required error={errorOf('title')}>
<Input bind:value={title} autocomplete="off" />
</Field>
<Field label="Fecha de la reunión" hint="A partir de mañana." required error={errorOf('date')}>
<DatePicker bind:value={date} min={firstDay} name="date" />
</Field>
<div style:display="flex" style:align-items="center" style:gap="var(--arche-spacing-4)">
<Button type="submit" variant="primary">Agendar</Button>
<span role="status" style:color="var(--arche-color-text-muted)">
{saved && date ? `Reunión agendada para el ${longDate.format(date.toDate(zone))}.` : ''}
</span>
</div>
</form> Props
| Prop | Descripción |
|---|---|
bind:value? DateValue | La fecha elegida, un DateValue de @internationalized/, que se crea con las utilidades de @archeblack/: new CalendarDate(2026, 9, 28), today(getLocalTimeZone()) o parseDate('2026-09-28'). Sin valor, los segmentos se ven vacíos con su formato («dd/mm/aaaa»). |
bind:open? boolean Por defecto false | Si el calendario está abierto. |
onValueChange? (value: DateValue | undefined) => void | Se llama con la fecha nueva, después de actualizar value y solo si el cambio se aceptó (un bind:value de función puede rechazarlo). Al borrar un segmento, la fecha pasa a undefined. |
onOpenChange? (open: boolean) => void | Se llama al abrir o cerrar el calendario, después de actualizar open y solo si el cambio se aceptó. |
min? DateValue | Primera fecha que se puede elegir. En el calendario, las anteriores se ven en text-disabled y no se enfocan; escrita en el campo, una anterior lo marca como inválido. |
max? DateValue | Última fecha que se puede elegir, con el mismo comportamiento que min. |
isDateUnavailable? (date: DateValue) => boolean | Fechas que no se pueden elegir (fines de semana, feriados). Se ven tachadas en text-disabled, se pueden recorrer con el teclado pero no elegir, y escritas en el campo lo marcan como inválido. |
locale? string Por defecto 'es' | Idioma (BCP 47) del orden y el formato de los segmentos, del nombre de los meses y de los días. |
weekStartsOn? 0 | 1 | 2 | 3 | 4 | 5 | 6 Por defecto 1 | Día en que empieza la semana del calendario: 0 es domingo, 1 lunes. |
name? string | Nombre del campo en el formulario. Se envía la fecha en ISO 8601 (2026-09-28), en un input oculto. |
required? boolean Por defecto false | Obligatorio. Dentro de un Field, lo decide su required. |
disabled? boolean Por defecto false | Deshabilitado. Dentro de un Field, lo decide su disabled. |
readonly? boolean Por defecto false | Se ve y se recorre con el teclado, pero no se puede cambiar. |
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. Dentro de un Field lo decide su error; una fecha fuera de rango o no disponible también lo marca. |
triggerLabel? string Por defecto 'Elegir fecha' | Nombre accesible del botón del calendario y del diálogo que abre. |
previousLabel? string Por defecto 'Mes anterior' | Nombre accesible del botón que va al mes anterior. |
nextLabel? string Por defecto 'Mes siguiente' | Nombre accesible del botón que va al mes siguiente. |
emptyLabel? string Por defecto 'vacío' | Lo que se anuncia de un segmento sin valor. Los nombres de los segmentos («día», «mes», «año») salen del locale. |
bind:ref? HTMLDivElement | null | El grupo de segmentos (role="group"). |
class? ClassValue | Clases del producto; se suman a arche-date-picker, el contenedor que dibuja la línea. |
...rest HTMLAttributes<HTMLDivElement> | Cualquier otro atributo va al grupo de segmentos: id, aria-label, aria-labelledby, aria-describedby, data-*… |
Accesibilidad
- El campo es un grupo (
role="group") de segmentos: día, mes y año sonspinbuttonque se escriben con números o se cambian con las flechas arriba y abajo; las flechas a los lados pasan de un segmento a otro y Retroceso borra. Cada segmento se nombra con su parte y la etiqueta del campo («día, Fecha de entrega»). - Un
<label for>no nombra a un<div>: el componente busca la etiqueta que apunta a su id (la de Field o una propia), la usa conaria-labelledbyen el grupo y en cada segmento, y un clic en ella enfoca el primer segmento. Suelto, también sirvenaria-labeloaria-labelledby. - El botón del calendario (
triggerLabel) abre un diálogo modal (role="dialog",aria-modal), como el patrón Date Picker Dialog de APG. El foco va al día elegido o a hoy sin desplazar la página; al cerrar, vuelve al botón. - El calendario y su grilla se nombran con el mes («septiembre de 2026»). Al cambiar de mes con los botones o el teclado, una región
aria-liveanuncia el mes nuevo. La grilla muestra solo las semanas del mes: cuatro, cinco o seis filas. - En el calendario: flechas para moverse por días y semanas (cruzan de mes), Inicio y Fin para el primer y el último día de la semana, Re Pág y Av Pág para el mes anterior y el siguiente, Mayúsculas con Re Pág o Av Pág para el año, Enter o espacio para elegir y Escape para cerrar sin elegir. Tab recorre los botones de mes y el día enfocado.
- Cada día se anuncia con la fecha completa («lunes, 28 de septiembre de 2026»); el día elegido lleva
aria-selected. Los encabezados muestran la inicial y dan el nombre completo a los lectores. - El día elegido usa el fondo de selección; hoy lleva un aro. Los días fuera de rango se ven en
text-disabledy no se enfocan; los no disponibles además se ven tachados, así la señal no depende solo del color. - Una fecha fuera de rango o no disponible, escrita en el campo, lo marca como inválido (
aria-invaliden los segmentos y la línea de peligro). El mensaje lo da elerrordel Field. - La línea llega a 3:1 contra toda capa. El foco en un segmento la enciende y el segmento toma el fondo de selección. En colores forzados la línea se engrosa y el segmento enfocado y el día elegido toman los colores de selección del sistema.
Qué evitar
- Un Date picker para una fecha lejana y conocida, como la de nacimiento. El mismo campo, escribiendo los segmentos: nadie recorre décadas de calendario.
- Fechas como texto (
'28/) en09/ 2026' value. UnDateValuecreado con@archeblack/(ui/ date parseDate('2026-09-28')): no depende del formato del idioma ni de la zona horaria. - Marcar días no disponibles sin explicar por qué. Dilo en la ayuda del Field («de lunes a viernes»), además de tacharlos.
- Dos Date picker para un rango sin relación entre ellos. Usa el
mindel segundo con la fecha del primero, y nombra cada campo («Desde», «Hasta»).