Skip to content
design
Foundations

Themes

Four palettes preserve control and instrument meaning from a dark cockpit to direct sunlight.

On this page

Display settings

Theme changes presentation; the control labels and behavior keep their meaning.

Switch a local theme, edit the field and open the popover. The specimen and its overlay share the selected theme.
Code
import { useState, type Dispatch, type SetStateAction, type ReactElement } from "react";
import { Button } from "@bosunkit/components/button";
import { Input } from "@bosunkit/components/input";
import { Stack } from "@bosunkit/components/stack";
import { Inline } from "@bosunkit/components/inline";
import { Label } from "@bosunkit/components/label";
import { Popover } from "@bosunkit/components/popover";
import { Text } from "@bosunkit/components/text";
import { Heading } from "@bosunkit/components/heading";
import { PresentationScope } from "@bosunkit/provider";
import { THEMES, type Theme } from "@bosunkit/tokens/theme";
import { useId } from "react";
import "../control-styles/example.css";

export const description: string =
  "Switch a local theme and open the popover to compare controls and their overlay without changing the page theme.";
export default function Example(): ReactElement {
  const [theme, setTheme]: [Theme | "inherit", Dispatch<SetStateAction<Theme | "inherit">>] = useState<
    Theme | "inherit"
  >("inherit");
  const id: string = useId();
  return (
    <Stack space="lg">
      <Inline>
        <Button
          textCase="none"
          aria-pressed={theme === "inherit"}
          variant={theme === "inherit" ? "primary" : "default"}
          onClick={(): void => {
            setTheme("inherit");
          }}
        >
          Page theme
        </Button>
        {THEMES.map((name: Theme): ReactElement => (
          <Button
            key={name}
            textCase="none"
            aria-pressed={theme === name}
            variant={theme === name ? "primary" : "default"}
            onClick={(): void => {
              setTheme(name);
            }}
          >
            {name}
          </Button>
        ))}
      </Inline>
      <PresentationScope theme={theme === "inherit" ? undefined : theme}>
        <div className="foundation-theme-preview">
          <Stack space="lg">
            <Heading level={2} variant="heading-component">
              Display settings
            </Heading>
            <Text color="text-dim">
              Theme changes presentation; the control labels and behavior keep their meaning.
            </Text>
            <Stack space="xs">
              <Label htmlFor={id}>Profile name</Label>
              <Input id={id} defaultValue="Nav station" />
            </Stack>
            <Inline>
              <Button textCase="none">Save profile</Button>
              <Button textCase="none" disabled>
                Unavailable
              </Button>
              <Popover label="Theme scope" trigger={<Button textCase="none">Open themed popover</Button>}>
                <Text>This overlay follows the specimen theme.</Text>
              </Popover>
            </Inline>
          </Stack>
        </div>
      </PresentationScope>
    </Stack>
  );
}
01Meaning survivesA theme changes presentation, not behavior. Selection, focus, alert priority and data integrity keep their meaning through labels, shape and contrast.
02One scoped paletteComponents read shared tokens. A presentation scope applies a theme to a region, and overlays inherit that region’s presentation.
03A deliberate Night choiceAuto follows the device between Dusk and Day. Night is selected explicitly; brightness is a separate setting with a reset.

The four themes

ThemeModeIntended settingCharacter
NightDarkA dark cockpit with dark-adapted eyesAmber interface text and controls on black; instruments use the same amber hierarchy.
DuskDarkEvening or a nav station below deckNeutral charcoal with one teal accent.
DayLightDaylight at a nav station or in the saloonLight neutral surfaces with a readable accent step for text.
BrightLightDirect sunlight on a helm displayWhite surfaces, dark text and stronger separation.

A software palette does not measure ambient light or calibrate a display’s luminance. Check the actual device at its intended brightness and viewing distance. Night uses the amber interface palette, and red marks only the alarm. Instruments use their own amber hierarchy and keep conventional port/starboard colors and labels where those colors communicate direction.

Surfaces and foregrounds

These values come from the same Bosunkit stylesheets loaded by the examples. Ratios are computed against --bg; a control on another surface needs a check against that surface.

TokenNightDuskDayBright
--bg
Page surface
#0000001.0:1
#0B0C0F1.0:1
#F2F3F51.0:1
#FFFFFF1.0:1
--surface
Recessed well
#14100A1.1:1
#15161A1.1:1
#FFFFFF1.1:1
#ECEEF11.2:1
--surface-elevated
Active surface
#221A101.2:1
#2021271.2:1
#FFFFFF1.1:1
#FFFFFF1.0:1
--border
Region hairline
#3A2C161.6:1
#26272E1.3:1
#D5D8DD1.3:1
#C9CDD41.6:1
--border-bright
Control outline
#523D1E2.0:1
#33353D1.6:1
#B9BEC61.7:1
#AAB0B92.2:1
--text
Primary text
#EAA75E10.2:1
#E7E9EC16.1:1
#17181C16.0:1
#0B0C0F19.6:1
--text-dim
Supporting text
#8F61303.9:1
#8B8F986.0:1
#5B60685.7:1
#4B50588.1:1
--text-faint
Sources and section labels
#8F61303.9:1
#878B945.7:1
#666B754.8:1
#5D626B6.1:1
--bk-action-text
Accent text
#FFC87813.8:1
#14B8A67.9:1
#0D645E6.3:1
#0D645E7.0:1

Do not use a fill color as small text merely because it belongs to the palette. Selected and notification fills have paired text tokens on Control styles. The color foundation records the remaining contrast limits of accent and alert-fill tokens.

Controls and states

Normal, flat, raised, indent, selected, amplified, thumb and notification roles share enabled, hover, pressed, activated, focused and disabled palettes. Integration navigation aliases the matching role rather than introducing another accent. Use the Control styles matrix to inspect each combination.

Selection survives hover and press. Keyboard focus adds a ring. Disabled blocks activation, read-only preserves inspection, and pending reports unfinished work. See Interaction states for working examples and state precedence. Typography, radius, spacing and target size remain separate foundations.

Instruments, alerts and special color families

FamilyTheme rule
Regular / enhanced instrumentsNeutral values and an accent emphasis; Night uses amber with brighter enhanced values.
Alert priorityColor and the shared priority mark travel together. A theme does not remap urgency.
Data integrityStale and invalid readings retain their indication, label and age.
Port / starboardPreserve navigational color meaning and directional cues.
Automation mediaPreserve medium identity through labels; Night uses the documented amber and grey medium palette.
Data seriesStable categories across themes, with labels or line patterns where hue is insufficient.
Notifications and validationOrdinary operation feedback, separate from the alert palette.
TokenNightDuskDayBright
--instrument-regular-primary-color
Regular instrument
#EAA75E
#FFFFFF
#3D3D3D
#3D3D3D
--instrument-enhanced-primary-color
Enhanced instrument
#FFC878
#14B8A6
#0F766E
#0F766E
--alert-alarm-color
Alarm fill / mark
#E90F20
#E1000E
#E30019
#EB0014
--alert-warning-color
Warning fill / mark
#BA5A00
#FA7D00
#FE9413
#FE9413
--alert-caution-color
Caution fill / mark
#897300
#FFCC16
#FFDB42
#FFDB42
--alert-low-integrity-border-color
Low integrity
#433700
#8E6A00
#6B4F00
#8F6B00
--alert-invalid-border-color
Invalid
#A60000
#A60000
#7F0000
#FF4D47
Do

▲ alarm

● warning

■ caution

Keep the shared priority mark and a label. They remain distinguishable when hue is difficult to see.
Don't

Three colored edges with no priority label or mark.

Color alone does not identify urgency or state.

Theme choice, brightness and reset

  • Auto follows the operating system: Dusk for dark and Day for light. It never chooses Night.
  • An explicit choice persists in this browser until it is changed. It is a device preference, not vessel data or browsing history.
  • Brightness adjusts presentation independently of the palette. Alert presentation and the control that restores brightness remain reachable.
  • Reset in Brilliance menu restores the configured default brightness and Auto, so the palette follows the system again.

The design site stores its own theme choice. Kit applications use the shared theme store for theme preference, brightness and size class. Their storage keys and controls are distinct; switching the design site does not change an application’s persisted settings.

Scopes and overlays

Use PresentationScope to show one theme inside another. It sets data-theme on its region; leaving the prop unset inherits the surrounding presentation. Applications import @bosunkit/tokens/index.css once. Components consume variables and do not inspect the theme name to choose their own colors. Both live theme explorers follow the page theme by default; selecting an explicit specimen theme overrides only that region. Choose Page theme to return to inheritance.

A portaled overlay must receive the trigger’s theme, size class and language. Use the shared overlay components and check the rendered overlay itself: inheriting the page root can look correct until a Day specimen opens on a Night page. The live popover above demonstrates this boundary. Do not assume every overlay inherits a local scope.

This region uses Day. Change the page theme, then open each overlay.

A Day region keeps its palette, size and language when a list, dialog or tooltip opens.
Code
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
import { Button } from "@bosunkit/components/button";
import { Inline } from "@bosunkit/components/inline";
import { Stack } from "@bosunkit/components/stack";
import { TaskDialog } from "@bosunkit/components/task-dialog";
import { Select } from "@bosunkit/components/select";
import { Tooltip } from "@bosunkit/components/tooltip";
import { Text } from "@bosunkit/components/text";
import { PresentationScope } from "@bosunkit/provider";
import "../control-styles/example.css";

export const description: string =
  "A Day region keeps its palette, size and language when a list, dialog or tooltip opens.";

export default function Example(): ReactElement {
  const [open, setOpen]: [boolean, Dispatch<SetStateAction<boolean>>] = useState(false);
  return (
    <PresentationScope theme="day" lang="en">
      <div className="foundation-theme-preview" data-testid="scoped-overlay-example">
        <Stack space="lg">
          <Text>This region uses Day. Change the page theme, then open each overlay.</Text>
          <Select.Root defaultValue="nav">
            <Select.Trigger aria-label="Display location">
              <Select.Value />
            </Select.Trigger>
            <Select.Portal><Select.Positioner alignItemWithTrigger={false} ><Select.Popup ><Select.List>
              <Select.Item value="nav">Nav station</Select.Item>
              <Select.Item value="helm">Helm</Select.Item>
            </Select.List></Select.Popup></Select.Positioner></Select.Portal>
          </Select.Root>
          <Inline>
            <Button
              textCase="none"
              onClick={(): void => {
                setOpen(true);
              }}
            >
              Open scoped dialog
            </Button>
            <Tooltip content="The tooltip uses the Day palette too.">
              <Button textCase="none">Hover or focus for a tip</Button>
            </Tooltip>
          </Inline>
          <TaskDialog
            open={open}
            onOpenChange={(open): void => { if (!open) ((): void => {
              setOpen(false);
            })(); }}
            title="Display location"
            actions={
              <Button
                textCase="none"
                onClick={(): void => {
                  setOpen(false);
                }}
              >
                Close example
              </Button>
            }
          >
            <Text>The dialog keeps the presentation of the region that opened it.</Text>
          </TaskDialog>
        </Stack>
      </div>
    </PresentationScope>
  );
}

Every kit overlay (lists, menus, pickers, dialogs, popovers, tooltips and toasts) copies its presentation scope into the portal, and one opened from a dialog or a toast stacks above it. With no explicit scope, they inherit the page theme.

Documents, charts and generated artwork

Reports use the same four themes in the site. Static investor documents and emails resolve their chosen palette at export time; they do not inherit a recipient's browser theme. Screenshots retain the theme in which they were captured. Explain their context rather than trying to recolor pixels with a surrounding CSS scope.

Mermaid reads computed roles from its own container and renders again when the effective theme changes. Two independent diagrams must keep separate palettes even though the renderer has global configuration. Verify the SVG itself, its source disclosure and its controls. An inherited page background is not proof that generated labels and connector colors have updated.

Use the token guide for renderer and export boundaries. Check categorical series, editorial accents and diagram roles as well as the ordinary page and control palettes.

Coverage and verification

The four palettes cover the ported instrument families and the documented control-state roles. The source vocabulary is pinned to OpenBridge 2.0.0-next.128. The kit is a React port with Bosun’s typography and palettes; it does not contain every upstream component or every automation and augmented-reality token.

Token tests check complete control-state definitions, readable selected/notification text pairs and nested scopes. Component verification also needs keyboard and pointer behavior, actual foreground/background contrast, focus, forced colors, reduced motion and overlays in every theme. Passing token tests alone is not a blanket component accessibility or bridge-display claim.