facebook / facebook/astryx

[Bug] Toast action theming cannot distinguish the root app theme

Open
#5,503 0 comments 0 reactions 1 assignee Claimed by @rubyycheung View on GitHub
bug
Dominant language
TypeScript
Stars
13.1k
Forks
1.1k
Avg merge
1d 14h
Merged PRs (30d)
669

Description

## Summary

`Toast` renders its content inside `MediaTheme mode="auto"`. That is useful for foreground contrast, but it means component theming sees the Toast's media-surface mode rather than the application's root theme.

When an application intentionally uses a dark Toast surface in both root light and root dark themes, both Toasts resolve as dark media surfaces. A component override, `light-dark(...)`, or a custom Button variant therefore selects the same branch in both application themes. There is no supported theme-level way to style a Toast action differently by root app theme.

The consumer currently has to read root `useTheme()` in product code and select between existing public Button variants at runtime.

## Versions

- `@astryxdesign/core@0.5.0`
- `@astryxdesign/cli@0.5.0`
- `@astryxdesign/theme-neutral@0.5.0`
- React 19
- Chrome on macOS

## Relevant public contracts

The documented public theming surface supports:

- `defineTheme.components.toast`
- Toast's `type` target, including `type:error`
- `defineTheme.components.button`
- Button's `size` and `variant` targets
- Public Button variants: `primary`, `secondary`, `ghost`, and `destructive`

Toast does not document a public component variable or target for styling `endContent` according to the root application theme.

## Reproduction

1. Render an app under an Astryx root `Theme` that can be light or dark.
2. Show a Toast whose surface is intentionally dark in both root modes—for example, one dark surface in the light app theme and a different dark surface in the dark app theme.
3. Put a Button in `endContent`.
4. Try to keep `secondary` in root light mode and use `ghost` in root dark mode through `defineTheme`, `light-dark(...)`, or a custom Button variant.
5. Observe that Toast's nested `MediaTheme mode="auto"` resolves as dark in both cases, so both application themes select the same media-theme branch.

Minimal shape:

```tsx

}
onDismiss={() => {}}
/>

```

The only working consumer-side implementation is:

```tsx
const {mode} = useTheme();
const actionVariant = mode === 'dark' ? 'ghost' : 'secondary';
```

## Approaches evaluated

### 1. Toast component override

Using `defineTheme.components.toast` is correct for Toast background, border, and `type:error` styling, but it does not provide a root-theme-aware action target.

### 2. `light-dark(...)` in a custom action variant

This does not solve the problem. `light-dark(...)` follows the local `color-scheme`, and Toast's `MediaTheme` sets that scheme from the Toast surface. Because both Toast surfaces are dark, both root themes select the dark branch.

### 3. Global dark-media Button override

Overriding every secondary Button inside dark `MediaTheme` would affect unrelated Buttons in other dark media surfaces. It is too broad and changes component semantics outside Toast.

### 4. Private or invented Toast custom properties

Inventing variables such as `--toast-action-*` is outside the documented public component-variable contract and couples consumers to implementation details.

### 5. Consumer-side root `useTheme()` branch

This works and uses only public APIs, but it moves a visual/theming distinction into product runtime code. Every consumer with this requirement must repeat the same branching logic.

## Expected behavior

Astryx Core should provide a supported, bounded way for Toast action theming to retain the root app-theme distinction while `MediaTheme` continues to own local foreground contrast.

Possible public shapes include:

- a root-theme semantic available to component theme targets;
- a Toast action/end-content variant target with root-theme branches; or
- another explicit API that distinguishes root application mode from nested media mode.

The exact API is up to Core. It should avoid globally changing Buttons in all dark media surfaces and should not require consumers to reach into private CSS variables or component internals.

## Privacy and data-minimization requirement

No sensitive-data leak has been observed. This is a preventive requirement for any new API, diagnostic, or telemetry added while solving the gap.

Only bounded semantic values should cross the theming or diagnostic boundary, such as:

- `light | dark`;
- a documented mode enum; or
- a bounded public variant name.

The implementation must not expose, serialize, inspect, or log:

- Toast body content;
- arbitrary `endContent`;
- action labels or hrefs;
- user-authored content;
- user, account, or session identifiers;
- URLs or navigation targets;
- tokens or credentials;
- serialized React props;
- arbitrary DOM attributes or text; or
- component children.

Core tests should cover both root theme modes and include a negative assertion that content and arbitrary props are not surfaced through the theming, diagnostics, or telemetry boundary.

## Acceptance criteria

1. Toast action styling can distinguish root light and root dark modes even when the Toast's nested `MediaTheme` is dark in both.
2. A consumer can express root-light `secondary` and root-dark `ghost` without product-owned theme branching.
3. `MediaTheme mode="auto"` continues to provide correct foreground contrast.
4. The behavior is scoped to Toast actions and does not restyle unrelated Buttons in dark media surfaces.
5. The API uses documented public contracts—no private CSS variables, swizzling, or component-internal selectors.
6. Any diagnostics or telemetry use a closed semantic vocabulary and do not expose or log content, labels, hrefs, identifiers, URLs, tokens, serialized props, arbitrary DOM data, or children.
7. Core tests cover both theme modes and the data-minimization requirement.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.