Ir al contenido

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.

ddmmaaaa

Escribe día, mes y año, o elige en el calendario.

Svelte
<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.

ddmmaaaa

De hoy a 60 días, de lunes a viernes.

Svelte
<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.

11032026

Month, day and year; the week starts on Sunday.

Svelte
<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.

ddmmaaaa
ddmmaaaa
ddmmaaaa
Svelte
<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.

ddmmaaaa

Elige la fecha de vencimiento de la factura.

14032024

Se fija al crear la cuenta.

Svelte
<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.

Los campos con * son obligatorios.

ddmmaaaa

A partir de mañana.

Svelte
<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

Props de DatePicker
PropDescripción
bind:value? DateValue La fecha elegida, un DateValue de @internationalized/date, que se crea con las utilidades de @archeblack/ui/date: 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 falseSi 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 1Dí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 falseObligatorio. Dentro de un Field, lo decide su required.
disabled? boolean Por defecto falseDeshabilitado. Dentro de un Field, lo decide su disabled.
readonly? boolean Por defecto falseSe 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 falseMarca 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 son spinbutton que 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 con aria-labelledby en el grupo y en cada segmento, y un clic en ella enfoca el primer segmento. Suelto, también sirven aria-label o aria-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-live anuncia 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-disabled y 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-invalid en los segmentos y la línea de peligro). El mensaje lo da el error del 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/09/2026') en value. Un DateValue creado 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 min del segundo con la fecha del primero, y nombra cada campo («Desde», «Hasta»).