# HeroGPUI — llms.txt > UI library for Rust desktop applications. HeroUI's design system, > implemented in Rust on GPUI — Zed's GPU-accelerated UI framework. > 71 documented components, light and dark, fully typed. ## Overview HeroGPUI implements HeroUI's design system in Rust: OKLCH semantic tokens, layout tokens, and typed component builders for Windows, macOS and Linux. - **Crate:** `herogpui` (umbrella, like `@heroui/react`) - **Theme:** `herogpui-theme` — `ThemeProvider` global, `ActiveTheme` trait (`cx.colors()`, `cx.role(Color::Accent)`, `cx.layout()`) - **Core:** `herogpui-core` — `Color`, `Variant`, `FieldVariant`, `Prominence`, `Backdrop`, `Size`, `SizeXl`, `Orientation`, `SelectionMode`, OKLCH math - **Components:** `herogpui-components` — one module per `@heroui/*` package - **Gallery:** `cargo run -p herogpui-gallery` or `cargo install --path gallery --locked` — living docs, the fifteen v3 categories, theme switcher **Scope note.** This file documents the current HeroUI design system only. The previous major version used different names, and they do not exist here: `content1..4` surface tokens, numbered 50–900 colour scales, `primary`/`secondary` as colours, the `radius` prop, and the components `Navbar`, `Image`, `User`, `Spacer`, `Code`, `Snippet`. Do not emit them. ## Installation ```toml [dependencies] gpui = { git = "https://github.com/zed-industries/zed", rev = "ee3b5558c581429633937e458fad8d109f29e9ee" } gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "ee3b5558c581429633937e458fad8d109f29e9ee", features = ["font-kit", "wayland", "x11", "runtime_shaders"] } herogpui = { path = "../HeroGPUI/crates/herogpui" } ``` Clone the repository for the source dependency above. The matching GPUI API is available from the pinned Zed git revision, not its crates.io release. ```rust use gpui::*; use herogpui::prelude::*; struct MyRoot; impl Render for MyRoot { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { app_focus_root( div().size_full().bg(cx.colors().background).text_color(cx.colors().foreground), window, cx, ) } } fn main() { gpui_platform::application().with_assets(HeroGpuiAssets).run(|cx: &mut App| { ThemeProvider::init(cx); // light + dark // or ThemeProvider::init_with(Theme::dark(), cx); let bounds = Bounds::centered(None, size(px(1280.), px(820.)), cx); cx.open_window(WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), ..Default::default() }, |_, cx| cx.new(|_| MyRoot)).unwrap(); }); } ``` `HeroGpuiAssets` embeds the `herogpui/icons/*.svg` files used by built-in component chrome. Use `HeroGpuiAssets::with_fallback(MyAppAssets)` when the app also has assets. Wrap the window's root element with `app_focus_root(root, window, cx)` to enable Tab traversal, focus-visible state, and root capture handlers. Tab traversal consumes the browser default so DOM focus stays in the GPUI input surface. Set root bg/fg via tokens: ```rust div().bg(cx.colors().background).text_color(cx.colors().foreground).font_family("Segoe UI") ``` Toggle: `herogpui::theme::toggle_light_dark(cx);` or `use_theme("dark", cx)` or `set_theme(custom, cx)`. ## Theming Every base value is transcribed verbatim from `packages/styles/themes/default/variables.css` in `oklch()`. Derived values use the same `color-mix(in oklab, …)` weights the stylesheet uses. **Base:** `background`, `foreground`, `muted`, `scrollbar`, `border`, `separator`, `focus`, `link`, `backdrop`. **Containers:** `surface` (`.background` / `.foreground`), `surface_secondary`, `surface_tertiary`, `overlay` (floating panels), `segment` (segmented controls). **Roles:** `default`, `accent`, `success`, `warning`, `danger` — each a `RoleColor` with `color`, `foreground`, and derived `hover()`, `soft()`, `soft_hover()`, `soft_foreground()`. No numbered scales in v3. **Fields:** `field.background`, `field.foreground`, `field.placeholder`, `field.border`, plus `field.hover()` and `field.focus()`. **Derived on `ThemeColors`:** `background_secondary()`, `background_tertiary()`, `background_inverse()`, `separator_secondary()`, `separator_tertiary()`, `role(name)`. **Layout:** `radius` (8px) with `radius_xs()`…`radius_4xl()` and `capped(r)` for v3's `min(32px, ..)` ceiling, `field_radius`, `border_width`, `field_border_width`, `disabled_opacity`, `ring_offset_width`, `spacing`, `surface_shadow` / `overlay_shadow` / `field_shadow` (all empty in dark mode), `skeleton_animation`, `tooltip_delay_ms`, `tooltip_close_delay_ms`. ```rust div().bg(cx.colors().surface.background) div().bg(cx.colors().accent.soft()) div().text_color(cx.colors().muted) ``` Custom theme — override a base token and every derived value follows: ```rust use gpui::px; use herogpui::core::oklch; use herogpui::theme::{snow, Theme}; let brand = Theme::builder("brand", Theme::dark()) .accent(oklch(0.55, 0.23, 295.0)) .role("success", oklch(0.73, 0.19, 150.0), snow()) .radius(px(6.)) // field_radius follows at 1.5x .build(); herogpui::theme::set_theme(brand, cx); ``` Color math (`herogpui-core`): `oklch(l, c, h)`, `oklcha(..)`, `mix_oklab(a, b, t)`, `soft_mix(color, pct)`, `with_alpha`, `readable_color`. ## Prop vocabularies **Corner radii.** v3 names a different step per component, so `util` has one helper each rather than a single "control" radius: `control_radius` (3xl, 24px — button, toggle button, avatar), `soft_radius` (2xl, 16 — chip, menu and list rows, colour area), `small_radius` (xl, 12 — close button, tag, link, tooltip), `key_radius` (lg, 8 — `Kbd`), `hairline_radius` (sm, 4 — separator, skeleton), `field_radius` (12 — every form field) and `container_radius` (`min(32px, 3xl)` — cards, the table and every floating panel; Surface carries none, upstream `.surface` declares no radius). `.shots/design_audit.py` diffs these against the real stylesheets. **Control heights** are v3's desktop values, 32 / 36 / 40 for sm / md / lg. Its sheet is mobile-first (`.button` is `h-10 md:h-9`), and a desktop app is past every breakpoint. A labelled button has no minimum width: it hugs its content, as `.button`'s `w-fit` does. **Number formatting.** `formatOptions` is v3's `Intl.NumberFormatOptions`; `herogpui_core::NumberFormat` implements the subset the components use — `NumberFormat::{decimal,percent,currency("USD"),unit("kilogram")}` with `currency_sign`, `unit_display`, `minimum_fraction_digits`, `maximum_fraction_digits` and `use_grouping`. Taken by `Meter`, `NumberField`, `ProgressBar`, `ProgressCircle` and `Slider`. `locale` is not offered: choosing separators, digit systems and currency placement per locale needs CLDR *number* data, and `herogpui-core`, where `NumberFormat` lives, carries no ICU dependency. The date components do carry one, which is why a calendar follows the reader's locale and a formatted number does not. v3 replaced v2's `variant` × `color` matrix with distinct enums: | Enum | Values | Used by | |---|---|---| | `Variant` | `Primary` `Secondary` `Tertiary` `Outline` `Ghost` `Danger` `DangerSoft` | Button, ButtonGroup (no `Outline`) | | `FieldVariant` | `Primary` `Secondary` | every form control | | `Prominence` | `Transparent` `Default` `Secondary` `Tertiary` | none: Card and Surface define their own `CardVariant`/`SurfaceVariant` | | `Backdrop` | `Opaque` `Blur` `Transparent` | Modal, Drawer, AlertDialog | | `Color` | `Default` `Accent` `Success` `Warning` `Danger` | components with a color role | | `Size` | `Sm` `Md` `Lg` | most components | | `SizeXl` | `Xs` `Sm` `Md` `Lg` `Xl` | ColorSwatch, ColorSwatchPicker | | `SelectionMode` | `None` `Single` `Multiple` | ListBox, TagGroup, ToggleButtonGroup, Dropdown, Table, Select, Autocomplete, ComboBox | | `Placement` | `Bottom` `BottomStart` `BottomEnd` `Top` `TopStart` `TopEnd` `Left` `Right` | every floating panel: Popover, Dropdown, Select, ComboBox, Autocomplete, ColorPicker | | `Orientation` | `Horizontal` `Vertical` | Separator, Toolbar, Slider, Tabs, ButtonGroup, ColorSlider, groups | | `Weekday` | `Sun`...`Sat` | `firstDayOfWeek` on every date component | | `VisibleDuration` | `Months(n)` `Weeks(n)` `Days(n)` | `visibleDuration` on Calendar, RangeCalendar | | `PageBehavior` | `Visible` `Single` | how far navigation steps | | `SelectionAlignment` | `Start` `Center` `End` | where the selection sits in the visible range | `Placement` collapses v3's physical and logical spellings (`"bottom left"` and `"bottom start"`) onto one value each, since this port has no RTL mode. Position a panel with `util::placed_panel(placement, offset)` — or `util::placed_field_panel` for a trigger-width one — and always hand the result to `util::floating`, since gpui paints in tree order and `absolute` alone does not lift a panel above later siblings. ## Render props v3 passes values *into* a child render function. Where a monolithic builder computes those values anyway, it hands them to a closure instead of asking the caller to supply them — so the prop is real, just inverted: - `Table::indicator(|SortDirection| ..)` — v3's `Table.SortableColumnHeader` `indicator`, handed `sortDirection` - `Pagination::link(|page, is_active| ..)` — `Pagination.Link`, handed `isActive` - `InputOTP::slot(|index, Option| ..)` — `InputOTP.Slot`, handed `index` - `Dropdown/Menu::item_content(|key, is_selected, is_indeterminate| ..)` — `Dropdown.Item`, handed `isSelected` / `isIndeterminate` - `Slider::thumb(|index, value| ..)` — `Slider.Thumb`, handed `index` - `TimeField::segment(|TimeSegment, text| ..)` — `TimeField.Segment` - `DateField::segment(|FieldSegment, text| ..)` — `DateField.Segment`, covering date and optional time slots ## Controlled and uncontrolled Every component takes the controlled prop as an `Option`, so leaving it unset hands the state to the component, seeded from the matching `default*`: ```rust Checkbox::new("terms").is_selected(self.agreed) // controlled Checkbox::new("terms").default_selected(true) // uncontrolled Select::new("lang", opts).is_open(self.open) // controlled Select::new("lang", opts).default_open(false) // uncontrolled Dropdown::uncontrolled(trigger, items) // no `is_open` at all ``` The uncontrolled value lives in `Window::use_keyed_state`, keyed on the component's id — so two uncontrolled components that share an id share their state. `Popover`, `Accordion` and `Tooltip` therefore take an `id` purely to tell instances apart. Pairs available: `is_selected`/`default_selected` (Checkbox, Switch, ToggleButton), `is_open`/`default_open` (Popover, Select, ComboBox, Autocomplete, Dropdown, DatePicker, DateRangePicker), `selected_key`/`default_selected_key` (Tabs), `expanded_keys`/`default_expanded_keys` (Accordion), `is_year_picker_open`/`default_year_picker_open` (Calendar, RangeCalendar), `value`/`default_value` (RadioGroup, Select, CheckboxGroup, Slider, ColorArea, ColorField, ColorPicker, ColorSlider, ColorSwatchPicker, Autocomplete, ComboBox, Calendar, RangeCalendar, DatePicker, DateRangePicker, DateField, TimeField, Input, TextField, SearchField, TextArea, NumberField), `input_value`/`default_input_value` (ComboBox). Where the controlled prop is itself an `Option` — `RadioGroup::value(None)` — supplying it at all is what makes the component controlled. For components whose value lives in a state entity, the entity constructor *is* the uncontrolled seed: `InputState::with_value`, `NumberState::with_value`, `CalendarState::with_selected`, `DateRangeState::with_range`, `TimeState::with_value`. ## Validation `validate` is a function the component runs, not something the caller substitutes for. It returns the message to show, or `None`: ```rust TextField::new(state) .validate(|v| (v.len() < 3).then(|| "At least 3 characters".into())) .validation_errors(["That name is taken"]) // e.g. from a server ``` `validation::resolve` fixes the precedence — the controlled `is_invalid`, then `validationErrors`, then `validate`, with the component's own `error_message` as the fallback rather than an addition. Available on Input, TextField, TextArea, SearchField, NumberField, Checkbox, Switch, InputOTP, ColorField, DateField and TimeField. `Form` takes `validation_errors(ValidationErrors)` — HeroUI's `Record` record of server errors keyed by field name. The form routes each name into that field's own error slot; editing a field clears only its messages, reset hides them all, and a genuinely new record re-arms every named field even when the content is identical (a clone keeps the record's identity and re-arms nothing). Identity is reference identity, as in React Stately: retain one record for as long as the response is current and pass a clone each frame; building a fresh record per frame — even with identical content — is a new server response and re-arms. Unmatched names neither display nor block; fields without an error-display path receive no routed messages, so a name can never block invisibly. ## Component API pattern All components are `#[derive(IntoElement)]` builders implementing `RenderOnce`: ```rust Button::new("save") .child(icon) // ordered children: leading icon first, then label text .child("Save") .variant(Variant::Primary) .size(Size::Md) .is_pending(true) .full_width(true) .on_press(cx.listener(|this, _, _, cx| this.save(cx))) ``` Controlled state lives in your entity (`InputState`, `CalendarState`, `TimeState`, `NumberState`, `OtpState`, `DateRangeState`): ```rust let name = cx.new(|cx| InputState::new(cx)); TextField::new(name.clone()).label("Name").is_required(true) ``` Callbacks: `Fn(&ClickEvent/&str/f32/usize/bool/Option, &mut Window, &mut App)`. Fields cloned into closures must be `Arc`. ## Components (67) HeroUI documents 71 components; this file groups them into 67 entries — a few entries cover a component together with its group or slot siblings, and two entries (`PickerColor`, `DateConstraints`) describe shared helpers. The gallery and website group the same components into 66 pages. v3 keeps `size` on nineteen components (Button, ButtonGroup, ToggleButton*, Chip, TagGroup, Badge, Avatar, Switch, Meter, ProgressBar, ProgressCircle, Spinner, Pagination, Modal, AlertDialog, ScrollShadow, ColorSwatch*) and gives every form field one height instead — `util::FIELD_HEIGHT` / `FIELD_TEXT` / `FIELD_ICON`. `color` survives only on Avatar, Badge, Chip, ColorSwatch*, Meter, ProgressBar, ProgressCircle, Spinner and Typography; elsewhere `variant` (or `status`, on Alert) is the whole visual vocabulary. `CloseButton` has neither. ### Buttons - **Button** — `variant(Variant)`, `size`, `full_width`, `is_icon_only`, `is_disabled`, `is_pending`, ordered children (the icons and label text are children in order, matching v3 — there are no start/end content slots), `on_press` - **ButtonGroup** — `variant`, `size`, `orientation`, `button(Button)` (members inherit), `is_disabled`, `full_width` - **CloseButton** — `is_disabled`, `icon(el)`, `content(|state| el)`, `on_press` (v3's `variant` has one value, so there is nothing to select and no builder for it) - **ToggleButton / ToggleButtonGroup** — `is_selected`, `variant(ToggleVariant::{Default,Ghost})`, `size`, `selection_mode`, `is_detached`, `orientation`, `full_width`, `ToggleSeparator`. Both apply v3's `scale(0.97)` press. - `ToggleButton::on_change(bool)` reports the selection a press moves to, alongside `on_press`. A button's selection key is `key()` when given, otherwise its element id, so a group can namespace child ids without breaking selection. ### Collections - **Dropdown** — `Dropdown::new(trigger, items, is_open).placement(Placement).on_open_change(bool).on_action(&str)`, `MenuItem::new(key,label).shortcut().icon().danger()`, `MenuItem::SectionLabel`, `MenuItem::Separator`. Built-in labels use 14px/20px medium text, descriptions and section headers use 12px/16px, and shortcut hints render as light `Kbd`. The menu positions against the measured trigger: all eight placements flip to the side with more room when the preferred side cannot fit, keep a 12px cross-axis viewport inset, and cap the scroller to the available height with short menus keeping their natural height. Submenus position independently against their row end top and flip sides to the side with more room when needed; the parent stays anchored and outside presses still dismiss the whole menu. Descriptions wrap within the popover width; long menus remain scrollable. - selection: `selection_mode(SelectionMode)`, `selected_keys`, `disabled_keys`, `on_selection_change(&[SharedString])`, `on_action(&str)`. Selected items draw a check; disabled ones are muted and inert. The next selection comes from `selection::next_selection`, shared with ToggleButtonGroup and Table. - **ListBox** — `ListBox::new(id, items).selection_mode().selected_keys().disabled_keys().escape_key_behavior(EscapeKeyBehavior::{ClearSelection,None}).max_h().on_selection_change().on_action()`, `ListBoxItem::new(key,label).description().icon().shortcut().danger()`, `::section()`, `::separator()`. Built-in labels use 14px/20px medium text, descriptions and section headers use 12px/16px, and shortcut hints render as light `Kbd`. Fixed-row virtual lists cap at `max_h`, shrink below it in a bounded parent, and page by the visible viewport, including after resize, while skipping disabled stops. - **TagGroup** — `TagGroup::new(id, tags).label().selection_mode().selected_keys().escape_key_behavior(EscapeKeyBehavior::{ClearSelection,None}).size().variant(TagVariant::{Default,Surface}).empty_state().on_remove().on_selection_change()`, `Tag::new(key,label).icon().remove_content(render)` Small/medium tags set 12px text with 16px leading; large tags set 14px text with 20px leading. Labels, descriptions, and empty states also set their own line height, so inherited leading does not change tag heights. ### Colors ColorSlider labels/output, ColorPicker triggers, and ColorField display text use 14px text with a 20px line height independent of their parent. - **PickerColor** — shared value: `from_hex`, `to_hex`, `from_rgb`, `to_rgb`, `to_hsla`, `channel()`, `with_channel()`, `with_alpha()` - **ColorArea** — `ColorArea::new(id, value)`; `color_space(ColorSpace)` sets both axes to that space's channel pair, `x_channel` / `y_channel` override either, `size(w,h)`, `show_dots` (the dot-grid overlay), `is_disabled`, `on_change`, `on_change_end` - **ColorField** — `ColorField::new(id, value)`; `channel(ColorChannel)`, `color_space(ColorSpace)`, `label`, `description`, `variant`, `full_width`, the validity flags - `full_width(true)` expands both the outer wrapper and control to the available parent width. - `state(Entity)` makes it **editable**; without one it is a read-only display of `value`. `placeholder`, and `on_change(Option)` reporting `None` when the text is not a colour, exactly as v3 types it. With a `channel` the text is that channel's number rather than a hex value. - **ColorPicker** — `label`, `is_open`, `show_alpha`, `on_change`, `on_open_change` - When the preferred side cannot fit the popup, it flips to the side with more room, keeping a 12px cross-axis viewport inset and an 8px trigger gap, and its scroller caps to the available height so taller control stacks scroll vertically; Tab moves through area/hue/alpha and reveals each focused control. - **ColorSlider** — `ColorSlider::new(id, value, channel).color_space(ColorSpace).orientation().length().show_label().on_change().on_change_end()` - `color_space` is not cosmetic: HSL and HSB saturation are different quantities for the same colour, so a saturation slider reads and writes through `PickerColor::{channel_in, with_channel_in}`. Every other channel is space-independent. - **ColorSwatch** — `ColorSwatch::new(color)`, `size(SizeXl)`, `shape(SwatchShape::{Circle,Square})` (v3's `colorName` is an accessible name and has nowhere to go here) - **ColorSwatchPicker** — `value`, `size`, `shape`, `layout(SwatchLayout::{Grid,Stack})`, `on_change` ### Controls - **Slider** — `Slider::new(id, value).value().default_value().min_value().max_value().step().orientation().label().show_value().format_options().on_change(f32).on_change_end(f32)`. The multi-thumb form is `values(Vec)` + `on_change_all(&[f32])`; dragging moves whichever thumb is nearest, and `thumb(|index, value|)` is v3's `Slider.Thumb` render prop. Labels and outputs use 14px text with a fixed 20px line height. - **Switch** — `is_selected`, `size`, `is_disabled`, `is_read_only`, `is_invalid`, `is_required`, `label(el)`, `on_change(bool)` / `on_press`. v3's `value` is the form-submission value, not the state, so there is no builder for it. Content uses 14px text with 20px lines; the built-in label uses 16px text with 24px lines. ### Data Display - **Badge** — `BadgeAnchor::new()` is v3's `Badge.Anchor`, the positioning wrapper whose children are the anchored element plus the `Badge::new().variant(BadgeVariant::{Primary,Secondary,Soft}).color().size().placement(BadgePlacement)` composed as a later child. `BadgeLabel::new()` is the `px-0.5` text slot — v3 auto-wraps plain text children in it, which GPUI cannot intercept, so the wrap is explicit. No children on the badge is v3's dot badge, and the page-background ring stays unconditional. - **Chip** — `Chip::new().variant(ChipVariant::{Primary,Secondary,Tertiary,Soft}).color().size()` renders arbitrary ordered children verbatim (an icon, a dot, a `ChipLabel`, a trailing element); `ChipLabel::new()` is the `px-0.5` text slot for the label text. A removable chip is `TagGroup`, which is where v3 puts `onRemove`. - **Table** — `Table::new(columns).row(cells).variant(TableVariant::{Primary,Secondary}).on_row_click(usize)` — `variant` is the whole visual story in v3; `isStriped`/`isBordered` were v2. Headers use 12px/16px text; body cells use 14px/20px text in ordinary and virtual rows. - Horizontal wheel input scrolls columns; vertical input does not shift them sideways. - columns: `column(TableColumn::new("Name").allows_sorting(true).is_row_header(true))`. `is_row_header` has no a11y layer to reach, so it renders that column's cells in medium weight. - selection: `selection_mode`, `selected_keys`, `on_selection_change(&[SharedString])`, `keyed_row(key, cells)` (a row's key defaults to its index). Adds a checkbox column, with select-all in the header for `Multiple`. - sorting: `sort_descriptor(SortDescriptor::new(col, SortDirection::Ascending))`, `on_sort_change`, `show_indicator`. `SortDescriptor::next` gives the descriptor a header click should produce — flip the sorted column, start any other ascending. **The table never reorders its own rows**: a row is already-rendered cells, so the caller sorts its data and rebuilds. - keyboard: PageUp moves from the body to the first column header — from the top of a virtual body; mid-body it pages by the visible viewport. Enter/Space sorts a sortable header; Down/PageDown returns to the first/last enabled row. Unsortable headers remain outside the Tab order and preserve their configured column widths. - virtualization: with `row_height` + `virtual_rows`, the fixed-row body caps at `max_h`, shrinks below it in a bounded parent, and PageUp/PageDown move by the visible viewport, including after resize, while skipping disabled stops. - resizing: `TableColumn::allows_resizing(true).default_width(px(..)).min_width(px(..)).max_width(px(..))`; use `width(px(..))` plus `Table::on_resize` for controlled widths. `on_resize_start` / `on_resize` / `on_resize_end` receive the known `(column label, pixel width)` map for pointer and keyboard resizing. - `empty_state(el)` for `renderEmptyState`; `is_pending(bool)` + `on_load_more(fn)` for the scroll-intersection load-more sentinel. ### Date and Time - **DateConstraints** — shared by all five: `min_value`, `max_value`, `is_date_unavailable(fn)`, `first_day_of_week(Weekday)`, `weeks_in_month`. Each component takes them individually, or a whole set via `.constraints(..)`. The default first weekday comes from the operating system's regional date preferences and ICU4X's compiled CLDR week data; invalid or unavailable preferences fall back to Sunday, and `first_day_of_week` remains an explicit override. The month grid's own text follows the same preference: the month and weekday names, and the order of the year-and-month heading, come from ICU4X's compiled CLDR (a Japanese reader gets a year-first heading, not a translated template), falling back to English when CLDR does not know the reported locale. On `wasm32-unknown-unknown` there is no operating system to ask -- the browser exposes no locale to `locale_config` -- so a calendar compiled for the web always draws that English fallback, whatever the visitor's browser language. - **Calendar** — `CalendarState::new(cx)`, `Calendar::new(state).value(..).is_disabled().is_read_only().is_invalid().focused_value().on_focus_change().on_change(Option)` plus the constraints. The `locale(tag)` Unicode calendar extension selects the grid system; month/year navigation, bounds and alignment use that system, while values and callbacks stay Gregorian. - **DateField** — three editable date segments in the operating system's regional order and separators, plus optional time segments in the regional order, separators, padding, and localized day-period names, with click-to-focus, arrow steppers that roll over month and year correctly, and the `segment(|FieldSegment, text|)` render prop. The ISO text stays in the bound `InputState`, so `name`, `FormField::text` and `on_change` are unchanged. `DateField::new(InputState).label().full_width().is_required().is_invalid().variant().placeholder_value().should_force_leading_zeros(true).value(Option, cx).on_change(Option)`; month, day and hour padding can be forced to two digits. - Group text and text slots use 14px text with a 20px line height, independent of parent leading. - constraints: `min_value`, `max_value`, `is_date_unavailable`, or `constraints(DateConstraints)`. The three ways a typed date can be wrong — unparseable, out of range, unavailable — report as distinct messages, and `on_change` yields `None` for a date the constraints reject, so a caller never receives a value it said it would not accept. - **DatePicker** — `DatePicker::new(CalendarState).label().is_open().on_open_change().value().is_disabled().is_invalid().on_change()` plus the constraints; its embedded date field uses the operating system's regional date order, separators, and padding while state and form data remain ISO-formatted - **DateRangePicker** — `DateRangeState::new(cx)`, `DateRangePicker::new(state).label().placeholder().size().is_open().is_disabled().is_invalid().on_open_change(bool).on_change()`, plus the `DateConstraints` setters; both embedded date fields use the operating system's regional date format while state and form data remain ISO-formatted - the popover renders `RangeCalendar`, so the hover preview between the anchor and the cursor, the constraint treatments and the year picker all come along. - **RangeCalendar** — `RangeCalendar::new(DateRangeState).value(start, end).allows_non_contiguous_ranges().focused_value().on_focus_change().on_change(start, end)` plus the constraints. During a contiguous selection, `is_date_unavailable` receives the first endpoint and derives temporary cell, focus and navigation bounds by scanning through one visible duration on each side plus the sentinel day beyond each limit; non-contiguous mode removes those interior bounds while unavailable endpoints remain inert. `locale(tag)` uses the same calendar-system conversions for the grid, paging, year picker and unavailable-range scan. Day buttons use 14px/20px medium text in all states; custom `cell` content inherits these metrics. - **TimeField** — `TimeState::with_value(cx, Time::new(9,30))`, `TimeField::new(state).value().min_value().max_value().placeholder_value().hour_cycle(HourCycle::{H12,H24}).should_force_leading_zeros(true).show_seconds().on_change(Option