Input Date
Overview
The Input Date is a segmented date field: a single visual surface that holds individually focusable parts — day, month, year (and optionally hour, minute) — separated by a muted glyph such as / or –. It is composed of two recipe parts:
useInputDateRecipe()— the wrapper that owns the visual field: border, background, padding, and the:focus-withinring. It is a member of the field recipes family, so it shares the same color, style, and state surface as the Input recipe.useInputDateSegmentRecipe()— the single editable segment that sits inside the field. It is transparent, inherits typography and color from the wrapper, centers its digits with tabular figures, and paints a highlight only on:focus.
The wrapper owns the entire surface and reveals a primary-colored ring on :focus-within, so focusing any segment lights the whole field — just like a native text input. Each segment is the focusable element itself, so keyboard focus moves part to part while the surface stays put. A plain .input-date-separator glyph sits between segments, muted via @color.text-weak.
The Input Date recipes integrate directly with the default design tokens preset and generate type-safe utility classes at build time with zero runtime CSS.
Why use the Input Date recipe?
The Input Date recipe helps you:
- Ship faster with sensible defaults: Get 3 surface colors, 3 styles, and 3 sizes out of the box with a single set of composable calls.
- Match the rest of your forms: The wrapper reuses the Input recipe's color / style / state surface, so a date field shares the same visual language as your text inputs.
- Maintain consistency: The focus ring, invalid border, and dark-mode surfaces follow the same design rules everywhere.
- Customize without forking: Override base styles, default variants, or filter out options you don't need — all through the options API.
- Stay type-safe: Full TypeScript support means your editor catches invalid color, style, or size values at compile time.
- Integrate with your tokens: Every value references the design tokens preset, so theme changes propagate automatically.
Usage
Register the recipes
Add the Input Date recipes to a local Styleframe instance. The global styleframe.config.ts provides design tokens and utilities, while the component-level file registers the recipes themselves:
import { styleframe } from 'virtual:styleframe';
import { useInputDateRecipe, useInputDateSegmentRecipe } from '@styleframe/theme';
const s = styleframe();
const inputDate = useInputDateRecipe(s);
const inputDateSegment = useInputDateSegmentRecipe(s);
export default s;
Build the component
Put the inputDate class on the wrapper, nest a .input-date-field row inside it, and render one inputDateSegment span per part, interleaved with a .input-date-separator glyph. The invalid, disabled, and readonly axes accept booleans directly:
import { inputDate, inputDateSegment } from "virtual:styleframe";
interface InputDateProps {
color?: "light" | "dark" | "neutral";
variant?: "default" | "soft" | "ghost";
size?: "sm" | "md" | "lg";
invalid?: boolean;
disabled?: boolean;
readonly?: boolean;
value?: string;
separator?: string;
}
export function InputDate({
color = "neutral",
variant = "default",
size = "md",
invalid = false,
disabled = false,
readonly = false,
value = "12/31/2026",
separator = "/",
}: InputDateProps) {
const segments = value.split(separator);
const focusable = !disabled && !readonly;
return (
<div
className={inputDate({ color, variant, size, invalid, disabled, readonly })}
role="group"
aria-label="Date input"
>
<span className="input-date-field">
{segments.map((segment, index) => (
<span key={index}>
<span
className={inputDateSegment({ size })}
tabIndex={focusable ? 0 : undefined}
role="spinbutton"
aria-disabled={disabled || undefined}
>
{segment}
</span>
{index < segments.length - 1 && (
<span className="input-date-separator" aria-hidden="true">
{separator}
</span>
)}
</span>
))}
</span>
</div>
);
}
See it in action
Colors
The Input Date wrapper includes 3 color variants: light, dark, and neutral. Like the Input and OTP recipes, these are neutral-spectrum surface colors rather than status colors — the color sets the field background and border, while the :focus-within ring stays @color.primary.
The neutral color adapts automatically: a light surface in light mode and a dark surface in dark mode, making it the safest default for general-purpose forms.
Color Reference
| Color | Token | Use Case |
|---|---|---|
light | @color.white / @color.gray-200 | Light surfaces, stays light in dark mode |
dark | @color.gray-900 / @color.gray-700 | Dark surfaces, stays dark in light mode |
neutral | Adaptive (light ↔ dark) | Default color, adapts to the current color scheme |
neutral as your default date-field color. It adapts to the user's color scheme automatically, so you don't need to manage light and dark surfaces separately.Variants
Three visual styles control how much surface the field shows. All three share the same :focus-within ring and invalid border — they differ only in their resting background and border.
Default
default draws a bordered field on a solid surface — the most legible option and the right default for forms.
Soft
soft fills the field with a subtle tinted background and a matching border, for a gentler look that still reads as an input.
Ghost
ghost is transparent until focused, for dense or low-chrome layouts. Pair it with a clear label so the field stays discoverable.
Sizes
Three size variants from sm to lg control the font size, field padding, and border radius. Pass the same size to the wrapper and to each segment so the segment typography and corner radius track the field.
Size Reference
| Size | Font Size | Padding (Y / X) | Border Radius |
|---|---|---|---|
sm | @font-size.xs | @0.375 / @0.625 | @border-radius.sm |
md | @font-size.sm | @0.5 / @0.75 | @border-radius.md |
lg | @font-size.md | @0.625 / @0.875 | @border-radius.md |
size axis that scales the segment font, horizontal padding, and the highlight's corner radius. Spread the same size to both parts so they stay in proportion.States
Invalid
Set the invalid state when the entered date is wrong or out of range. The field border and the :focus-within ring switch to @color.error, layered over whatever color and style the field already uses.
Disabled
The disabled state dims the field to 0.5 opacity, switches the cursor to not-allowed, and blocks pointer interaction. Drop tabindex from the segments so the field also leaves the tab order.
Read-only
Applies a subtle background shift and a default cursor while keeping the value selectable. Keep the segments out of the tab order (or non-editable) so the value is shown but not changed.
invalid is applied before readonly and disabled in the compound variant order, so an invalid field keeps its error border even when it is also read-only, and a disabled field still dims on top of any other state.Anatomy
The Input Date is composed of two recipes plus a plain separator selector.
| Part | Recipe / Selector | Role |
|---|---|---|
| Field | useInputDateRecipe() | The .input-date wrapper — owns the visual field (border, background, padding, :focus-within ring) and lays out the inner .input-date-field row |
| Segment | useInputDateSegmentRecipe() | The .input-date-segment editable part — transparent, inherits typography, centers digits with tabular figures, and highlights on :focus |
| Separator | .input-date-separator | A muted, non-selectable glyph (@color.text-weak) between segments — registered by the wrapper's setup, no axis of its own |
<div class="inputDate(...)" role="group" aria-label="Date input">
<span class="input-date-field">
<span class="inputDateSegment(...)" tabindex="0" role="spinbutton">12</span>
<span class="input-date-separator" aria-hidden="true">/</span>
<span class="inputDateSegment(...)" tabindex="0" role="spinbutton">31</span>
<!-- one segment per part -->
</span>
</div>
The wrapper carries the entire color / style / state surface; the segments are transparent and only paint a highlight on focus, so every segment in a field stays visually consistent.
Accessibility
- Group and label the field. Wrap the segments in an element with
role="group"and anaria-label(e.g. "Date input") so assistive technology announces them as one control rather than several unrelated fields. - Expose each segment as a spinbutton. Give every editable segment
role="spinbutton"witharia-valuenow/aria-valuetextso screen readers announce the current value and arrow-key editing is understood. - Keep segments focusable. Make each segment reachable with
tabindex="0"(when not disabled or read-only) so keyboard users can move part to part; the wrapper's:focus-withinring lights the whole field as they go. - Don't rely on color alone. The
invalidstate changes the border and ring color; pair it with a visible error message so the failure isn't conveyed by color only, satisfying WCAG 1.4.1. - Reflect disabled and read-only. Drop
tabindex(and setaria-disabled) fordisabled, and keep segments out of the edit path forreadonly, so the field behaves correctly for keyboard and assistive-technology users. - Verify contrast. The
:focus-withinring and the segment focus highlight use@color.primary. Default tokens meet WCAG AA; if you override the primary color, verify with the WebAIM Contrast Checker.
Customization
Overriding Defaults
Each Input Date composable accepts an optional second argument to override any part of the recipe configuration. Overrides are deep-merged with the defaults, so you only need to specify the properties you want to change:
import { styleframe } from 'virtual:styleframe';
import { useInputDateRecipe } from '@styleframe/theme';
const s = styleframe();
const inputDate = useInputDateRecipe(s, {
defaultVariants: {
color: 'neutral',
size: 'lg',
},
});
export default s;
Filtering Variants
If you only need a subset of the available variants, use the filter option to limit which values are generated. This reduces the output CSS and keeps your component API focused:
import { styleframe } from 'virtual:styleframe';
import { useInputDateRecipe } from '@styleframe/theme';
const s = styleframe();
// Only generate the neutral color and the default style
const inputDate = useInputDateRecipe(s, {
filter: {
color: ['neutral'],
variant: ['default'],
},
});
export default s;
API Reference
useInputDateRecipe(s, options?)
Creates the Input Date wrapper recipe — the .input-date field that owns the visual surface (border, background, padding, :focus-within ring) and lays out the inner segment row. Its setup callback registers the .input-date-field flex row and the muted .input-date-separator glyph.
Parameters:
| Parameter | Type | Description |
|---|---|---|
s | Styleframe | The Styleframe instance |
options | DeepPartial<RecipeConfig> | Optional overrides for the recipe configuration |
options.base | VariantDeclarationsBlock | Custom base styles for the wrapper |
options.variants | Variants | Custom variant definitions for the recipe |
options.defaultVariants | Record<keyof Variants, string> | Default variant values for the recipe |
options.filter | Record<string, string[]> | Limit which variant values are generated |
Variants:
| Variant | Options | Default |
|---|---|---|
color | light, dark, neutral | neutral |
variant | default, soft, ghost | default |
size | sm, md, lg | md |
invalid | true, false | false |
disabled | true, false | false |
readonly | true, false | false |
useInputDateSegmentRecipe(s, options?)
Creates the Input Date segment recipe — the .input-date-segment editable part that is transparent, inherits typography from the wrapper, centers its digits with tabular figures, and highlights on :focus (@color.primary background, @color.white text). It carries no color or style axis; all surface styling lives on the wrapper. Accepts the same parameters as useInputDateRecipe.
Variants:
| Variant | Options | Default |
|---|---|---|
size | sm, md, lg | md |
Best Practices
- Pass
sizeto both parts: Spread the samesizeto the wrapper and the segments so the field and segment typography stay in proportion. - Use
neutralfor general forms: The neutral color adapts to light and dark mode automatically, making it the safest default. - Expose segments as spinbuttons: Give each segment
role="spinbutton"and keep it focusable so arrow-key editing and screen-reader announcement work. - Group and label the field: Use
role="group"and anaria-labelso the segments are announced as a single control. - Filter what you don't need: If your forms use only the neutral color and default style, pass a
filteroption to reduce generated CSS.
FAQ
color axis controls the field surface, which reflects the form's surface rather than a status. This mirrors the Input recipe's color model. For an error state, use the invalid state, which switches the border and focus ring to @color.error.:focus-within ring, invalid border, and dark-mode surfaces. The difference is structural: an Input wraps one transparent native <input>, while an Input Date lays out several focusable segments separated by muted glyphs inside the same surface.inputDateSegment per part, interleaving a .input-date-separator between them. Change the separator (e.g. /, -, or : for time) and the number of segments to match the format you need — the wrapper's .input-date-field row lays them out automatically.Input
A text-field component with a wrapper-owned visual surface, inline prefix/suffix addons, and invalid/disabled/readonly states. Supports light, dark, and neutral colors, default/soft/ghost styles, and three sizes through the recipe system.
OTP
A one-time-password / pin input — a row of single-character cells with light, dark, and neutral surface colors, default, soft, and ghost styles, three sizes, and invalid, disabled, and focus states through the recipe system.