Color modes

Color modes are token-level. The provider sets data-bs-theme on its wrapper; a [data-bs-theme=‘dark’] block overrides the surface tokens, and every component follows because it reads those tokens.

The mechanism

The library defines its tokens twice:

  • On the root, the light values (--bs-body-bg, --bs-body-color, --bs-border-color, the per-color emphasis tokens, …).
  • Inside a [data-bs-theme='dark'] block, the dark overrides for those same tokens.

BootstrapStyledProvider renders a wrapper with data-bs-theme={colorMode}. When the mode is dark, that attribute activates the dark block for the wrapper and everything inside it. Because the switch is a scoped attribute selector — not a global change — nesting a second provider re-scopes a subtree independently.

tsx
<BootstrapStyledProvider colorMode="dark">
{/* everything here reads the dark tokens */}
</BootstrapStyledProvider>

Side by side

Two providers, same markup, different colorMode — the surface, text, borders, and controls differ only in their tokens:

What flips

The mode changes the neutral surface and emphasis tokens; the eight contextual colors stay the same hue (their emphasis tints adjust for contrast):

TokenChanges with the mode
—bs-body-bg · —bs-body-colorPage background and text.
—bs-secondary-bg · —bs-tertiary-bgThe raised / sunken surface layers.
—bs-border-colorDefault border color.
—bs-emphasis-colorHigh-contrast text.
—bs-{color}-text-emphasisPer-color emphasis text, re-tuned for the dark surface.
—bs-link-colorLink color, adjusted for contrast.

Customizing the dark palette

The dark token values live under theme.dark and can be overridden with createTheme, just like the light tokens:

tsx
import { createTheme } from '@metatoy/bootstrap-styled';

const theme = createTheme({
dark: {
  body: { bg: '#0d0e0c', color: '#ece9f1' },
},
});

Portaled overlays

Overlays (Modal, Dropdown, Tooltip, Popover, Offcanvas) render through a portal into document.body, outside the provider wrapper. The library re-establishes the data-bs-theme scope on the portal root from the active mode, so overlays match — no extra work. For the app-level toggle pattern and the useColorMode hook, see Dark mode.