@marigold/theme-rui@6.0.0
Major Changes
-
b7122c0: feat(DST-1545): replace
ActionBar.Buttonwith a plain<Button>via aButtonContextcascade.ActionBarnow provides aghost/defaultcascade to its toolbar, so authors place a standard<Button>inside the bar and it adapts to the toolbar look automatically, with the full Button API available (disabled,loading,slot,size="icon"). This mirrors the pattern already used byPanel.HeaderandButtonGroup.Breaking:
ActionBar.Buttonis removed. Replace<ActionBar.Button>…</ActionBar.Button>with<Button>…</Button>. For icon-only actions use<Button size="icon" aria-label="…">, which also fixes an accessibility defect where the old wrapper silently droppedaria-label, shipping unlabeled icon buttons. The duplicatedactionButtontheme style is gone (it hand-mirrored Button'sghostvariant at the default size, minus the press and loading affordances a real<Button>now brings).// Before <ActionBar selectedItemCount={3} onClearSelection={clear}> <ActionBar.Button onPress={edit}> <Pencil /> Edit </ActionBar.Button> </ActionBar> // After <ActionBar selectedItemCount={3} onClearSelection={clear}> <Button onPress={edit}> <Pencil /> Edit </Button> </ActionBar>
-
b7122c0: feat(DST-1237): theme-owned breakpoints with CSS fallback
Breakpoint resolution is now theme-driven:
useSmallScreenanduseResponsiveValuereadtheme.screensfrom the ThemeProvider context instead of relying on hardcoded values indefaultTheme. If no theme provides screens, the hooks fall back to reading Tailwind v4's--breakpoint-*CSS custom properties.- Added
screensto@marigold/theme-rui(matches Tailwind v4 defaults) - Removed
screensfromdefaultThemein@marigold/system - Added
resolveScreensutility for theme-first, CSS-fallback resolution
- Added
-
b7122c0: refactor([DST-1283]): Breaking Change — Remove
<Multiselect>(and thereact-selectdependency) from@marigold/components.Use
<TagField>instead. -
b7122c0: DST-878: Polish design tokens and add token documentation.
New color palette:
- Replace default gray scale with warm neutral "charcoal" palette (oklch, hue 54, 11 steps from 50-950)
Token renames and restructuring:
- Rename
brandtoprimary,muted-foregroundtosecondary,focustofocus-highlight - Rename status token structure from
*-muted/*-muted-foreground/*-muted-accentto*/*-foreground/*-accent - Add
disabled-surfacetoken for disabled control backgrounds - Add
overlay-backdroptoken for modal/tray backdrops - Update page background colors
New hover utilities:
- Add
ui-state-hoverutility (solid hover for list items, table rows, menu items) - Add
ui-state-hover-ghostutility (translucent hover for ghost buttons, tabs, action bar) - Migrate all components from raw
hover:bg-hover/hover:bg-current/10to the new utilities
Documentation:
- Add new Token Overview page with complete token reference
- Add annotated UI diagram, color palette demo, Do/Don't guidance, hover and selection pattern docs
- Remove outdated
design-tokens.mdxanddesign-token-guidelines.mdxpages
-
b7122c0: DST-1209: Refactor elevation documentation and remove deprecated utilities.
Breaking: Remove
util-surface-*utilities- Delete
util-surface-sunken,util-surface-body,util-surface-raised,util-surface-overlay - Remove
utils.cssfrom theme-rui build
Migration from legacy utilities:
Old utility New replacement util-surface-sunkenRemoved, use bg-backgroundfor the page base layerutil-surface-bodybg-background(no shadow needed)util-surface-raisedui-surface shadow-elevation-raisedutil-surface-overlayui-surface shadow-elevation-overlayDocumentation:
- Rewrite elevation page around the 3-tier shadow system (border, raised, overlay)
- Add surfaces section explaining
bg-background,bg-surface,bg-muted, andui-surface - Add interactive demos, annotated SVG diagram, and per-tier Do/Don't guidelines
- Add migration table from legacy
util-surface-*to new tokens
Fixes:
- Fix broken
bg-bg-surface-*tokens in card-elevation demo, inset-equal demo, and Columns story - Update Card docs elevation section to match new page structure
- Delete
-
b7122c0: feat(DST-1246): update Switch component layout and sizing to align with Checkbox and Radio
The Switch component previously rendered its label on the left and toggle on the right, which was inconsistent with Checkbox and Radio where the control sits on the left. When used together in forms, this created a visually misaligned layout.
Layout: Toggle now renders before the label (control on the left, label on the right), matching Checkbox and Radio. This ensures consistent visual alignment when Switch is used alongside other boolean controls in form layouts.
Sizing: Reduced the default track size from 24x40px to 16x28px and thumb from 20px to 12px. This brings the Switch closer in visual weight to Checkbox/Radio (16px), making it fit better in the flow of forms.
Settings variant: A new
variant="settings"mirrors the default layout — label and description on the left, toggle on the far right. This is the common pattern used on settings/preferences pages. The variant is propagated toBooleanFieldso that grid columns and description placement adjust accordingly.Description support: Switch now accepts a
descriptionprop (help text rendered below the control), matching Checkbox's existing support. The description text aligns with the label text using CSS grid + subgrid, automatically adapting to any control size without hardcoded padding. Properly wired witharia-describedbyfor accessibility.Form support: The
nameprop passes through to the underlying input for HTML form submission.Shared BooleanField: Extracted a reusable
BooleanFieldwrapper used by both Checkbox and Switch for consistent description rendering andaria-describedbywiring. Uses CSS grid with subgrid to align description text with label text across both components.Breaking changes
Restoring the old Switch behavior
The default Switch layout has changed: the toggle is now on the left and the label on the right (previously reversed). If you need the old layout (label left, toggle right), use the new
variant="settings":- <Switch label="Wi-Fi" /> + <Switch label="Wi-Fi" variant="settings" />
The
size="large"prop has been removed. The default size is now smaller (16x28px track). There is no built-in way to get the old large dimensions (24x40px track) — if needed, create a custom size variant in your theme'sSwitch.styles.ts.Custom theme migration
This release introduces a new required theme component
BooleanFieldand changes the layout model of theCheckboxandSwitchcontainer slots from flexbox to CSS grid. Custom themes must be updated or Checkbox/Switch will throw a runtime error.1. Add
BooleanFieldto your theme (required)BooleanFieldis a new multi-slot theme component used internally by bothCheckboxandSwitchto render descriptions. If your theme does not include it, anyCheckboxorSwitchwith adescriptionprop will throw:Error: Component "BooleanField" is missing styles in the current theme.Add the following to your theme's component styles:
import { cva } from '@marigold/system'; export const BooleanField = { container: cva({ base: 'grid gap-x-2', variants: { variant: { default: 'grid-cols-[auto_1fr]', settings: 'grid-cols-[1fr_auto]', }, }, defaultVariants: { variant: 'default' }, }), description: cva({ base: 'mt-0.5', variants: { variant: { default: 'col-start-2', settings: 'col-start-1', }, }, defaultVariants: { variant: 'default' }, }), };
container: Defines the 2-column grid layout wrapping the control and its description. Thedefaultvariant usesgrid-cols-[auto_1fr](control left, label right). Thesettingsvariant usesgrid-cols-[1fr_auto](label left, control right).description: Styles the description text wrapper. Placed under the label column viacol-start-2(default) orcol-start-1(settings).mt-0.5adds vertical spacing between the label row and description.
Then export it from your theme's component index file:
export { BooleanField } from './BooleanField.styles';
2. Update
Checkboxcontainer slot (required if customized)The
Checkboxcontainer slot changed from flexbox to CSS grid with conditional subgrid support:Before:
container: cva({ base: 'cursor-pointer read-only:cursor-default gap-2' }),
After:
container: cva({ base: [ 'grid grid-cols-[auto_1fr] gap-x-2 items-center', 'cursor-pointer read-only:cursor-default', 'group-data-[booleanfield]/booleanfield:grid-cols-subgrid group-data-[booleanfield]/booleanfield:col-span-full', ], }),
Key changes:
gap-2changed togap-x-2(column gap only, since row gap is now handled byBooleanField.description)grid grid-cols-[auto_1fr] items-centerreplaces theflex items-centerthat was previously hardcoded in the componentgroup-data-[booleanfield]/booleanfield:grid-cols-subgridandgroup-data-[booleanfield]/booleanfield:col-span-fullenable subgrid when inside aBooleanFieldwrapper, so the description aligns with the label
3. Update
Switchcontainer slot (required if customized)The
Switchcontainer slot also changed from minimal styles to CSS grid with subgrid:Before:
container: cva({ base: 'disabled:cursor-not-allowed disabled:text-disabled-foreground', }),
After:
container: cva({ base: [ 'grid gap-x-2 items-center', 'disabled:cursor-not-allowed disabled:text-disabled-foreground', 'group-data-booleanfield/booleanfield:grid-cols-subgrid group-data-booleanfield/booleanfield:col-span-full', ], variants: { variant: { default: 'grid-cols-[auto_1fr]', settings: 'grid-cols-[1fr_auto]', }, }, defaultVariants: { variant: 'default' }, }),
Key changes:
- Added
grid gap-x-2 items-center(replacesflex items-center gap-2that was previously hardcoded in the component) - Grid columns moved to
variantto support both default and settings layouts - Added subgrid support for BooleanField integration
-
b7122c0: refa([DST-1162]): Breaking changes: The
Cardcomponent has been refactored into a compound component pattern.What changed:
- The previous prop-based API (
padding,space, etc.) has been removed. - Content must now be composed using explicit sub-components:
Card.Header,Card.Body,Card.Footer, andCard.Preview. - A
CardContextis now required — sub-components will throw an error if used outside of a<Card>.
Migration:
// Before <Card> <SomeContent /> </Card> // After <Card> <Card.Header>Title</Card.Header> <Card.Body><SomeContent /></Card.Body> <Card.Footer>Actions</Card.Footer> </Card>
- The previous prop-based API (
Minor Changes
-
b7122c0: feat(DST-1381): add
master/adminaccess variants toLinkandMenuItemthat mark actions requiring elevated access rights with an icon (lock = master, key = admin).Badgerenders itsmaster/adminvariants with the same icons.The components render the icon as a decorative
<svg>colored by the theme's access foreground tokens (which also keeps it visible in forced-colors mode). OnLinkandMenuItemthe restriction is exposed to assistive technology through a visually hidden "Master"/"Admin" text label rendered after the visible label, so restricted links and menu items carry the access level in their accessible name.Badgerenders no extra label because its visible text already is the access level, which rules out double announcements by design.Note that
variantis a single axis: an access variant cannot be combined with another variant (e.g.destructiveonMenuItem). For destructive actions that are access-restricted, the access variant takes precedence (variant="master"), with the destructive nature conveyed by the action's label and confirmation flow. See the Admin & Master Mark pattern docs. -
b7122c0: feat(DST-1461):
Accordionaligns likeTableinside a bledPanel.What changed:
- A bled
Panel.Content/Panel.CollapsibleContentnow publishes a--bleed-pxcustom property (set to the Panel's--panel-px). Non-bled content is unchanged and does not set it. - The
defaultAccordionheaderandcontentinset themselves by--bleed-px(via--accordion-x-padding, which falls back to0px). So inside a bled Panel the item dividers span edge-to-edge while the header/content align with the Panel title. - In a bled Panel the full-width header (and its focus ring) is inset off the Panel border by one spacing step, matching
Panel.Collapsible.
Why:
Dropping an
<Accordion>into<Panel.Content bleed>now gives full-width item dividers and header/content aligned with the Panel title — the same behaviorTablealready had, with no new Accordion prop or variant.Impact:
- Standalone Accordions are unchanged (
--bleed-pxis only set by a bled Panel container, so the inset resolves to0px). - Accordions inside a non-bled
Panel.Contentare also unchanged — the inset stays0px, so header/content keep aligning with the dividers as before (no double indent). - Only Accordions inside a bled Panel gain the inset. The
cardvariant is unaffected; it keeps its ownpx-4.
- A bled
-
b7122c0: feat(DST-1551): add
DateRangePickercomponentNew
<DateRangePicker>lets users enter or select a start–end date range through a single field, mirroring<DatePicker>'s API and behaviour. Two date inputs (start/end) sit in one field group with a calendar button that opens a<RangeCalendar>in a popover on desktop and a tray on small screens. Supports per-input paste (ISO/EU/US formats),granularity(inline time segments),visibleDuration(up to three months), and the usual Marigold field props (disabled,readOnly,required,error,errorMessage,description,minValue,maxValue,dateUnavailable,width,variant,size). Adds a matchingDateRangePickertheme entry totheme-rui. -
b7122c0: feat([DST-1134]): add
<RangeCalendar>component (alpha)Adds a new
<RangeCalendar>for selecting a contiguous or non-contiguous date range, built on react-aria's<RangeCalendar>with Marigold conventions (disabled,readOnly,error,dateUnavailable,allowsNonContiguousRanges). Supports up to three side-by-side months viavisibleDuration, stacking vertically below thesmbreakpoint; the same responsive stacking now applies to multi-month<Calendar>for parity.descriptionanderrorMessageroute through<FieldBase>so the help/error UI matches the rest of the form-component family (TriangleAlert icon + HelpText container). Ships as an alpha component with a stub docs page under the form section. -
b7122c0: feat(DST-765): add
<SegmentedControl>componentAdds a new
<SegmentedControl>for compact, single-select view switching and quick filters. It is built on react-aria'sRadioGroup/RadioField/RadioButtonwith aSelectionIndicator, so it is a real form field:value/defaultValue/onChange, thenameattribute (submits like a radio group),required,error+errorMessage,description,readOnly, and validation all work exactly like the other Marigold form components (label/description/error route through<FieldBase>). The selected segment is marked by an animated indicator that slides between options.Options are declared via the compound API
SegmentedControl.Option(also exported asSegmentedControlOption), each with avalue:<SegmentedControl label="View" defaultValue="list"> <SegmentedControl.Option value="list">List</SegmentedControl.Option> <SegmentedControl.Option value="grid">Grid</SegmentedControl.Option> </SegmentedControl>
Two variants —
default(abg-controltrack with a raisedui-surfacethumb, mirroring theSwitch) andghost(track-less, with a translucent ghost-Button-style indicator for dense toolbars) — at a singledefaultsize (matching theh-controlInput height). Hover and focus reuse the sharedui-*utilities (ui-state-focus,ui-state-hover-ghost); the indicator slides between options (ease-out-quint) and respectsprefers-reduced-motion.To make segments divide the available width equally, use the standard
widthprop — e.g.width="full". There is no separatefullWidthprop.When the options exceed the available width the control scrolls horizontally instead of compressing the segments, keeping the selected option scrolled into view (reduced-motion aware). A scroll-driven edge fade signals there is more to scroll where supported, falling back to a native scrollbar otherwise.
ToggleButtonGroupnow logs a dev-only warning when used withselectionMode, steering single-select use cases towardsSegmentedControl(it remains for independent on/off actions in toolbars). -
b7122c0: style(DST-1586): scale breadcrumb separators with the size variant and emphasize the current page
The chevron separator now scales with the breadcrumb size (
small/default/
large) instead of rendering at a fixed 16px, so it stays a quiet mark between
crumbs; gaps tighten onsmallaccordingly. The current page reads a tier above
the trail — medium weight in theforegroundink — matching how the sidebar
marks the active item.The
<Breadcrumbs>chevrons drop their hardcodedsize={16}prop: the theme's
Breadcrumbsitemslot now owns the separator size ([&_svg]:size-*, which
wins over the SVG's width/height), so the number in the component was dead and
misleading. Themes that don't set a separator size fall back to the icon's
default size. -
b7122c0: feat([DST-1429]):
Cardnow exposes aPanel-aligned padding API.What changed:
Cardacceptsp/px/pyprops (mutually exclusivepvspx+py), resolving to CSS custom properties--card-pxand--card-pyon the container. Defaults tosquare-regular.- A new
spaceprop controls the gap between slots, resolving to--card-gap. Defaults toregular. Card.BodyandCard.Footeraccept an opt-inbleedprop to skip horizontal padding for tables, media, or full-width action bars.- Internally,
Cardswitched from CSS grid withgrid-template-areasto a flex column withgap-y. JSX order now determines visual order — placeCard.Previewfirst when used. - Slot theme styles (
header,body,footer) no longer hardcodepx-4/py-*; padding lives in the component layer and is driven by the CSS variables above. Card.Previewautomatically escapes the container's vertical padding when used as the first or last child via negative margins.
Why:
Cards previously had no consumer-controllable padding API and no default padding on the container — content rendered as direct children of
<Card>was visually broken. The new API mirrorsPanel's padding model so the two surfaces behave consistently.Migration:
- Wrap bare children in
<Card.Body>. Bare children inside<Card>are no longer rendered with horizontal padding; this matchesPanel's composition contract. - If you used
Card.Previewfor media at the top, keep doing so — it stays edge-to-edge. - No changes needed for the canonical composition (
Preview+Header+Body+Footer).
-
b7122c0: feat(DST-1373): adopt the slot-configuration pattern in
CardCard.Headeris now a slot provider: drop a<Title>and an optional<Description>directly inside it and the header wires up the heading level, id, accessible name, and theme classes automatically. A bare<Title>placed directly inside<Card>(noCard.Headerwrapper) is also picked up by the root, so title-only cards can skip the header and still get the right padding andaria-labelledbywiring.<Card>itself now renders an<article>landmark and is automatically labelled by its<Title>viaaria-labelledby, or by an explicitaria-label. A newheadingLevelprop (default3) controls the underlying heading tag for the document outline.The theme
Cardslot map gainstitleanddescriptionentries — the typography previously carried on theheaderslot has moved totitle. Variant text color now flows through a new--card-accentCSS custom property, somasterandadmincards pick up the matching accent automatically. Raw<Stack>/<Headline>composition insideCard.Headerstill renders but does not pick up the slot wiring; prefer<Title>/<Description>going forward. -
b7122c0: feat(DST-876): add Card usage guidelines
Renames the
Card.Previewslot toCard.Mediaacross components, theme, and docs. This is a breaking change: consumers using<Card.Preview>, thedata-card-previewselector, or thepreviewtheme slot key must migrate toCard.Media,data-card-media, and themediaslot key respectively.Adds a "Usage" section to the Card docs covering when to use cards, media slot guidance.
-
b7122c0: feat(DST-1369): adopt the slot-configuration pattern in
Dialog,Drawer, andTrayThe three overlay components now follow the same slot-configuration pattern as
PanelandCard. Each publishes the slot contexts at its root, so the title, description, and action primitives pick up the overlay's theme classes wherever they are dropped:Dialog.Title/Drawer.Title/Tray.Titleare thin wrappers over<Title slot="title">.- New
Dialog.Description/Drawer.Description/Tray.Descriptionwrap<Description slot="description">. - New
Dialog.Header/Drawer.Header/Tray.Headerare optional layout wrappers that group a title and description. A bare<Title slot="title">(or<*.Title>) without a header is a first-class, accessible authoring form —aria-labelledbyresolves to it automatically.
The compound-component API is unchanged. The
<header>element that previously wrapped the title is gone; the title now carries the header chrome directly, with no change to the rendered visuals. -
b7122c0: feat(DST-1633):
Dialogreveals a header seam once its body scrolls.What changed:
- The
Dialogheader is borderless at rest and fades in a bottom hairline once the body scrolls beneath it, matching theSidebar's scroll-revealed seam. A short (non-scrolling) dialog stays seamless. - The scroll-seam mechanism is promoted from a private
Sidebarutility to a reusableui-scroll-seam-*primitive. It is the companion toui-scroll-edgefor the case where the header is a grid sibling of the scrolling body rather than its ancestor. The seam color is a--seam-colorcustom property that defaults to--color-border. - The
Sidebaris migrated onto the shared primitive with its color pinned to--color-surface-border, so it renders exactly as before.
Why:
A long dialog gave no cue that its body scrolled under the header. The
Sidebaralready had this affordance, and theDialogis the second occurrence of the same header-over-scrolling-body structure, so the mechanism becomes a shared primitive instead of a copy.Impact:
Dialoggains the affordance automatically with no API change, covering both<Dialog.Header>and bare<Title>dialogs.Sidebaris unchanged visually.- Browsers without CSS scroll-driven animations (for example Firefox) fall back to an always-on hairline, so every engine still shows a divider.
- The
-
b7122c0: feat(DST-1238): align Dialog with the shared
ui-panel-*patternDialog now adopts the
ui-panel-contentandui-panel-actionsutilities already used by Drawer, Tray, and Sidebar. Visible changes are scoped to Dialog:- Actions gain a
border-tdivider as the interaction-zone marker - Content gains consistent
py-4vertical padding (waspy-1) - Actions padding becomes symmetric
py-4(waspt-4 pb-6) - Responsive button stacking (
flex-col-reverse sm:flex-row) remains Dialog-specific identity
Dialog's header keeps its original treatment (
px-6 pt-6, no border). It's a conversational opener that sits close to the message body, not a section divider above scrollable content — the ticket's "maintain unique identity where functionally necessary" guidance applies.New
ui-panel-contentutility (overflow-y-auto outline-none px-6 py-4) added; adopted by Dialog and Drawer. Tray retains its compactp-2content padding and Sidebar retains its tighterpx-3 py-1nav padding because those areas have intentionally tighter densities.The
ui-panel-*utilities are now documented with an inline docstring inui.cssclarifying they're for modal-style panels (Dialog/Drawer/Tray/Sidebar) and distinct from thePanelcomponent.Drawer, Tray, and Sidebar have no visual change — Drawer migrates inline padding/scroll classes to the new
ui-panel-contentutility (identical CSS output); Tray and Sidebar style files are untouched. - Actions gain a
-
b7122c0: feat(DST-1282): scroll the Tabs row horizontally when it overflows
When more tabs are rendered than fit the available width,
Tabs.Listnow scrolls
horizontally instead of wrapping onto multiple lines or pushing the page wide. Tabs
keep their natural width (shrink-0) and snap gently into place (proximity) as you
scroll, with the adjacent tab kept peeking past the edge so the scrollability stays
discoverable. A vertical mouse wheel scrolls the row horizontally (pointer users
without a trackpad), without hijacking normal page scroll. Horizontal overscroll is
contained so it does not trigger browser back/forward gestures, and scrolling is
smooth for users who allow motion. On browsers that support scroll-driven animations
the overflowing edges fade out (ui-scroll-mask-x); elsewhere it falls back to a
plain scroll container. When all tabs fit, nothing changes visually.The sliding selection indicator stays correct while react-aria scrolls an off-screen
tab into view (the scroll container is alayoutScrollmotion element). No runtime
API change.Breaking change (
@marigold/system): theTabsthemeRecordgains a new
requiredtabsListScrollslot. It is deliberately required so a theme cannot ship
tabsList(whosew-maxtriggers the overflow) without the scroll container that
makes it behave. Custom themes that define aTabsblock must add atabsListScroll
entry to type-check. -
b7122c0: feat(DST-1360): introduce
AppShell,Page,Page.Header, andPage.Content; removeAppLayoutRenames
AppLayouttoAppShelland removes its three pass-through subcomponents (AppLayout.Sidebar,AppLayout.Header,AppLayout.Main) —<Sidebar>,<TopNavigation>, and<Page>now sit directly inside<AppShell>(each owns its grid area, so child order does not matter).AppShellabsorbsSidebar.Providervia thedefaultSidebarOpenprop; render your own<Sidebar.Provider>around<AppShell>for controlled state,variant, orsizeand it is detected and used instead of the internal one.Adds
<Page>— the<main>landmark with page padding (p, orpx/py; defaultsquare-relaxed) and vertical rhythm between sections (space; defaultgroup). The page's<main>is named by its<h1>viaaria-labelledby; when there is no<Title>, passaria-label(or your ownaria-labelledby) instead. With none of these,<Page>warns in development so the landmark is never silently unnamed. Like<Panel>,<Page>forwards standard HTML attributes (id,data-*, event handlers) and arefto its<main>.Adds
<Page.Header>— a slot-based title/description/actions header that mirrorsPanel.Header— and an optional<Page.Content>(with its ownspace) for when the rhythm between sections should differ from the header-to-content gap. The page heading outline now falls out of the defaults:<Title>inPage.Headeris anh1,<Title>inPanel.Headeranh2,<Title>inPanel.Collapsibleanh3(override per<Page>withheadingLevel).Migration:
-<Sidebar.Provider defaultOpen> - <AppLayout> - <AppLayout.Sidebar>…</AppLayout.Sidebar> - <AppLayout.Header>…</AppLayout.Header> - <AppLayout.Main>{content}</AppLayout.Main> - </AppLayout> -</Sidebar.Provider> +<AppShell defaultSidebarOpen> + <Sidebar>…</Sidebar> + <TopNavigation>…</TopNavigation> + <Page> + <Page.Header> + <Title>Billing</Title> + <Description>Manage your plan and invoices.</Description> + <Button variant="primary">Upgrade plan</Button> + </Page.Header> + {content} + </Page> +</AppShell>
-
b7122c0: feat(DST-1439): give
SectionMessagea neutral surface with a muted variant borderSectionMessageno longer fills its background with a per-variant tint (bg-info,bg-success,bg-warning,bg-destructive). It now sits on a neutralui-surfacewith neutral title and body text. The severity is carried by a muted per-variant colored border (the accent mixed halfway into the neutral border) plus the colored icon.This is a visible design change for every variant. Because the surface stays neutral, standard
<Button>and<Link>actions placed inside read correctly instead of floating on a colored fill. On the old tint the default<Button>(variantsecondary, which has its ownui-surfacefill) rendered as a foreign chip on the colored container for every variant except error. The muted border sits at the container edge, away from the content, so it signals the variant without touching the actions. The border color is set throughui-surface's--ui-border-colorhook, which is registered as a non-inheriting custom property, so it stays scoped to the container and does not leak into nested action borders.Variants stay distinguishable without relying on color alone through the colored border, the distinct icon shape and color, and the title. The bordered, in-flow treatment keeps an inline message visually distinct from the floating, shadowed
<Toast>. No API changes. -
b7122c0: feat(DST-1483): remove ActionButton in favor of a slot-aware Button (rename ActionGroup → ButtonGroup)
The beta-only
<ActionButton>is removed.<Button>is now slot-aware: it adapts
automatically inside a button container, so you write<Button>everywhere instead
of learning a second button component.<ActionButton>is removed. Use<Button>; it adapts inside<ButtonGroup>and
<Panel.Header>. Opt a button out of the cascade withslot={null}.<ActionGroup>is renamed to<ButtonGroup>, mirroring the existing
ToggleButtonGroup → ToggleButtonContext → ToggleButtontrio.- A single Marigold-owned
ButtonContextdrives the cascade (replaces
ActionButtonContext+ActionGroupContext). RAC's ownButtonContext
(close/increment/decrementslots) is untouched. - Uniform precedence: a local prop (
variant,size,disabled) always wins
over the container. This drops the formerActionGroupsize-group-wins outlier. <ButtonGroup>cascadesvariant: 'secondary'when unset, the same baseline
as a standalone<Button>. Slot-aware parents override it where they want
lower emphasis:<Panel.Header>cascadesvariant: 'ghost'+size: 'small',
so a labelled header action stays readable. An icon-only action (a bare-icon
<Button>, an<ActionMenu>kebab) setssize="icon"to render as a square.<ButtonGroup>now owns a structuralflex gap-1layout (orientation-aware), so
a standalone cluster is spaced correctly —<ActionGroup>had no layout of its
own. A container's positional className (e.g. Panel's[grid-area:actions]) still
rides along and positions the group.- Overlays (
Popover,Modal,Tray,Drawer) resetButtonContextat their
content root, so a header/group cascade can't leak through the portal into an
overlay'sslot="close"orDialog.Actionsbuttons. <SelectList.Option>cascadesvariant: 'ghost'to a nested<Button>,
<LinkButton>, or<ActionMenu>, so a trailing in-row action reads as
low-emphasis chrome without an explicitvariant.
Migration
<ActionButton>→<Button>(itsdefaultvariant maps tovariant="ghost").<ActionGroup>→<ButtonGroup>.ActionButtonContext/ActionGroupContext→ButtonContext.<ActionMenu>keeps its public name. Its trigger is now a slot-aware<Button>
that inherits the cascade instead of hardcoding a variant: it renderssecondary
on its own (the standalone<Button>baseline, matching the pre-unification look)
andghostinside<Panel.Header>,<SelectList.Option>, or a<ButtonGroup>.
Avariantset on the<ActionMenu>still wins.
-
b7122c0: Add relative date presets to
Calendar,RangeCalendar,DatePicker, andDateRangePickervia a newpresetsprop. On desktop the presets render as a quick-selection list beside the calendar. On small screens the grid renders first with a "Quick selection" row: inline calendars open the preset list in a bottom sheet, while the pickers switch their existing sheet to the list in place. Ships built-in localized presets (today,yesterday,tomorrow,this-week,next-7-days,next-30-days,last-7-days,last-30-days,this-month,this-quarter), supports custom presets with value resolvers, and exportsuseDatePresets/useDateRangePresetsfor userland compositions. -
b7122c0: feat(DST-1533): add
Table.FootercomponentNew
<Table.Footer>renders a semantic<tfoot>after<Table.Body>for summary rows like totals, counts, or averages, composed from<Table.Row>and<Table.Cell>just like the body. Supports astickyprop that pins the footer to the bottom of the viewport while scrolling, mirroring sticky table headers. Adds a matchingfootertheme entry totheme-rui. -
b7122c0: style(DST-1602): quiet surfaces — one shadow tier, raised caps, tonal panels
The next iteration of the surface/elevation model: nothing in normal document
flow casts a shadow anymore.- One shadow tier.
shadow-elevation-overlayis the only remaining tier and
means one thing — a surface floats above the page (Dialog, Drawer, Menu,
Popover, Toast, ActionBar).shadow-elevation-borderand
shadow-elevation-raisedare removed. - Raised caps, not shadows. Secondary Button and Menu trigger move to the new
ui-softcap; the cap itself is the lift, no drop shadow. - Flat controls, tonal panels. Fields become flat wells; Card, Panel, and the
Accordion card variant separate from the gray page by fill instead of elevation. - Softer structural lines & lighter backdrop.
--color-borderand the modal
backdrop are quieted to match the flatter surfaces.
No component API changes — components using the removed shadow tokens are migrated
in this release. Consumers referencing--shadow-elevation-borderor
--shadow-elevation-raiseddirectly should move toshadow-elevation-overlayor
a flat surface. - One shadow tier.
-
b7122c0: feat(DST-1609): two-level sidebar navigation with
Sidebar.RailandSidebar.RailItemAdds a two-level navigation mode to the sidebar: a persistent rail of icon-first
top-level destinations next to a panel showing the active section's sub-navigation.
ASidebar.RailItemwrapping aSidebar.Navis a section that shows a panel. One with
only anhrefis a direct link, and one insideSidebar.Footerpins to the bottom of the
rail. Itsactiveprop overrides href matching for pages the URL can't identify.
Collapsing (toggle or Cmd/Ctrl+B) hides the panel while the rail narrows to an icon
strip, so top-level navigation always stays available. On small screens the rail
renders as the same single-column drawer as the plain sidebar: sections drill in
(opened at the active section) and links close the drawer.<AppShell>switches to a full-width top bar automatically when a rail is present
(pure CSS via:has()), so the brand never moves when the panel collapses.Sidebar.Togglegainsvariant="rail"for its top-bar placement between the
brand and the breadcrumbs.- New theme tokens:
--spacing-topbar(the shell's shared vertical datum for the top bar
height, sidebar brand row, and rail sticky offset), the rail column widths
--spacing-rail/--spacing-rail-collapsed/--spacing-rail-panel, and
--spacing-touch-target(44px minimum row height on small screens, shared by the
drawer's nav rows and the existing Tray-mode ListBox/Menu options). - Idle single-column nav labels darken a step (new
--color-secondary-boldtoken,
charcoal-700) so they clearly out-rank the quiet group-label captions. - Keyboard: the rail supports arrow-key (and Home/End) movement on top of its flat
tab order, and the section panel's tab stop re-syncs to the current page when the
route changes, so Tab re-enters at the active item. - The
TopNavigationbottom edge is now an always-on border. The non-reusable
ui-scroll-edgeandui-sidebar-seam-headerutilities are removed, so the sticky
bar and the sidebar header carry a plain border instead. - The
AppShellheader row is now sizedauto(was a fixed3.5rem), so a shell
without aTopNavigationno longer reserves an empty header band, so the row
collapses to the height of its content. - The shell's viewport-height claims (
AppShellgrid, sidebar and rail asides) read
the new--ui-viewport-heightcustom property with a100dvhfallback. Set it on a
wrapper to render the shell inside a bounded container (embedded previews, demos)
instead of the browser viewport. Nothing changes when it is unset.
-
b7122c0: feat(DST-1643): add a
fullscreensize toDialog<Dialog size="fullscreen">fills the viewport (minus a small margin) at every breakpoint, giving content-heavy picks room for search, filters, and a long scrollable list while the title and actions stay fixed. The existingxsmall/small/medium/largesizes are unchanged. -
b7122c0: feat(DST-990): enrich
<Menu>with selection visuals, keyboard shortcuts, and dividers<Menu>gains richer building blocks for advanced menus:- Selected-item visuals. In
selectionMode="single"or"multiple", items show a leading checkmark and a highlighted row, aligned like<ListBox>. Command menus (noselectionMode) render exactly as before. - Keyboard-shortcut hints via a new shared
<Keyboard>primitive (a sibling to<TextValue>and<Description>). It renders a<kbd>key-cap on its own and adapts to its container, so inside aMenu.Itemit becomes a muted, right-aligned hint wired to react-aria'saria-describedby. - Dividers. Drop the shared
<Divider>between<Menu.Item>s to separate groups with arole="separator"line.
Breaking (
@marigold/system): theMenurecord in theThemetype now requires akeyboardkey. Custom themes implementingMenumust add it to keep compiling. All@marigold/componentsadditions are backward compatible. - Selected-item visuals. In
-
b7122c0: refa([DST-1298]): Refactor Divider component: API, styling, and docs
We fixed the vertical orientation of the divider, which previously didn't work.
Added new Divider stories and updated the Divider docs. -
b7122c0: feat(DST-1641): add
ErrorState, the error sibling ofEmptyState: same anatomy (title,description,action,headingLevel), plus typed DOM passthrough (role,tabIndex,ref) for error-boundary fallbacks. -
b7122c0: feat(DST-1257): add universal
nonespacing token- Introduce
NoSpacingToken = 'none'shared across all spacing token families - Add
'none'toSpacingTokens,PaddingSpacingTokens, andInsetSpacingTokens - Add
--spacing-none: --spacing(0)CSS custom property to the theme
'none'now works wherever a spacing token is accepted:Stack/Inlinegap (space="none"),Insetaxis padding (spaceX="none"/spaceY="none"), andInsetrecipes (space="none") — useful for wrappers that should render without adding any spacing (e.g. an edge-to-edgeTableinside a containing component). - Introduce
-
b7122c0: feat(DST-1326): introduce
Panel.CollapsibleHeader,Panel.CollapsibleTitle, andPanel.CollapsibleDescription. The collapsible mirrorsPanel.Header— a header wrapper with a title plus an optional description — and the whole visual surface is a single click target: title and description render as spans inside the trigger<button>, with the accessible name wired viaaria-labelledbyand the description viaaria-describedby. The chevron icon uses a reusableMorphCaretthat animates via SVG path morphing (honoursprefers-reduced-motion). -
bb23186: feat(DST-1700):
--bleed-pxis the documented way to align content with a container's padding, and the overlay surface anatomy no longer borrows Panel's name.What changed:
- The
ui-panel-header/ui-panel-content/ui-panel-actionsutilities and their--ui-panel-pxtoken are nowui-surface-header/ui-surface-content/ui-surface-actionsand--ui-surface-px. They style the shared anatomy of sectioned overlay surfaces (Dialog,Drawer,Tray,Sidebar) — never thePanelcomponent, which has its own per-instance--panel-px. They are now named afterui-surface, the role those containers already wear. Tablederives its edge cell padding from--bleed-pxalone:var(--bleed-px, var(--cell-x-padding)). It previously readvar(--panel-px, var(--bleed-px, var(--cell-x-padding))).- A bled
Card.ContentandCard.Footernow publish--bleed-px(set to the Card's--card-px), which they never did. - Every non-bled content area (
Panel.Content,Panel.CollapsibleContent,Card.Content,Card.Footer,Drawer.Content) clears--bleed-pxwithinitial. Custom properties inherit through the whole subtree, so without the reset a container nested inside a bled one would read the outer container's padding and inset its edge-aware children twice. --panel-px/--panel-py/--panel-gap, their--card-*and--page-*counterparts, and--bleed-pxare documented as read-only public API under Reading container spacing.
Why:
--panel-pxis set on thePanelroot, so it inherits into the whole subtree whether content is bled or not. Reading it first meant the chain always resolved at step one inside a Panel and the remaining branches were unreachable: aTablein a non-bledPanel.Contentwas inset by the Panel's padding on top of the content area's own padding, putting its edge cells 12px from the border while the Panel title sat at 12px — offset twice. Every bleedable container already publishes--bleed-px, and because that is declared on the bled element rather than on a container root — and cleared again on every non-bled one — it is only in effect where edge alignment actually applies.Impact:
- A
Tablein a bledPanel.Content,Panel.CollapsibleContentorDrawer.Contentis unchanged. - A
Tablein a non-bledPanel.Contentnow uses the ordinary cell padding for its first and last cell instead of the Panel's horizontal padding. Docs steer tables tobleed, so most tables are unaffected. TableandAccordioninside a bledCard.ContentorCard.Footernow align with the container title, where previously they got no edge alignment at all.- A
TableorAccordionin a non-bled container nested inside a bled one no longer picks up the outer container's padding. Dialog,Drawer,TrayandSidebarare visually unchanged. If you override--ui-panel-pxor apply theui-panel-*utilities directly, rename them; the token was undocumented, so nothing else in the public API moves. It also only ever existed in18.0.0-rc.*, never in a stable release, so only prerelease consumers are affected.
- The
-
b7122c0: feat([DST-753]):
SectionMessageexposes anannounceprop and uses react-aria'sLiveAnnouncerto notify assistive technology.What changed (DST-753):
<SectionMessage>accepts a newannounce?: booleanprop. When set, the message text is sent to a shared, always-mounted live region maintained by@react-aria/live-announcer. Priority ispoliteforinfo/success/warningandassertiveforerror.announcedefaults totrueforvariant="error"andfalsefor all other variants, preserving today's behavior for the common error case while letting consumers opt in for confirmations and informational updates.- The wrapper element no longer carries
role="alert"for the error variant. Announcements are now delegated to the singleton live announcer instead. - Re-announcing the same message uses the React
keypattern: pass a changingkeyto force a remount.
Why:
The previous implementation only announced the
errorvariant, and it did so by addingrole="alert"to a conditionally rendered element. Per the WAI-ARIA spec and MDN guidance,role="alert"should be on an element that already exists in the DOM before its content is injected, and it should not contain interactive elements. Marigold'sSectionMessageviolated both constraints (the alert was mounted together with its content, and it can contain close buttons and action links), making announcements unreliable on some screen reader / browser combinations.The new implementation uses
@react-aria/live-announcer, which maintains persistent polite and assertive live regions at the document root. This is the same mechanism used across React Spectrum and avoids the conditional-rendering and interactive-content pitfalls of inlinerole="alert". It also unifies the API: opt in to announcement for any variant with a single prop.Additional cleanup bundled with this release (beyond DST-753):
- Close button now matches the rest of the system. The previous theme defined a
closeslot forSectionMessagewith bespoke overrides (size-8,[&_svg]:size-6,text-foreground, negative margins) that produced a visibly larger close button than every other close button in the design system. The component now renders the shared<CloseButton>with no overrides, so it gets the same 16px icon, focus ring, hover-opacity, and rounded-full styling as Dialog, Drawer, etc. Thecloseslot has been removed from theSectionMessagetheme type. - Component cleanup. Dropped a stale
useButton(props, buttonRef)call that was applying div-level props to a button, the unusedbuttonRef, and the{...buttonProps}spread on<CloseButton>. TheButtoninsideCloseButtonalready provides all keyboard/press semantics. - Theme variant order normalized.
info(the default) is now listed first across thecontainer,content, andiconslots intheme-rui, matching the variant table in the docs and the existingdefaultVariantssetting.
Docs:
- New anatomy SVG matching the Card / Sidebar / SelectList style; title and close button marked as optional, with content rules (no period in title, don't repeat title in body).
- Two realistic announcement demos: a bulk-archive form (polite, with RAC
validateand thekeyre-announce pattern) and a server-availability save error (assertive). - Added focus-management guidance for dynamic appearance and post-dismiss.
- Added form-summary placement rule pairing
<SectionMessage>with field-level validation. - Added action constraints (one primary action, verb+noun labels, descriptive link text).
- Added two-line body rule with a link-out overflow pattern for longer content.
- Folded the previous Position subsection into Usage; removed redundant Do/Don't tiles; renamed subsections to Dismissal / Actions / Announcements.
- Drive-by: typo fix in the
feedback-messagespattern doc.
Migration:
- Code relying on
getByRole('alert')or[role="alert"]selectors to find renderedSectionMessageerror nodes needs to be updated. The message text itself is still rendered as before; only the wrapper role is gone. - Consumers who previously wrapped a dynamic
<SectionMessage>in their own<div role="status">or<div aria-live="polite">can replace that wrapper with<SectionMessage announce>. - Custom themes that defined a
SectionMessage.closeslot will now see a type error. Remove the slot. Close button styling now flows entirely from theCloseButtontheme. - The SectionMessage's close button is visually smaller after this release (matches every other close button in Marigold). If you previously relied on the larger size, that was an inconsistency, not a feature.
-
b7122c0: feat(SelectList): standardized API, item layout, and visual distinction from ListBox (DST-1076)
<SelectList>has been refined into a first-class form field for picking one or many items from a visible list of rich two-line rows. This release contains breaking renames and a tightened type surface.Breaking changes
SelectList.Item→SelectList.Option. The option semantic matchesSelect.Optionand the HTML<option>mental model. Update any<SelectList.Item>usage to<SelectList.Option>.SelectList.Actionhas been removed. Drop your<ActionMenu>or<IconButton>directly inside<SelectList.Option>— the component positions, sizes, and styles the nested control automatically viaButtonContext. Limit: one action per option (multi-button groups will arrive with a futureActionGroup).- Leading-image slot has been removed. Compose images inside
<Text slot="label">(or anywhere in children) as you see fit. selectionMode="none"is no longer accepted.SelectListis a form field; the default is now"single".onChangeis strictly typed perselectionMode:(key: Key | null) => voidfor single,(keys: Key[]) => voidfor multiple. The shape matchesSelect<T, M>. PassingsetStatedirectly may require adapting the callback.
Other changes
- Selection indicator — single-select rows render a visible radio circle; multi-select renders a checkbox.
- Label & description slots — use
<Text slot="label">and<Text slot="description">inside<SelectList.Option>. The row skeleton isselection · label + description · action (optional). - Dev-mode warning when
textValueis missing on an option whose children aren't a plain string. - Own theme entry —
SelectListships a dedicated theme component. The theme exposes first-classlabel,description, andactionentries; slot styling no longer uses descendant selectors. Consumers with custom themes must add or update aSelectListentry.
Documentation
The SelectList docs page is rewritten around the new API. Adds an anatomy diagram, a decision table for choosing between
<SelectList>and lighter controls (<Radio.Group>,<Checkbox.Group>,<Select>,<Combobox>,<TagField>), and dedicated sections for multi-selection, per-row actions (decision-help and configuration patterns), horizontal orientation, and empty state. Replaces selected prose with Do/Don't tiles. Tightens the accessibility section to what's specific to SelectList (keyboard model, label requirement,textValuefor rich rows).Migration
- <SelectList selectionMode="none"> - <SelectList.Item id="free"> - <SelectList.Action> - <IconButton aria-label="Info"><Info /></IconButton> - </SelectList.Action> - Free - </SelectList.Item> - </SelectList> + <SelectList selectionMode="single"> + <SelectList.Option id="free"> + Free + <IconButton aria-label="Info"><Info /></IconButton> + </SelectList.Option> + </SelectList>
-
b7122c0: style(DST-1586): quieter sidebar navigation hierarchy and a seamless app shell
The sidebar navigation now carries hierarchy with semantic tokens instead of raw
charcoal values. The current page is an inset rounded pill (selectedfill,
foregroundtext); idle rows sit a step lighter onsecondaryand preview the
pill in thehovercharcoal. Section labels stay onsecondaryso they meet
WCAG AA contrast and read as their own tier through treatment — uppercase,
smaller, heavier, tracked — with an even gap opening above every section. The
drill-in back action shares the nav-row pill geometry, so it aligns to the same
content column and reads at the same weight as a nav item.The navigation is also denser: rows sit at a fixed 30px height with a tighter
horizontal inset, section labels keep one even rhythm, and the sidebar toggle
steps down to the small control size with a lighter icon — more rows per screen
without losing the pill affordance.The app shell keeps exactly one structural line: the sidebar divider, now at
full surface-hairline strength (it bounds the nav column so the branch rows'
trailing chevrons anchor against it, and it stays perceivable at low vision).
Every other shell line is gone — the sidebar header/footer hairlines and the
TopNavigationbottom border — so regions separate on whitespace and the
content panels carry the structure. The stickyTopNavigationreveals a bottom
hairline only once page content scrolls underneath it (ui-scroll-edge, a
scroll-driven progressive enhancement; non-supporting browsers simply stay
borderless). The mobile sidebar drawer wears the overlay elevation,
Sidebar.Separatorsteps up to the full-strength surface hairline, and the
sidebar footer now sits on the same content column as the nav and quiets its
links (secondary color, normal weight, nav-row pill on hover) so escape hatches
read as a continuation of the nav rather than competing with it.When the nav scrolls, the sidebar's own sticky header and footer reveal a seam
so mid-list rows never butt against them without a divider. Because the nav is
a grid sibling between the two (not their ancestor), the seam is driven by a
named scroll timeline the nav declares, hoisted into scope for the header via
timeline-scope— a set ofui-sidebar-seam-*utilities marked non-reusable
since they are specific to this layout. The header seam fades in as content
scrolls under it; the footer seam shows while content remains below and fades
out as the list bottoms out. Without scroll-driven animation support it falls
back to an always-on hairline. -
b7122c0: feat(theme-rui): preflight fixes, file layout refactor, token and export corrections
A cohesive set of changes that (1) adds peer-dependency fixes needed
for page-level scroll, (2) splits the theme's CSS into files with
unambiguous roles, and (3) fixes a prefixer bug that stranded design
tokens inside the scoped bundle.New:
preflight.cssTwo peer-dependency fixes on the real
<html>/<body>:html { scrollbar-gutter: stable }— prevents a 1 px reflow when
@react-aria/overlayslocks the page (it setsoverflow: hidden
on<html>and compensates scrollbar width).body { position: relative; overflow-x: clip }— contains the
@react-aria/live-announcerportal (mounted attop: -10000px; left: -10000px) so it cannot expand the document's scrollable
area.clip(nothidden) keepsposition: stickyon
descendants working.
These rules ship inside both entry points; the prefixer excludes
html/bodyso the rules reach the document root while the rest
of the bundle stays scoped to[data-theme="rui"].Scrollbar track
ui-scrollbar's track is now transparent so the themed scrollbar
blends into any surface.File layout refactor
tokens.css(new) —@plugindeclaration and every design token
in one place. Easy to find, easy to extend.theme.css(Tailwind-native entry) — importspreflight.css+
tokens.css+ui.css+variants.cssand paints<body>
directly. A Marigold-first app needs zero extra markup to get the
theme's page background, text color, and font.styles.css(pre-compiled entry) — imports the same base and
paints[data-theme="rui"]. The consumer places the attribute on
<html>,<body>, or a wrapper<div>to control where Marigold
paints — whole-app or island.global.css(removed, never released) — the body paint now lives
inline in the two entry points that actually need it, so a shared
file with ambiguous semantics is no longer necessary.
Two clean mental models
- Use
theme.csswith your own Tailwind build when Marigold is the
whole app. Body paints automatically, tokens live at:root,
utilities tree-shake against your content. - Use
styles.cssas a pre-compiled drop-in when Marigold is an
island or you are not running Tailwind. Placedata-theme="rui"
wherever Marigold should paint.
Tokens at
:rootpostcss-prefix-selectorpreviously rewrote@theme's output from
:root, :hostto[data-theme="rui"], [data-theme="rui"] :host,
which meant any unscoped rule could not resolve
var(--color-background)because the variables were only declared
inside the[data-theme="rui"]scope. The prefixer now excludes
:root,:host, and[data-theme="rui"]in addition to
html/body, so design tokens are emitted globally while utility
classes remain scoped. The[data-theme="rui"]exclude uses a
quote-agnostic regex so prettier round-trips don't reintroduce
double-prefixing.CSS exports carry a
styleconditionTailwind v4's CSS resolver uses
conditionNames: ["style"].
Bare-string export entries without a matching condition fail under
strict resolvers, so every.csssubpath now declares bothstyle
anddefaulttargets. The unused./*JS catchall is removed. New
subpath exports:./tokens.css,./preflight.css.Existing documented imports (
theme.css,styles.css) continue to
work.
Patch Changes
-
b7122c0: fix(DST-1359): align
ActionBaraction button spacing with the regularButton. TheactionButtonstyle intheme-ruiwas missinggap-2 items-center justify-center, which caused icons and labels inside ActionBar buttons to render without the proper spacing/alignment used by the ghost/defaultButton. Adding these utilities restores visual parity across the design system. -
b7122c0: Paint
CheckboxandRadiocontrols withbg-surfaceso the inner area follows the theme surface token. Keeps the controls visually distinct over containers that paint a non-default background — e.g. a hovered or selectedSelectListrow.Radioalready usedbg-surface(added in DST-878 token polish); this bringsCheckboxin parity. -
b7122c0: feat([DST-1395]): SelectList horizontal layouts now automatically flip to a vertical stack when the wrapping container is narrower than
40rem(~640px). -
b7122c0: feat(DST-1635): add a
bleedprop toDrawer.Contentso edge-aware children can span the full Drawer width.What changed:
<Drawer.Content bleed>drops the Drawer's horizontal content padding and publishes a--bleed-pxcustom property, mirroringPanel.Content'sbleed.- The horizontal padding shared by the sectioned overlay surfaces (
ui-panel-header/ui-panel-content/ui-panel-actions) now comes from a single--ui-panel-pxtoken, and a bledDrawer.Contentre-publishes that exact token as--bleed-pxso the two can't drift. - Edge-aware children stay aligned with the Drawer title while their dividers/backgrounds reach the Drawer edges:
Accordionreads--bleed-pxdirectly, andTable's edge-cell padding now falls back to--bleed-px(after the Panel-only--panel-px), so it aligns inside a bled Drawer too.
Why:
Placing an
<Accordion>(orTable) inside<Drawer.Content>previously trapped it inside the content padding, so item dividers and hover/selection backgrounds could not reach the Drawer edges.bleedgives the same full-width alignmentPanel.Content bleedalready offered.Impact:
- Default behavior is unchanged: without
bleed,Drawer.Contentkeeps the paddedui-panel-contentand--bleed-pxstays unset (children resolve their inset to0px). The--ui-panel-pxtoken resolves to the same24pxthe surfaces used before, so Dialog/Drawer/Tray are visually identical.
-
b7122c0: fix([DST-1295]): replace
gapbetweenCheckboxGroupandRadioGroupitems with per-item padding so the full space between items is clickable. Vertical items now meet the 24px target-size minimum; horizontal spacing keeps visual parity. StandaloneCheckboxis unaffected.Also align the label and icon: switched the inner row layout from
items-centertoitems-startso the icon stays on the first line when the label wraps.Radiolabels now useleading-4to matchCheckbox, andRadio's icon-to-label gap moves from an inlinegap-[1ch]to the theme-drivengap-x-2for parity withCheckbox. -
b7122c0: feat([DST-1339]): FileField gains a
size="small"compact layout renders as a single-row input-height control (upload button + file list) instead of the full drop-zone, suited for space-constrained forms. -
b7122c0: refactor(DST-1374): use
<TextValue>and<Description>for selection-container itemsConsumer-facing JSX in component stories and documentation demos for
<Select>,<SelectList>,<ListBox>,<Menu>,<ComboBox>, and<Autocomplete>now composes item content with the<TextValue>and<Description>primitives instead of hand-written<Text slot="label">/<Text slot="description">. The primitives are drop-in replacements that render the same RAC<Text>with the same default slot values, so rendering,aria-describedbywiring, and accessibility are identical.<Menu.Item>gains first-classlabelanddescriptiontheme slots, mirroring<SelectList.Option>.MenuItemmerges the Marigold theme classNames into RAC'sTextContextso nested<TextValue>/<Description>pick up Menu styling without losing RAC's slot wiring. Menu items adopt a two-column grid layout (icon column + content column) so descriptions render below labels; existing plain-text and icon+text menu items are unaffected.The
Menutheme type in@marigold/systemis extended with requiredlabelanddescriptionslot keys. Consumers maintaining a custom theme that overridesMenuwill need to add these two slots to satisfy the type.@marigold/theme-ruiis updated accordingly in this release.No public API change on
Select.Option,SelectList.Option,ListBox.Item,Menu.Item,ComboBox.Option, orAutocomplete.Option. -
b7122c0: fix(DST-1406): restore focus outline on virtualized ListBox items. RAC's virtualizer wrapper sets an inline
z-index: 0per item, creating a stacking context the option'sfocus-visible:z-1cannot escape — adjacent wrappers paint on top in DOM order and clip the focused outline (most visible when the next item isselected). Lift the wrapper containing the focused option above its siblings so the outline is fully visible. Affects all virtualized listboxes (Select,ComboBox,Autocomplete). -
b7122c0: fix([DST-1412]): fix multi-month Calendar/RangeCalendar layout at non-default widths. With three months at
width="1/2"the third month overflowed the calendar wrapper becausemin-w-[250px]only sized for a single month; withwidth="full"the date grids stayed at content size while their columns expanded, leaving header text floating over empty space. Switch the calendar minimum tomin-w-fitso multi-month grows to fit its natural content, and addw-fulltocalendarGridso the date table fills its column. -
b7122c0: fix(DST-1436): drop
backgroundfrom hover transitions acrossui-surface,ui-surface-contrast,Button,Tabs,Table(row + edit buttons),LegacyTable,Sidebar(navLink + backButton),Calendarcells,SelectList, andActionBar.clearButton. Hover background flips now happen instantly, making high-frequency controls feel snappier and aligning Button variants (primary/secondary previously transitioned the background while ghost/destructive were already instant). Color, border, box-shadow, and transform transitions are preserved. -
b7122c0: refactor(DST-1546): replace the bespoke TagGroup "Remove all" wrapper with a plain
<Button>via aButtonContextcascadeTagGroupnow provides alink/smallButtonContextaround its internal
RemoveAllrender, so the "Remove all" action is a bare Marigold<Button>
instead of the raw react-ariaButtonwith hand-rolled link styling. This
mirrors the cascade pattern already used byActionBarandPanel.Header.The change is internal-only.
TagGroupRemoveAllis not part of the public API
(TagGrouprenders it itself), the authoring API (removeAll/onRemove) is
unchanged, and there is no behavioral or accessibility change.The redundant
removeAlltheme style is removed fromTag.styles.ts(the
linkvariant atsize="small"reproduces it), and the now-unusedremoveAll
key is dropped from theTagtheme type. -
b7122c0: chore(DST-1547): move z-index classes out of theme style files into component implementations
Per the z-index management rule (
CLAUDE.md),z-*utilities belong in component implementations, never in theme*.styles.tsfiles. The local focus/drop/sticky stacking classes forCalendar/RangeCalendar,LegacyTable,ListBox,Table,ToggleButtonandSegmentedControlhave been moved from theirtheme-ruistyles into the matching componentclassName(viacn()), preserving the exact modifiers and important flag. No visual or stacking change. -
b7122c0: refactor(DST-1548): rename
Card.BodytoCard.ContentAligns the Card body sub-component with
Panel.ContentandPage.Contentso all three container primitives expose the main body region under one name.Breaking change:
Card.Body(CardBody) is removed. Rename usages toCard.Content:- <Card.Body>...</Card.Body> + <Card.Content>...</Card.Content>
The
bleedprop and padding behavior are unchanged. The internaldata-card-bodyattribute has been removed to matchPanel.Content.The
Cardtheme slot key is renamed frombodytocontentin the@marigold/systemThemetype and in@marigold/theme-rui. Theme authors overriding this slot must rename their key accordingly. -
b7122c0: fix(DST-1551): round the
RangeCalendarhover and focus highlight on days outside the selected rangeDays outside the selected range now round their hover and focus highlight to match the selected state, instead of showing a square highlight against the rounded endpoints. In-range cells stay square so the range fill still connects seamlessly.
-
b7122c0: fix(DST-1560): align IconButton and Pagination to the control sizing token
IconButton and Pagination hardcoded
h-9/size-9instead of theh-control/size-controltoken that every otherui-button-basebutton uses. The values are identical today (--spacing-control=2.25rem=h-9), so there is no rendered change, but the literals would silently drift if the token were retuned. Also dropped IconButton's redundantdisabled:text-disabled disabled:cursor-not-allowedclasses, whichui-button-basealready applies viadisabled:ui-state-disabled. -
b7122c0: fix(DST-1560): correct the
@source inlinepadding safelist to the real spacing tokensThe padding safelist in
styles.cssstill used the pre-rename vocabulary{compact, tight, regular, relaxed, spacious}, but the spacing tokens were long ago renamed to{tight, snug, regular, relaxed, loose}. As a resultcompact/spaciousforce-generated dead utility classes (resolving to undefined--spacing-*vars), while the realsnug/loosetokens were never safelisted and so were unavailable in scanner-excluded stories and in the (unscanned) docs app. The leading family-less line was also stale — it predates thesquare/squish/stretchsplit.Replaced the three lines with the three inset-padding families (
square/squish/stretch) using the real size names, so utility classes likep-squish-relaxedresolve to a concrete value and the full inset vocabulary is available where the scanner can't see it. -
b7122c0: fix(DST-1560): restore the
Toastclose-button hover stateThe Toast close button kept its
transition-[color,box-shadow]but lost its hover declaration, so it animated nothing on hover. Addedhover:text-foregroundso the icon darkens on hover, consistent with the system-wide convention that color (not background) animates on interactive controls. -
b7122c0: fix(DST-1560): align Tray content padding and Card radius to the shared surface tokens
Traycontent usedp-2while its own header and actions (and Dialog/Drawer content) follow theui-panel-contentrhythm (px-6 py-4), leaving the content visually out of step within the same component. Switched it toui-panel-content.Cardhardcodedrounded-mdwhile the rest of the raised surface tier (Panel,Accordion) usesrounded-surface. Moved Card torounded-surfacein itsbaseso every variant (default,master,admin) shares the same 8px corner, making the raised tier consistent. -
b7122c0: fix(DST-1560): use the defined
ui-scrollbarutility on Drawer and SidebarDrawerandSidebarreferencedutil-scrollbar, which is not defined in the theme (onlyui-scrollbarexists inui.css), so they rendered without the themed scrollbar while Dialog, Tray, and ContextualHelp got it. Corrected both toui-scrollbar. -
b7122c0: fix(DST-1565): restore the ToggleButtonGroup divider, fix segment focus, and match Pagination hover
Three polish fixes on the control surfaces:
- ToggleButtonGroup divider. Segments in a group had a transparent right border, so no line separated them. The divider is restored with the opaque
bordertoken (the structural-line token, opaque on purpose so it never doubles up), removed on the last segment. - ToggleButtonGroup focus (DST-1597). The group was
overflow-hidden, which clipped the focus outline of a full-height segment so it was hard to tell which segment was focused. The group no longer clips. The end segments round their own outer corners to match the frame, so the standard focus outline now renders unclipped around any segment. - Pagination hover. Unselected page numbers now pick up the same translucent ghost hover as the prev/next arrows. The selected page keeps its control fill and is left untouched.
- ToggleButtonGroup divider. Segments in a group had a transparent right border, so no line separated them. The divider is restored with the opaque
-
b7122c0: refa(DST-1565): unify surfaces and controls on one boundary and elevation model
theme-ruidrew edges two different ways with no rule for which to use: abackground-clipgradient border on surfaces, and an opaqueborderon controls. Both are replaced with a single model built on two independent axes.- The boundary encodes role. Both roles compose one role-neutral primitive,
ui-frame(a fill, a 1px ring, and a radius).ui-surfaceandui-controlare siblings of it that differ only in the boundary token, so a control is never modeled as "a kind of surface". Surfaces (Card, Panel, Dialog, Menu) wearui-surface, a quiet translucent hairline. Controls (Input, Select, fields, the neutral Button family, SegmentedControl) wearui-control, the same charcoal stroke about 2.5× denser, reading as something to operate. Both are translucent, so the edge composites over its ground and stays consistent on white, the page background, or a tinted panel. - Elevation encodes depth, independently. Now that the ring owns the crisp edge, the three
shadow-elevation-*tiers carry only lift, and controls sit on the lowest tier.
Mechanics:
- The surface boundary is a 1px Tailwind
ringin thebox-shadowchain, replacing the1px solid transparentplusborder-boxgradient that hijackedbackgroundand got covered by edge-to-edge children.ui-state-errorswaps--ui-border-color, and the theming contract is unchanged. - New
--color-surface-border(decorative surface rim) and--color-control-border(functional control edge).--color-bordernarrows to structural lines (dividers, grid lines, table rules) and stays opaque, because crossing translucent strokes double-darken at their intersections. ui-contrast(primary Button, ActionBar) uses the same model inverted: a crisp dark ring plus light on the face (top glow, top-down gradient, top-edge highlight), all derived from--ui-background-color.ui-contrast-destructiveis that recipe retinted red.- Elevation shadows retuned: the contact layer is cut and spread across lighter, warm-tinted, contained layers so dense fields stay calm.
- The
Popoverowns the overlay surface. ListBox, Calendar, Menu, and Dialog render flat inside it and keep their own surface only when standalone.
No breaking changes: visual refinement plus additive tokens.
- The boundary encodes role. Both roles compose one role-neutral primitive,
-
b7122c0: refactor(DST-1585): drive SegmentedControl's reduced-motion scroll from CSS
SegmentedControl now gates its selection-reveal scroll animation with the same
CSS approach as Tabs: the scroll container carriesmotion-safe:scroll-smooth
and the component'sscrollTousesbehavior: 'auto', which follows that CSS —
animating when motion is allowed and jumping instantly under reduced motion.
This replaces the previous JSwindow.matchMedia('(prefers-reduced-motion)')
check. Behavior is unchanged; the initial mount reveal stays instant. No API
change. -
b7122c0: fix(DST-1628): apply the disabled state to the
Slidertrack, fill, thumb, and value, dim the description text of disabledSelectList,ListBox, andMenuitems, and show the not-allowed cursor on disabled items. The Slider slots drove their disabled styles off the baredisabled:variant on plain<div>s, where it never matches, so they now key offgroup-disabled/field:. The item description slots hardcodedtext-secondary, which overrode the inherited disabled color, so they now addgroup-disabled/option:text-disabled. DisabledSelectListandListBoxitems still showed the pointer cursor because the interactivedata-selection-mode:cursor-pointerutility outrankeddisabled:cursor-not-allowedon source order, so the selectable cursor is now gated behindnot-disabled:.Menugained thedisabled:cursor-not-allowedit was missing. -
b7122c0: fix(DST-1630): match the Panel collapsible header caret to the Accordion chevron. It rendered at the default 24px in the foreground color, while Accordion uses a 16px
text-secondarycaret, so the two collapsible patterns looked inconsistent. The Panel caret now renders at 16px and its color is driven by a new themeablecollapsibleIconslot (defaulting totext-secondaryin the RUI theme). -
b7122c0: fix(DST-1631): keep the
RangeCalendarwithin its container on small screens. Each month carried a hardmin-w-[250px]with no breakpoint guard, so belowsm(where the months stack) the calendar could not shrink and overflowed a narrow Panel. The floor is now gated tosmand up (min-w-0 sm:min-w-[250px]), letting the flexible-cell grid shrink to fit while the desktop two-across layout is unchanged. -
b7122c0: fix(DST-1632): center the section
Loadertogether with its label and give theTabledrag handle edge spacing.The section
Loadersized its container to a fixed square, so a labelled loader overflowed the box and the section wrapper centered the box instead of the spinner-and-label group. The spinner now carries the fixed size and the container is content-sized, so the whole group centers as one. TheTabledrag cell had no padding, leaving the grip flush against the row edge. It now uses the shared cell edge padding and its column matches the checkbox column width, so the grip lines up with its header and the first cell. -
b7122c0: feat(DST-1634): standardize
Inputtrailing-action alignment and makesize="icon"a publicButtonAPI.The
Inputleading icon is clamped to 16px so it no longer overlaps the placeholder. Every trailing action (clear button, chevron, loading spinner, or a custom icon button) now sits in a control-sized centered box flush to the edge, so its icon aligns at the same inset as the leading icon acrossInput,SearchField,ComboBox,Autocomplete,TagField, andDatePicker.Button'ssize="icon"is now public and documented as the way to build an icon button, composing with anyvariant(for examplevariant="ghost" size="icon").Migration:
variant="icon"was never a real Button variant. It silently rendered a default button, so any usage from older docs should switch tosize="icon"(with a variant, for examplevariant="ghost" size="icon"). -
b7122c0: fix(DST-1636): simplify the Accordion
iconslot topointer-events-none text-secondary, matching Panel'scollapsibleIcon. Drops the redundantshrink-0(already baked intoMorphCaret) and the obsoletetransition-transform duration-250(the caret morphs its pathdrather than rotating). No visual change. -
b7122c0: feat([DST-901]): styleProps for
width,maxWidth,height,space,spaceX,spaceY,pr,pl,pt,pbnow accept both numeric scale values (4) and their string equivalents ("4"). The public types are now declarative (Scale | Fraction | WidthKeyword, etc.) instead of being derived from the internal class-name maps.Components that previously resolved
width,maxWidth, andheightvia class-name lookup (Form, Calendar, legacy Table column header / select-all cell, Slider, Scrollable, Switch, Grid) now resolve them through CSS custom properties (createWidthVar/createHeightVar) targeting--width,--max-width,--height. Those variables — along with--container-widthand--field-widthalready used byFieldBase— are registered as non-inheriting (@property … inherits: false) in the RUI theme so they cannot leak into descendants.createWidthVargained support for the previously missing keywords (svh,lvh,dvh,px,container), and a newcreateHeightVarhelper was added. Both share a common factory and a base keyword set, so they remain trivially in sync.The runtime class-name maps
width,maxWidth,height,gapSpace,paddingSpace,paddingSpaceX,paddingSpaceY,paddingRight,paddingLeft,paddingTop,paddingBottomare no longer exported from@marigold/system. These were internal utilities consumed only by@marigold/components. Use the prop types (WidthProp,HeightProp, …) and the CSS-var helpers (createWidthVar,createHeightVar,createSpacingVar) instead. The corresponding TypeScript prop types are unchanged. -
b7122c0: fix(DST-1354): restore collapsing
Table.EditableCelledit triggerThe overlay/ring affordance introduced in #5250 (DST-1275) did not read as editable in user testing: sighted users did not associate the hover ring with inline editing, and there was no discoverable trigger for keyboard or touch. This change reverts that approach and restores the explicit pencil edit button.
The trigger collapses to zero layout space at rest (
w-0 overflow-hidden) and expands on row hover or keyboard focus, so static layout remains clean while the affordance is discoverable the moment the user interacts with the row. When expanded, the wrapper switches tooverflow-visibleso the button's focus outline is not clipped. The cell itself stays clickable as a touch target. Enabled editable cells always truncate their content to stay aligned with column headers and match the single-line editing controls; disabled cells behave like a regularTable.Cell. -
b7122c0: fix(DST-1352): use correct outline for focus + error state in compound fields
-
7a122c7: fix(DSTSUP-269): wrap long unbreakable option labels inside the list instead of letting them widen the item
A listbox item is a grid, so a label without any break opportunity (a long word with no spaces) set the automatic minimum of the label track to its own width and pushed the item out of a narrow
Select,ComboBox,Autocomplete,TagField,ListBoxor the Calendar's preset list. Items now break anywhere, matching how labels with spaces already wrapped. -
b7122c0: chore(DST-1364): migrate
ListBoxitem label/description styling off descendant selectorsListBoxnow exposeslabelanddescriptionas first-class theme entries, andListBox.Iteminjects their classNames into react-aria'sTextContext(merging rather than replacing, so RAC'saria-describedbywiring is preserved) instead of styling[slot=description]via a descendant selector onitem. This also benefitsSelect.Option,ComboBox.Option, andAutocomplete.Option, which re-exportListBox.Item.The
Themetype in@marigold/systemnow requireslabelanddescriptionkeys on theListBoxrecord, so custom themes implementingListBoxmust add these entries. No public API change in@marigold/components; visually identical exceptdescriptionnow explicitly setsfont-normal(parity withSelectList). -
b7122c0: Give Menu items and the FileField drop zone a real keyboard focus indicator.
Both signalled focus with a background wash alone — 1.11:1 for a menu item, ~1.05:1 for the drop zone — well under the 3:1 WCAG 1.4.11 asks of a state indicator. In a menu the wash is doubly unreliable, since focus follows the mouse there and the same wash fires on hover.
Adds
ui-state-focus-item, the missing counterpart toui-state-focus.ui-state-focusis a two-part indicator: the border flips to full-opacity--color-ringand the outline is a soft halo around it. On an element with no border to flip it degrades to the halo alone, which measures 2.08:1.ui-state-focus-itemis for those borderless rows — a full-opacity inset ring (4.97:1 over the focus wash, 3.14:1 on the contrast ground), inset so overflow-hidden overlays like Popover cannot clip it.Only keyboard focus changes. Hover, selection and pointer interaction are untouched.
-
b7122c0: fix([DST-1408]): drop
position: relativefrom body in theme-rui preflightthemes/theme-rui/src/preflight.csspreviously set
body { position: relative; overflow-x: clip }to contain
@react-aria/live-announcer's portal. Empirically that containment
isn't needed: the live-announcer node is a 1×1 px element with
overflow: hiddenandclip-path: inset(50%), so its content cannot
expand the document's scrollable area in any state — a synthetic
test togglingbody.positionbetweenrelativeandstaticwith
the announcer mounted produces identicalbody.scrollWidth/scrollHeight.Meanwhile,
position: relativeon<body>makes it the containing
block for absolute portals (Tooltip, Popover, Menu, Select dropdown).
React-aria'suseOverlayPositionthen takes a special-case branch
that adds the page's scroll offset on top of itself when computing
"available space above the trigger", producing a near-zero number
even when the viewport has hundreds of pixels of headroom. Every
overlay with atopplacement (notably<Tooltip>) flips to
bottom, and forcingplacement="top"positions the overlay far
off-screen because the same math is broken in both directions.Removing
position: relativewhile keepingoverflow-x: clip
restores correct overlay placement without losing the defensive
horizontal-scroll guard.clipdoes not establish a containing
block for absolute descendants (per CSS Overflow Module Level 3), so
the bug cannot reappear from this rule alone — but the file's
comment now warns thatposition,transform,contain,
filter,backdrop-filter, orwill-change: transformon<body>
would re-introduce it. -
b7122c0: Restructure
tokens.cssand fix three token defects. The public token surface is unchanged — every token consumers can reference still resolves to the same computed value.- Fix: the pre-compiled bundle force-generated the wrong palette.
styles.cssdeclared@source inline('{bg}-{stone}-…'), but the theme's palette ischarcoal. Consumers writingbg-stone-500were getting Tailwind's default stone, never a theme color, andbg-charcoal-*did not exist in the bundle at all. The line went stale whencharcoalwas introduced and the theme's ownstoneutility usages were migrated without updating it. The bundle now ships the 11bg-charcoal-*utilities and no longer ships the 9bg-stone-*ones — the only consumer-visible removal in this release. - Fix:
<Panel p="collapsed">referenced an undefined variable.collapsedis a member ofInsetSpacingTokensandresolveInsetAxesappends an axis suffix to every non-numeric token, so it resolved tovar(--spacing-collapsed-x), which was never declared — leaving padding at0by accident rather than by design. Adds--spacing-collapsed-x/--spacing-collapsed-y. --color-borderis now translucent (charcoal-950 / 14%) instead of a fixed lightness. This is the one visual change in the release. A fixed lightness made a divider's weight depend on its ground: the same token measured 1.352:1 onsurface, 1.297:1 onmutedand 1.222:1 onbackground, so a rule in the Sidebar read 11% weaker than the same rule in a Table. At 14% alpha the three land within 0.003 of each other and within 0.007 of the old value onsurface, so dividers on white are unchanged to the eye and the ones on tinted grounds tighten up. Alpha picked by sweeping 0.04–0.30 and sampling rendered pixels on all three grounds. Adds a private--ui-divider-solid(no utility, not a Tailwind namespace) for the two recipes that read the divider's channels rather than its appearance — Drawer'scalc(l - 0.1)and SectionMessage's status-accentcolor-mix— so both render exactly as before rather than inheriting an alpha that would compound.--color-softand--shadow-elevation-overlaynow take their hue from the palette via relative color syntax instead of hardcoding54. Computed values are identical today; a rebrand now carries the divider color, the soft cap and the shadow's warm cast with it instead of leaving them behind. Note this covers the token definitions: on the pre-compiled pathtokens.cssis imported withtheme(inline), so palette utilities such asbg-charcoal-500compile to inlined literals rather thanvar(--color-charcoal-500)and are not retinted by overriding the token at runtime.--contrast-*(7 vars) are no longer theme tokens. They are single-use arguments to theui-contrastutility and now live inside it. They were never in a Tailwind namespace, so they never generated a utility — private implementation detail sitting on the public:rootsurface.- Regroups the file so the five boundary tokens sit together, splits radius out of the surface section, renames the
TYPOGRAPHYsection toFONT FAMILIES(it holds no type scale), and trims comments that restated their own values or had gone stale.
- Fix: the pre-compiled bundle force-generated the wrong palette.
-
6cfcea4: fix(DST-1686): make
controla ground-adaptive track fill, and give theSliderrail the same token asSwitchandSegmentedControlThe Slider rail was the wrong token, painted twice. It used
bg-border— the token for structural lines (dividers, grid lines, table rules) — where theSwitchgroove and theSegmentedControltrack both usebg-control. On top of that,Sliderapplied itstrackstyle to two exactly-overlapping elements (theSliderTrackand an inner raildiv), so the translucentbg-bordercomposited with itself and the rail rendered at ~0.26 effective alpha instead of 0.14 — measuring#c0bfbeon white where the token specifies#dddddc. The redundant inner element is gone; theSliderTrackitself is the rail. Geometry is unchanged (both wereh-2at the same position) andtouch-none,select-noneand the disabled cursor stay on the interactive track element.controlis now translucent (charcoal-950 / 16%, was the opaquecharcoal-300). A track is not painted on one known background — it appears on a white Card, the gray page ground, amutedfill, and inside a hovered Table or ListBox row — and a fixed palette step drifts across those. charcoal-300 measured 1.53:1 on white but only 1.21:1 inside a hovered row, where the groove half-dissolved into the row it sits in. At 16% the four grounds land within 0.02 of each other (1.41–1.43:1), so a track weighs the same wherever it goes. Same rationale asborderin DST-1672.The three tracks (
Switch,SegmentedControl,Slider) read a touch lighter on white surfaces as a result:#d8d8d7rather than#d4d0ce. The trade-off is that the white Switch thumb and SegmentedControl indicator now vary against the track by ground (1.43:1 on white, 1.79:1 on a hovered row) where the opaque step held a flat 1.53:1 — track-vs-ground and thumb-vs-track cannot both be constant while the thumb is opaque, and ground legibility wins because it decides whether the control reads as a control at all.The
SegmentedControlindicator is larger. The frame around the selected thumb went from 4px to 3px (inset-y-[3px]on the indicator,p-[3px]on the list), so the thumb is 30px tall instead of 28px. Since the thumb's own 1px rim occupies the innermost pixel of that frame, 2px of bare track is what you see.The
SegmentedControlthumb's focus ring is now the sharedui-state-focus. It previously hand-rolled only the outline, so it missed the other half of that utility — firming--ui-border-colorto the opaque ring colour — and read noticeably lighter than every other focused control. A focused thumb and a focusedInputnow resolve identically: 3pxoutline-ring/50atoutline-offset-0, plus a 1px opaqueoklch(0.52 0.008 54)rim. The ring stays outside the thumb, and 3px is the minimum frame that clears it: the list is a scroll container clipping at its padding box, so that padding is the only room an outset ring can grow into. Below 3px the first and last thumbs get a visibly shaved ring.The
SegmentedControltrack's corners are now concentric with the indicator's. Both used the sharedrounded-surface, but two rounded rectangles nested with a gap only look like parallel arcs when the outer radius is the inner radius plus that gap. With the thumb at 8px and 3px of frame around it, the track needs 11px; at 8px its corner read visibly tighter than the arc it frames. The track is nowcalc(var(--radius-surface) + 3px)— derived from the token, so it stays concentric if the radius is retuned. This is a deliberate exception to usingrounded-surfaceeverywhere.The
SegmentedControlindicator's resting rim is re-derived from the track.ui-framedraws its rim as an outset ring, so the indicator's rim lands on the track and composites overbg-control— it reads denser than the samecontrol-bordertoken does on a field. The old compensation subtracted a flat 0.08, hand-tuned against the previous opaquecharcoal-300track, which left the rim at an effective 0.31 against a field's 0.26. It is now derived from the track's own alpha (--control-alpha, newly exposed on the token), so a resting indicator rim and a restingInputedge render the same on every ground, and retuning the track cannot silently detune the rim.If you use
bg-controlin your own code, note that it is now translucent: two stacked elements that both carry it composite into a darker track than the token specifies, and anything that paints a translucent edge over a track has to account for the track underneath it. -
b7122c0: feat(DST-1370): migrate
ContextualHelp,SectionMessage, andEmptyStateto the slot-configuration pattern-
SectionMessage.Titlenow renders a semantic heading (<h3>by default) instead of a<div>, fixing an a11y gap. The level is configurable via the newheadingLevelprop on<SectionMessage>. When a title is present, the container becomes arole="group"labelled by the title viaaria-labelledby. -
New
<SectionMessage.Description>sub-component for a short summary between title and content. -
ContextualHelp.Titlenow usesslot="title", so the popover dialog gets a properaria-labelledby. The title tag changes from<h3>to<h2>(same asDialog.Title); visual appearance is unchanged. -
New
<ContextualHelp.Description>sub-component. -
The
Themetype now requires adescriptionkey on theSectionMessageandContextualHelpstyle records; themes defining styles for these components must add it. -
EmptyState'stitlenow renders as a semantic heading (<h3>by default, configurable via the newheadingLevelprop), and itsdescriptionrenders through the<Description>primitive (same DOM as before, now sitting 4px below the title to match the description rhythm of the other components). The flat-props API is unchanged. -
All three roots now also publish a
ButtonContext, completing the slot-configuration set. It scopes action buttons (e.g. those placed inSectionMessage.Content, theEmptyStateaction, orContextualHelpcontent) to a clean baseline so they never inherit a surrounding container's button cascade (such as aPanel.Header's ghost/small look). No variant or positioning is imposed, so existing usage renders unchanged.
-
-
b7122c0: feat(DST-1366): introduce slot-configurable primitives
Adds three text-bearing role primitives —
Title,Description,TextValue— and three action primitives —ActionButton,ActionGroup,ActionMenu— that participate in slot-keyed context. Text/heading slots use React Aria'sHeadingContext/TextContextdirectly; action slots use Marigold-owned contexts (ActionButtonContext,ActionGroupContext,ActionMenuContext) consumed viauseContextProps.Titlewraps RAC's<Heading>withslot="title"andlevel={2}as defaults, both overridable byHeadingContext. Thelevelprecedence is default ← context ← local, so a container can publish{ level: 4 }and drive a stretch of nested<Title>s to<h4>without each call site setting it.DescriptionandTextValueforward straight to RAC's<Text>withslot="description"andslot="label"defaults respectively, letting<Text>consumeTextContexton its own. None of the three carry typography props. Styling cascades from the surrounding container (or selection item) viaHeadingContext/TextContext. Consumers drop these into containers without anyslotwiring. The container provides level, layout (e.g. a grid area), size, variant, color, and any other styling through a singleProvider.ActionGroupis its own top-level component (own folder, own docs page, own Storybook entry) — there is noActionButton.Groupcompound. It cascadessize,variant, anddisabledto nested<ActionButton>,<LinkButton>, and<ActionMenu>triggers viaActionGroupContext, with explicit per-prop precedence:size: group wins (visual uniformity within a cluster).variant: local wins (so a single destructive action can sit inside an otherwise uniform group).disabled: local wins; the group provides the default. Writingdisabled={false}on a child re-enables it inside an otherwise-disabled group.
ActionMenuis rebuilt to compose its ownMenuTrigger+<ActionButton>+Popover/Tray+ RACMenurather than delegating to Marigold'sMenu. The trigger uses<ActionButton>so an outerActionButtonContextcascades to it. Marigold'sMenuis untouched.LinkButtonis now slot-aware: it picks upActionButtonContextandActionGroupContextso a navigating action can sit alongside<ActionButton>inside an<ActionGroup>and inherit the same cascade. Adestructive-ghostvariant is added to match<ActionButton>. Context is consumed read-only (viauseSlottedContext) to sidestep the anchor/button ref-type mismatch thatuseContextPropswould have created. The read-only consumption now also absorbsclassNamefromActionButtonContext(mirroring<ActionButton>'suseContextProps-driven className merge) so positional classes published by a parent container — e.g. a grid-area class injected viaActionButtonContext— reach the rendered anchor. This lets<LinkButton>participate in container-driven layouts the same way<ActionButton>does.The container-driven layout pattern this enables comes with a corresponding convention: positional
classNameflows through slot contexts and is absorbed at the first layout boundary.<ActionGroup>enforces the convention at its own boundary by scrubbingActionButtonContextfor its descendants — it republishes an empty value so nested<ActionButton>s and<LinkButton>s do not individually re-claim a positional class that was meant for the group as a whole. Cascading props (size,variant,disabled) still reach the children viaActionGroupContext, which they read independently. This convention scales to every future container that adopts the slot-configuration pattern.<ActionBar>'s legacy top-levelActionButtonslot is internalized and re-exposed asActionBar.Button. Existing consumers that already use<ActionBar.Button>are unaffected.Typography prep:
HeadlineexportsHeadlineSize,TextexportsTextSizeandTextVariant. The aliases aren't yet consumed by other primitives, but exposing them now lets a future typography-token PR replace runtime classes without rewriting consumer-facing prop types. -
b7122c0: Rename universal spacing token from
nonetocollapsedto avoid a Tailwind v4 collision.--spacing-noneinside@theme staticcausedleading-noneto resolve to0instead ofline-height: 1. The new namecollapsedis a semantic design term (cf. CSS margin collapse) that reads naturally in both gap (space="collapsed") and padding (inset="collapsed") contexts. -
b7122c0: fix: make Select and Menu overlay appear above Drawer on small screens
On small screens,
SelectandMenurender their options in aTray(bottom sheet). TheTrayoverlay hadz-40in the theme while theDraweroverlay usesz-50, so the tray rendered behind an open drawer and was unreachable.Moved the
z-indexfrom the theme style file into theTrayModalcomponent implementation (matching the project's z-index architecture rule), and raised it toz-50. Both theDrawerandTrayportal todocument.body; at equal z-index, DOM order determines stacking. TheTrayis always mounted after theDrawer, so it correctly appears on top. -
b7122c0: fix: register
--ui-background-color,--ui-border-color, and--ui-highlight-coloras non-inheriting custom propertiesPreviously, setting one of these variables on a themed surface (e.g. a destructive Panel overriding
--ui-border-color) would cascade the value into every nested element that also readsui-surface, tinting Inputs, Buttons, Cards, etc. with the parent's color.These three custom properties are now registered via
@property { inherits: false }, so each surface resolves its own fallback via the existingvar(..., var(--color-…))pattern and nested surfaces keep their defaults. -
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [85e9a45]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [0e2c676]
-
Updated dependencies [b7122c0]
-
Updated dependencies [a9fdcff]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [2b9df4c]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [68122ff]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [e0f9c05]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [bb23186]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [0e2c676]
-
Updated dependencies [f331a41]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [04e22ab]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [6cfcea4]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
-
Updated dependencies [b7122c0]
- @marigold/components@18.0.0
- @marigold/system@18.0.0