Styleframe Logo
Forms

Input Date

A segmented date input — a wrapper-owned field surface holding individually focusable date segments separated by muted glyphs, with light, dark, and neutral colors, default, soft, and ghost styles, three sizes, and invalid, disabled, and read-only states through the recipe system.

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-within ring. 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.

Styling, not behavior. These recipes style the field and its segments. Parsing input, advancing focus between segments, clamping values, and opening a calendar popover are interaction concerns you wire up yourself (or with a headless library). Calendar / popover styling lives in the Calendar recipe — this recipe styles the segmented input only.

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:

src/components/input-date.styleframe.ts
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:

src/components/InputDate.tsx
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

ColorTokenUse Case
light@color.white / @color.gray-200Light surfaces, stays light in dark mode
dark@color.gray-900 / @color.gray-700Dark surfaces, stays dark in light mode
neutralAdaptive (light ↔ dark)Default color, adapts to the current color scheme
Pro tip: Use 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

SizeFont SizePadding (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
Good to know: The segment recipe carries its own 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.

Good to know: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.

PartRecipe / SelectorRole
FielduseInputDateRecipe()The .input-date wrapper — owns the visual field (border, background, padding, :focus-within ring) and lays out the inner .input-date-field row
SegmentuseInputDateSegmentRecipe()The .input-date-segment editable part — transparent, inherits typography, centers digits with tabular figures, and highlights on :focus
Separator.input-date-separatorA 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 an aria-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" with aria-valuenow / aria-valuetext so 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-within ring lights the whole field as they go.
  • Don't rely on color alone. The invalid state 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 set aria-disabled) for disabled, and keep segments out of the edit path for readonly, so the field behaves correctly for keyboard and assistive-technology users.
  • Verify contrast. The :focus-within ring 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:

src/components/input-date.styleframe.ts
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:

src/components/input-date.styleframe.ts
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;
Good to know: Filtering also removes compound variants and adjusts default variants that reference filtered-out values, so your recipe stays consistent.

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:

ParameterTypeDescription
sStyleframeThe Styleframe instance
optionsDeepPartial<RecipeConfig>Optional overrides for the recipe configuration
options.baseVariantDeclarationsBlockCustom base styles for the wrapper
options.variantsVariantsCustom variant definitions for the recipe
options.defaultVariantsRecord<keyof Variants, string>Default variant values for the recipe
options.filterRecord<string, string[]>Limit which variant values are generated

Variants:

VariantOptionsDefault
colorlight, dark, neutralneutral
variantdefault, soft, ghostdefault
sizesm, md, lgmd
invalidtrue, falsefalse
disabledtrue, falsefalse
readonlytrue, falsefalse

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:

VariantOptionsDefault
sizesm, md, lgmd

Learn more about recipes →

Best Practices

  • Pass size to both parts: Spread the same size to the wrapper and the segments so the field and segment typography stay in proportion.
  • Use neutral for 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 an aria-label so 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 filter option to reduce generated CSS.

FAQ