Skip to content

feat(Item): add default/outline/muted surface variants#4277

Open
AKnassa wants to merge 1 commit into
facebook:mainfrom
AKnassa:ak-4179-item-variants
Open

feat(Item): add default/outline/muted surface variants#4277
AKnassa wants to merge 1 commit into
facebook:mainfrom
AKnassa:ak-4179-item-variants

Conversation

@AKnassa

@AKnassa AKnassa commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

What this does

Gives Item a variant prop with three looks — default (filled card background with a border), outline (border only), and muted (soft grey background, no border). It's opt-in: an Item without the prop looks exactly like it does today.

Refs #4179 — left open deliberately, since the issue is assigned and carries API questions this PR shouldn't close on its own.

Why

Today an Item is always transparent and only paints a background when you hover or select it. There's no way to give one a standing border or a filled surface the way Card can. This adds that, reusing Card's own colour and border tokens so the two stay visually consistent across themes — but at Item's smaller --radius-element corner radius, since an Item is an element, not a container.

What changed

  • New variant prop on Itemdefault, outline, or muted. Leaving it off keeps the current transparent look, which is what lists and menus want.
  • Hover and selection now sit on top of the variant surface instead of replacing it. This one isn't cosmetic — see the note below.
  • The border is drawn inside the padding. Its width is subtracted from whichever density padding is in play, so the total inset still lands on the spacing scale and a bordered Item is the same overall size as a borderless one.
  • variant is reflected as data-variant so themes can target each look.
  • Documented in Item.doc.mjs (English, Chinese and dense), plus two new Storybook stories.

The interesting bit

--color-background-muted and --color-overlay-hover are the same value in light mode (#0536590C). So the obvious implementation — having the variant paint background-color — means a muted Item gets no visible hover at all, and a default Item loses its solid card surface the moment you hover it.

The fix: the variant owns background-color, and the four interaction states (hover, pressed, highlighted, selected) moved to a background-image gradient layer, which CSS paints above the background colour. That's the same technique already used by TreeListItem, AvatarGroupOverflow and ChatComposerDrawer.

Because all four still write the same property in the same order, their precedence is unchanged — an Item with no variant renders byte-identically to before. There's a test pinning exactly that.

How to see it

Storybook → Core / ItemVariants and Variants (interactive). Hover the muted row: it visibly darkens. Before this change it wouldn't have
01-variants-light
02-variants-dark
03-interactive-light-resting
04-interactive-light-hover-MUTED
05-interactive-light-hover-DEFAULT
06-interactive-dark-hover-MUTED

Testing

  • 51 Item tests, 14 of them new. They read the real compiled CSS out of document.styleSheets rather than just checking the prop arrived, so they assert actual declarations.
  • All 14 verified load-bearing by mutation testing — 9 deliberate breakages introduced, 9 caught.
  • Full suite green apart from pre-existing packages/cli timeout flakes present on main at the same commit; zero failures in packages/core. Lint and typecheck clean.

Open questions for review

Flagging these honestly rather than assuming — #4179 leaves them to the spec, and I've made a judgement call on each:

  1. Should default become Item's default? I kept the no-variant look transparent (the issue leans this way too), so nothing regresses in the many lists and menus using Item today. default is opt-in.
  2. What background should outline have? I used none — border only, fully transparent — so it composes over any surface. The alternative was --color-background-surface.
  3. Per the Component Specification Protocol, a new prop on a core component wants API agreement on the issue before implementation. Treat this PR as a concrete proposal to react to, not a fait accompli — happy to reshape or drop it if the API should land differently.

Adds `variant` to Item, using Card's background and border tokens at
Item's element radius (--radius-element, not Card's container radius):
`default` paints the card background with a visible border, `outline`
draws the border only, and `muted` paints the muted background with no
border. The border is drawn inside the padding — its width subtracted
from whichever density padding is active — so the total inset stays on
the spacing scale.

`variant` is opt-in. An Item without it renders exactly as before, since
a standing border would be surprising in the lists and menus Item mostly
appears in.

Interaction states now composite over the variant surface instead of
replacing it. The hover, active, highlighted and selected overlays moved
from background-color to a background-image gradient layer — the
technique already used by TreeListItem and AvatarGroupOverflow — leaving
background-color to the variant. This is load-bearing, not cosmetic:
--color-background-muted and --color-overlay-hover are the same value in
light mode, so painting hover as a background color gave a `muted` Item
no hover feedback at all and stripped the opaque surface off a `default`
Item. Overlay precedence is unchanged, so a no-variant Item composites
byte-identically to before.

Item does not use container(), so Card's --container-padding-* inset
technique was unavailable; each density instead publishes its padding as
local custom properties that the bordered variants subtract from. The
inset uses padding longhands because StyleX gives shorthands lower
specificity, so longhands win regardless of rule order.

Issue facebook#4179
@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview, Comment Jul 24, 2026 1:04am

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Jul 24, 2026
@github-actions github-actions Bot added community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge needs:design-review Affects visuals — Design should review labels Jul 24, 2026
@AKnassa
AKnassa marked this pull request as ready for review July 24, 2026 01:07
@github-actions

Copy link
Copy Markdown
Contributor

PR Analysis Report

Modified Components

Item
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 386 -
Complexity N/A Very High (42) -

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.7KB 0B

Accessibility Audit

Status: 2 accessibility violation(s) found — 1 critical, 1 serious.

Item - 2 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 3/11 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
  • 🔴 critical: Ensure an element's role supports its ARIA attributes
    • Rule: aria-allowed-attr · Affects 2/11 stories · Learn more
    • WCAG: 4.1.2 (Level A)

Generated by PR Enrichment workflow | View full report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant