Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/docs/api/helpers/theme.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,17 @@ function GlobalTheme() {
The `colorMode` prop sets Bulma's `data-theme` on `<html>`, flipping the light/dark scheme globally.
Use `'system'` to follow the OS preference.

:::warning Single-mode designs should pin `colorMode`

Omitting `colorMode` preserves whatever `data-theme` is already set — and when nothing has set
one (the usual case), the scheme follows the visitor's OS: Bulma's text colors flip on a
dark-mode machine even if your design is light-only, breaking contrast against any fixed custom
backgrounds. If you support only one mode, pin it (`<Theme isRoot colorMode="light">`); if you
support both, don't hardcode surface/text colors — see
[Dark Mode & Contrast](../../guides/features/css-variables.md#dark-mode--contrast).

:::

```tsx
import { useState } from 'react';

Expand Down
58 changes: 58 additions & 0 deletions docs/docs/guides/features/css-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,64 @@ $family-primary: 'Helvetica Neue', sans-serif;
5. **Testing**: Easy to test different color schemes
6. **Debugging**: Modify values in DevTools for instant feedback

## Dark Mode & Contrast

Bulma's scheme variables (`--bulma-text`, `--bulma-scheme-main`, …) flip automatically when
`data-theme="dark"` is present — and when no `data-theme` attribute has been set at all (the
usual case for an app that never configured `colorMode`), they follow the visitor's OS
`prefers-color-scheme`. **Dark mode is effectively on by default**, even for designs that never
intended to support it.

That creates a silent contrast trap the moment you introduce your own fixed color tokens: on a
dark-mode machine, Bulma's text goes near-white while your fixed light backgrounds stay light —
white text on cream, unreadable, and invisible to you unless your own OS is in dark mode.

Apply exactly one of these rules:

**Rule 1 — single-mode design: lock the scheme.** If the design is light-only (or dark-only),
pin it at the app root so an OS preference can never invert text out from under your palette:

```tsx
<Theme isRoot colorMode="light">
<App />
</Theme>
```

**Rule 2 — both modes: never expose a fixed token to the flip.** Derive your tokens from
Bulma's scheme variables, or provide the dark-mode pair yourself:

```css
/* Preferred: your tokens track the scheme automatically. */
:root {
--my-canvas: var(--bulma-scheme-main);
--my-ink: var(--bulma-text);
}

/* Or keep custom values, but flip them for BOTH ways dark mode arrives:
the explicit attribute (colorMode="dark")… */
[data-theme='dark'] {
--my-canvas: #14251b;
--my-ink: #eef3e7;
}

/* …and the OS preference, which applies when no data-theme is set
(colorMode="system" removes the attribute): */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
--my-canvas: #14251b;
--my-ink: #eef3e7;
}
}
```

This is exactly why deriving from the scheme variables is the preferred form — one line, and
both dark-mode paths are covered automatically.

**Corollary — fixed-color surfaces need fixed-color content.** A surface that never flips (a
dark hero, a brand banner) must pin its content's colors too: filled buttons
(`color="light"`, or `color="primary" isInverted`) and explicit text colors — not thin
outlined buttons or scheme-derived defaults, which wash out when the surrounding scheme flips.

## Best Practices

### Organization
Expand Down
7 changes: 7 additions & 0 deletions skills/bestax-layout-scaffold/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ Centered; a collection of items → Card grid. For mixed requests, pick the domi
`textAlign="centered"`, and `textColor`/`bgColor` instead of `style={{ marginTop, textAlign,
color }}`. Set the app-wide icon library once with `<ConfigProvider iconLibrary="…">` at the root
rather than `library` on every `<Icon>`.
- **CTAs on a colored hero must stay legible in both schemes.** On a fixed-color surface
(`Hero color="primary"`, a dark banner), use **filled** buttons — `color="light"` or
`color="primary" isInverted` — never a thin `isOutlined` secondary: a light outline + light
label on a dark surface is low-contrast and gets worse under OS dark mode. And when the page's
design is single-mode (a fixed light or dark look), pin it at the root —
`<Theme isRoot colorMode="light">` — so a visitor's OS dark mode can't flip Bulma's text
colors out from under the fixed palette (details: the `bestax-theming` skill's contrast rules).

## References

Expand Down
7 changes: 6 additions & 1 deletion skills/bestax-layout-scaffold/examples/landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,16 @@ export default function LandingPage() {
<SubTitle size="3">
The all-in-one platform for modern teams.
</SubTitle>
{/* Both CTAs are FILLED: on a fixed-color hero a thin outlined
button (light outline + light label) reads washed out, and
worse under OS dark mode. isInverted (no isOutlined) gives a
solid white button with primary text — high contrast in both
schemes. */}
<Buttons isCentered mt="5">
<Button color="light" size="large">
Get started
</Button>
<Button color="primary" isInverted isOutlined size="large">
<Button color="primary" isInverted size="large">
Live demo
</Button>
</Buttons>
Expand Down
6 changes: 6 additions & 0 deletions skills/bestax-layout-scaffold/references/archetypes.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ pricing page. The default for "build me a site/page".
**Responsive:** `Section`s already stack vertically. The feature `Columns` collapse to one feature
per row on mobile. Use `Hero size="large"` / `"fullheight"` for a taller hero.

**Hero CTAs:** on a colored hero use **filled** buttons only — `color="light"` for the primary
CTA and `color="primary" isInverted` (solid white, primary text) for a secondary. A thin
`isOutlined` button on a fixed-color surface is low-contrast and degrades further under OS dark
mode. Single-mode page designs should also pin the scheme at the root
(`<Theme isRoot colorMode="light">`) — see the `bestax-theming` skill's contrast rules.

---

## 3. Centered single-column
Expand Down
20 changes: 20 additions & 0 deletions skills/bestax-theming/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,26 @@ For **dark mode**, pass `colorMode` to `Theme` (`'light' | 'dark' | 'system'`).
`Theme`; `'system'` follows the OS `prefers-color-scheme`. Drive it from state on the app-root
`Theme`: `<Theme isRoot colorMode={mode}>`.

## Contrast rules (dark mode is on by default)

When nothing sets a `data-theme` attribute (omitting `colorMode` preserves an existing one, but
apps that never configured it have none), Bulma follows the visitor's OS: `--bulma-text`,
`--bulma-scheme-main`, etc. flip on a dark-mode machine even if the design never intended a dark
theme. Custom fixed tokens (`--my-canvas: #f6f4ec`) do **not** flip — producing near-white Bulma
text on the author's fixed light background. Apply exactly one of these rules whenever custom
color tokens or fixed-color surfaces exist:

- **Single-mode design → pin the scheme.** `<Theme isRoot colorMode="light">` (or `"dark"`), so
an OS preference can never invert text out from under the fixed palette.
- **Both modes → no exposed fixed tokens.** Derive custom tokens from scheme variables
(`--my-canvas: var(--bulma-scheme-main)`) — or flip them yourself under **both** dark-mode
paths: `[data-theme='dark']` **and** `@media (prefers-color-scheme: dark)` scoped to
`:root:not([data-theme])`, since `colorMode="system"` removes the attribute (snippets in
`references/css-variables.md`).
- **Fixed-color surface → fixed-color content.** On a surface that never changes (a dark hero,
a brand banner), pin the content's colors too: solid/filled buttons and explicit text colors,
never scheme-derived defaults or thin outlines that depend on the flipping scheme.

Reach for the helper props (`color` / `textColor` / `bgColor` / `colorShade`, `textSize`,
`textWeight`, `fontFamily`) to apply themed colors and type to individual components.

Expand Down
49 changes: 49 additions & 0 deletions skills/bestax-theming/references/css-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,55 @@ Under dark mode Bulma flips the scheme/text/border/background lightness variable
`Theme isRoot` or `:root` still apply on top, because they set the hue/saturation/lightness
channels directly.

### The single-mode contrast trap

Because the OS preference applies whenever no `data-theme` attribute is set — the default state
of every app that never configured `colorMode` — a light-only design silently breaks for any
dark-mode visitor: Bulma's text goes near-white while author-defined fixed tokens stay light —
white text on cream. The failure is invisible unless the author's own OS is in dark mode.

**If the design is single-mode, pin the scheme** so text can't flip out from under the palette:

```tsx
<Theme isRoot colorMode="light">
<App />
</Theme>
```

**If both modes are supported, never expose a fixed custom token to the flip** — derive it from
scheme variables, or flip it yourself:

```css
/* Preferred: track the scheme automatically. */
:root {
--my-canvas: var(--bulma-scheme-main);
--my-ink: var(--bulma-text);
}
/* Or, when custom values must be kept, provide the dark pair for BOTH
ways dark mode arrives — the explicit attribute (colorMode="dark")… */
[data-theme='dark'] {
--my-canvas: #14251b;
--my-ink: #eef3e7;
}

/* …and the OS preference, which applies when no data-theme is set
(colorMode="system" removes the attribute): */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
--my-canvas: #14251b;
--my-ink: #eef3e7;
}
}
```

Deriving from scheme variables is preferred precisely because it covers both dark-mode paths
with no extra selector.

The same reasoning applies to **fixed-color surfaces** inside either kind of page (a dark hero,
a brand banner): content sitting on a surface that never flips must use pinned colors — filled
buttons and explicit text colors — not scheme-derived defaults (see the layout skill's hero CTA
rule).

## `Theme` props (named)

Color trios: `primaryH/primaryS/primaryL`, `linkH/linkS/linkL`, `infoH/S/L`, `successH/S/L`,
Expand Down
Loading