component — date & time
Date Field
Accessible segmented date entry built on react-aria-components, styled with the quebi design system. Each date part is individually editable; the field is boxed and the focused segment fills with the signal colour. Segment padding follows the locale — pass shouldForceLeadingZeros for two-digit day and month (30.06.2026 rather than de-DE's 30.6.2026).
- form
- input
- date
- datetime
- interactive
- leading-zeros
06 examples
Default
A labelled date field with a muted hint.
With value
Pre-filled via @internationalized/date.
Two digits
shouldForceLeadingZeros pads the day and month segments to two digits (30.06.2026 rather than the 30.6.2026 de-DE asks for). The year is unaffected.
Invalid
Validation state surfaces a red border and error message.
Disabled
Controlled
source
date-field.tsxCopy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.
"use client"
import type { DateFieldProps, DateInputProps, DateValue } from "react-aria-components"
import {
composeRenderProps,
DateField as DateFieldPrimitive,
DateInput as DateInputPrimitive,
DateSegment,
} from "react-aria-components"
import { useFieldSizing } from "@/lib/field-size"
import { cn } from "@/lib/utils"
import { fieldStyles } from "@/components/field"
/**
* DateField — quebi design system
*
* Built on react-aria-components. A segmented date entry control: each part
* (day / month / year) is an individually editable segment. The wrapper is
* drawn like `Input` — boxed, its edge turning the focus colour on focus; the
* focused segment takes the `signal` fill, the library's mark for "this one".
*
* Segment padding follows the locale, which is why `de-DE` renders `30.6.2026`
* and not `30.06.2026`. Pass `shouldForceLeadingZeros` for the padded form —
* it applies to the day, month and hour segments; the year is never padded.
* Left unset the locale decides, which is the default on purpose: the same
* prop exists on `DatePicker`, `DateRangePicker`, `TimeField` and
* `ConformDateField`, so padding is a per-field decision rather than one the
* library takes on everyone's behalf.
*/
export function DateField<T extends DateValue>({ className, ...props }: DateFieldProps<T>) {
return (
<DateFieldPrimitive
{...props}
data-slot="control"
className={composeRenderProps(className, (resolved) =>
cn("group", fieldStyles, "w-fit", resolved),
)}
/>
)
}
/**
* The padding half of the field size scale — the same three steps `Input`
* publishes, so a date field and the button beside it are the same height:
* `xs` is 30px and `sm` 38px, `md` is the default. No horizontal padding,
* as in `Input`. Spelled out
* here rather than imported so `DateField` does not gain `Input` as a registry
* dependency for three strings; the type size travels with it below, because a
* segmented field's height is its segments' line box.
*/
const dateInputSizeStyles = {
xs: "px-(--q-field-px) py-1.5",
sm: "px-(--q-field-px) py-2",
md: "px-(--q-field-px) py-2.5",
} as const
type DateInputSize = keyof typeof dateInputSizeStyles
/** `bare` strips the input chrome (box, focus treatment)
* so the DateInput can be composed inside a wrapper that owns those — e.g.
* a DatePickerTrigger which adds a calendar-icon button on the right. */
interface DateInputComponentProps extends Omit<DateInputProps, "children"> {
bare?: boolean
/**
* Control height. Matches `Input`'s scale. Left out, it is whatever the
* surrounding surface asked for — a table cell being the one that does. See
* `@/lib/field-size`.
*/
size?: DateInputSize
}
export function DateInput({
className,
bare = false,
size: sizeProp,
...props
}: DateInputComponentProps) {
const { size } = useFieldSizing({ size: sizeProp })
const textSize = size === "xs" ? "text-xs" : "text-sm"
return (
<span data-slot="control" className={bare ? "relative block w-full" : "relative block"}>
<DateInputPrimitive
className={composeRenderProps(className, (resolved) =>
cn(
"relative block appearance-none text-quebi-fg",
textSize,
bare
? ["w-full border-0 bg-transparent outline-none", dateInputSizeStyles[size]]
: [
// `Input`'s box: focus or an open picker turns the edge the focus
// colour and adds a 1px shadow inside it, no ring.
"quebi-field",
dateInputSizeStyles[size],
"transition-[border-color,box-shadow] duration-150",
"outline-none focus-within:outline-none focus-within:shadow-(--q-field-focus)",
"group-open:shadow-(--q-field-focus)",
"data-invalid:[--q-field-edge:var(--q-danger)] focus-within:data-invalid:shadow-(--q-field-focus-danger)",
"in-disabled:cursor-not-allowed in-disabled:opacity-50",
],
resolved,
),
)}
{...props}
>
{(segment) => (
<DateSegment
segment={segment}
className={cn(
"inline shrink-0 rounded-(--q-radius-mark) px-1 py-0.5 text-quebi-fg tracking-wider caret-transparent outline-0 type-literal:px-0",
textSize,
"data-placeholder:text-quebi-fg-subtle data-[type=literal]:text-quebi-fg-muted",
"data-focused:bg-quebi-signal data-focused:text-quebi-on-signal",
"data-invalid:text-quebi-danger data-focused:data-invalid:bg-quebi-danger data-focused:data-invalid:text-quebi-on-action",
"forced-colors:data-focused:bg-[Highlight] forced-colors:data-focused:text-[HighlightText]",
"forced-color-adjust-none forced-colors:text-[ButtonText]",
"in-disabled:opacity-50 disabled:opacity-50 forced-colors:disabled:text-[GrayText]",
)}
/>
)}
</DateInputPrimitive>
</span>
)
}