No guard catches a style-driving prop that never reaches themeProps on a closed axis — Banner and ChatComposer still ship an unthemeable elevation
@cixzhang is already working on this.
Since Sep 2, 2026.
- 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
Banner — elevationStyles[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.tsenforces a subset policy — docs may list more targets and visual props than the source passes. It therefore validates what is passed tothemeProps(), and a prop the source never passes is invisible to it.packages/core/src/theme/extensibleAxes.test.tsdoes check "this prop drives style and never reachesthemeProps" — but only for extensible*VariantMapaxes.Elevationis 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.