facebook / facebook/astryx

No guard catches a style-driving prop that never reaches themeProps on a closed axis — Banner and ChatComposer still ship an unthemeable elevation

Open
#5,556 2 comments 0 reactions 1 assignee View on GitHub

@cixzhang is already working on this.

Since Sep 2, 2026.

bug
Dominant language
TypeScript
Stars
13.2k
Forks
1.1k
Avg merge
1d 14h
Merged PRs (30d)
669

Description

Summary

Five core components take an elevation prop that selects between StyleX style objects. Three of them now reflect it as a theme target (Card #5491, ButtonGroup, Button #5552). Banner and ChatComposer still do not, so a theme cannot reach the axis on either one — and, more importantly, nothing in the repo catches that class of defect, which is why it shipped five times.

Filed as a system-level finding rather than two component bugs, because the remedy that matters is the guard, not the two call sites.

The two remaining components

BannerelevationStyles[elevation] is applied to the root:

// packages/core/src/Banner/Banner.tsx
elevationStyles[elevation],            // ~558
...
themeProps('banner', {container, status}),   // ~569 — no `elevation`

ChatComposer — the same shape on the composer body:

// packages/core/src/Chat/ChatComposer.tsx
themeProps('chat-composer', {density}),   // ~460 — no `elevation`
...
elevationStyles[elevation],               // ~474

Effect: a theme author who raises a Banner or a ChatComposer cannot restyle that shadow at all. Same user-visible consequence T6 describes for Button.

Note that the two are not a copy of the Button fix. On both components the element that paints the shadow carries no stable astryx-* class of its own, so there is no target for the axis to hang on — the repair is closer to the popup-target move than to adding a key to an existing themeProps call. Bounded direction only; the shape is deliberately not prescribed here.

Why no guard caught it

Two theming guards exist and this class falls between them:

  • packages/core/src/theme/themingTargets.test.ts enforces a subset policy — docs may list more targets and visual props than the source passes. It therefore validates what is passed to themeProps(), and a prop the source never passes is invisible to it.
  • packages/core/src/theme/extensibleAxes.test.ts does check "this prop drives style and never reaches themeProps" — but only for extensible *VariantMap axes. Elevation is a closed union, so it is out of scope.

So the rubric's T6 ("any prop that selects between StyleX style objects is in the themeProps() call on that element") is enforced for extensible axes and unenforced for closed ones. T6's own row says as much: "semi — grep; no lint rule, and historically the single most frequent finding."

What would close this

Something that fails when a <name>Styles[prop] lookup appears inside a stylex.props() call and prop is absent from the sibling themeProps() on the same element, regardless of whether the axis is extensible. Whether that is best as an extension of extensibleAxes.test.ts, a new Vitest guard, or an ESLint rule is open — the AST shape is the same one the grep in T6 describes.

Evidence

Found by the nightly component audit of core/Button (#5552) and confirmed by the review loop on that PR, which checked the class rather than the case. Verified by reading all five components on 58f9542; SelectableCard and ClickableCard are clean because they delegate the painting to Card, which carries the axis.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.