ibcs-react

Theming

Token presets, the IbcsThemeProvider context and per-component overrides - every colour and scenario fill in ibcs-react is a token.

Every colour, font stack and scenario fill is a token. Pass a ready-made preset, set one for a whole subtree with a provider, or override just the few values you care about - overrides are deep-merged onto the theme underneath.

How a component resolves its tokens

Nearest wins, and each layer is deep-merged onto the one below:

  1. the component's own tokens prop (a partial override is fine), merged onto
  2. the nearest IbcsThemeProvider theme, merged onto
  3. defaultTokens.

Providers nest: an inner provider's override composes onto the outer theme rather than replacing it. That means "brand palette at the app root, dark surface for one panel, one red tweaked on one card" is three small overrides, not three full themes.

IbcsThemeProvider

Set the theme once instead of threading a tokens prop through every component:

import { IbcsThemeProvider, KpiCard, StatementTable, tokenPresets } from "ibcs-react";

<IbcsThemeProvider tokens={tokenPresets.cvd}>
  <KpiCard label="Revenue" values={{ AC: 30.1e6, PY: 25.6e6 }} />
  <StatementTable lines={statement} />
</IbcsThemeProvider>;

tokens accepts a full theme (a tokenPresets entry) or a partial IbcsTokensOverride - either way it is deep-merged onto the parent theme.

Below, one provider (the Ocean preset) themes all three components; the middle card additionally passes its own tokens prop, which wins over the provider for that one leaf.

From the provider
Revenue
€30.1M+4.5M+17.6%vs PY
Provider + own override
Revenue
€30.1M+4.5M+17.6%vs PY
Also from the provider
6.8 mQ17.3 mQ27.6 mQ38.4 mQ4+0.7 m+0.9 m+0.9 m+2 m
AC versus PY - data table
ACPYΔPYΔPY%
Q16.8 m6.1 m+0.7 m+11.5%
Q27.3 m6.4 m+0.9 m+14.1%
Q37.6 m6.7 m+0.9 m+13.4%
Q48.4 m6.4 m+2 m+31.3%
<IbcsThemeProvider tokens={tokenPresets.ocean}>
  <KpiCard label="Revenue" values={{ AC: 30.1e6, PY: 25.6e6 }} />

  {/* Provider theme + this card's own override - the prop wins. */}
  <KpiCard
    label="Revenue"
    values={{ AC: 30.1e6, PY: 25.6e6 }}
    tokens={{ color: { good: "#7d3cc8" } }}
  />

  <VarianceColumnChart data={quarterly} comparison="PY" />
</IbcsThemeProvider>

The tokens prop is deep-merged

Every component takes tokens?: IbcsTokensOverride. You supply only the leaves you want to change; everything else falls back to the provider theme and then to defaultTokens. Tweaking the favorable colour does not force you to re-specify all four scenario fills:

import { StatementTable } from "ibcs-react";

<StatementTable
  lines={statement}
  tokens={{
    color: { good: "#0f8a5f", bad: "#d23b3b" }, // variance colours
    scenario: {
      AC: { fill: "#1f5e7d", stroke: "#1f5e7d", variant: "solid" }, // teal actuals
    },
  }}
/>;

Prefer the strict black/red "good = neutral" convention? Override color.good to your neutral ink and keep color.bad red - the deep merge makes that a one-line change.

useIbcsTokens

useIbcsTokens(override?) is what every component calls internally to resolve its tokens prop against the active theme. It is public, so a custom chart you build on the library's core helpers participates in the same theme:

import { useIbcsTokens, type IbcsTokensOverride } from "ibcs-react";

function MyMiniBar({ tokens }: { tokens?: IbcsTokensOverride }) {
  const t = useIbcsTokens(tokens); // provider theme + this override + defaults
  return <rect fill={t.scenario.AC.fill} width={80} height={12} />;
}

Outside any provider it returns defaultTokens with your override applied, so a component works standalone as well as inside a theme.

The eight presets

The library ships eight full token sets plus a named registry:

  • defaultTokens - neutral greys for actuals, muted green/red variance. Tuned for a light surface.

  • oceanTokens - a cool blue-grey set: dark navy actuals, light blue-grey previous year, IBCS semantic green/red.

  • azureTokens - a monochromatic bright-blue alternative: navy actuals, azure previous year.

  • greenRedTokens - the strict "good stands out, bad stands out" business look: darker actuals, vivid green/red.

  • vividTokens - teal/blue actual bars with vivid variance, for a more colourful deck.

  • cvdTokens - colour-vision-deficiency safe: favorable teal, unfavorable orange. Only the impact colours change; scenario fills stay greyscale, because IBCS distinguishes scenarios by fill, not hue.

  • monoTokens - greyscale for black-and-white printing: favorable reads as a darker grey, unfavorable as a lighter one, with the signed labels and the hatched/framed fills carrying the rest.

  • darkTokens - a dark-surface ink set (including color.surface and color.onFill, so cards, tooltips and in-bar labels come along).

  • tokenPresets - all eight under stable, autocompleting ids (default, ocean, azure, greenRed, vivid, cvd, mono, dark - the TokenPresetId type), with matching display names in tokenPresetLabels - ready to drive a theme switcher:

    {
      (Object.keys(tokenPresets) as TokenPresetId[]).map((id) => (
        <option key={id} value={id}>
          {tokenPresetLabels[id]}
        </option>
      ));
    }

Same data, same props - only tokens changes. Each card is painted with its preset's own color.surface:

Default
6.8 mQ17.3 mQ27.6 mQ38.4 mQ4+0.7 m+0.9 m+0.9 m+2 m
AC versus PY - data table
ACPYΔPYΔPY%
Q16.8 m6.1 m+0.7 m+11.5%
Q27.3 m6.4 m+0.9 m+14.1%
Q37.6 m6.7 m+0.9 m+13.4%
Q48.4 m6.4 m+2 m+31.3%
Ocean
6.8 mQ17.3 mQ27.6 mQ38.4 mQ4+0.7 m+0.9 m+0.9 m+2 m
AC versus PY - data table
ACPYΔPYΔPY%
Q16.8 m6.1 m+0.7 m+11.5%
Q27.3 m6.4 m+0.9 m+14.1%
Q37.6 m6.7 m+0.9 m+13.4%
Q48.4 m6.4 m+2 m+31.3%
Azure
6.8 mQ17.3 mQ27.6 mQ38.4 mQ4+0.7 m+0.9 m+0.9 m+2 m
AC versus PY - data table
ACPYΔPYΔPY%
Q16.8 m6.1 m+0.7 m+11.5%
Q27.3 m6.4 m+0.9 m+14.1%
Q37.6 m6.7 m+0.9 m+13.4%
Q48.4 m6.4 m+2 m+31.3%
Green / Red
6.8 mQ17.3 mQ27.6 mQ38.4 mQ4+0.7 m+0.9 m+0.9 m+2 m
AC versus PY - data table
ACPYΔPYΔPY%
Q16.8 m6.1 m+0.7 m+11.5%
Q27.3 m6.4 m+0.9 m+14.1%
Q37.6 m6.7 m+0.9 m+13.4%
Q48.4 m6.4 m+2 m+31.3%
Vivid
6.8 mQ17.3 mQ27.6 mQ38.4 mQ4+0.7 m+0.9 m+0.9 m+2 m
AC versus PY - data table
ACPYΔPYΔPY%
Q16.8 m6.1 m+0.7 m+11.5%
Q27.3 m6.4 m+0.9 m+14.1%
Q37.6 m6.7 m+0.9 m+13.4%
Q48.4 m6.4 m+2 m+31.3%
CVD-safe
6.8 mQ17.3 mQ27.6 mQ38.4 mQ4+0.7 m+0.9 m+0.9 m+2 m
AC versus PY - data table
ACPYΔPYΔPY%
Q16.8 m6.1 m+0.7 m+11.5%
Q27.3 m6.4 m+0.9 m+14.1%
Q37.6 m6.7 m+0.9 m+13.4%
Q48.4 m6.4 m+2 m+31.3%
Mono / print
6.8 mQ17.3 mQ27.6 mQ38.4 mQ4+0.7 m+0.9 m+0.9 m+2 m
AC versus PY - data table
ACPYΔPYΔPY%
Q16.8 m6.1 m+0.7 m+11.5%
Q27.3 m6.4 m+0.9 m+14.1%
Q37.6 m6.7 m+0.9 m+13.4%
Q48.4 m6.4 m+2 m+31.3%
Dark
6.8 mQ17.3 mQ27.6 mQ38.4 mQ4+0.7 m+0.9 m+0.9 m+2 m
AC versus PY - data table
ACPYΔPYΔPY%
Q16.8 m6.1 m+0.7 m+11.5%
Q27.3 m6.4 m+0.9 m+14.1%
Q37.6 m6.7 m+0.9 m+13.4%
Q48.4 m6.4 m+2 m+31.3%
import { VarianceColumnChart, greenRedTokens, tokenPresets } from "ibcs-react";

// Pass a whole preset…
<VarianceColumnChart data={data} tokens={greenRedTokens} />

// …or pick one by id from the registry (handy for a theme switcher):
<VarianceColumnChart data={data} tokens={tokenPresets.vivid} />

Dark surfaces

The surface itself is a token, so dark mode is a preset swap rather than a fork: darkTokens carries color.surface (cards, menus, tooltips, the fill of hollow plan shapes), color.surfaceMuted and color.onFill (ink drawn on a solid bar) alongside the scenario fills.

Revenue
€30.1M+4.5M+17.6%vs PY
 PYACΔPYΔPY%
Product revenue16.1M17.2M+1.1M+6.8
Service and other revenue9.5M12.9M+3.4M+35.8
=Revenue25.6M30.1M+4.5M+17.6
−Cost of goods sold8.4M9.7M+1.3M+15.5
=Gross margin17.2M20.4M+3.2M+18.6
import { IbcsThemeProvider, KpiCard, StatementTable, darkTokens } from "ibcs-react";

<div style={{ background: darkTokens.color.surface }}>
  <IbcsThemeProvider tokens={darkTokens}>
    <KpiCard label="Revenue" values={{ AC: 30.1e6, PY: 25.6e6 }} />
    <StatementTable lines={statement} />
  </IbcsThemeProvider>
</div>;

Cards and blocks

How a KPI card or a Report block is framed is a theme decision too. The card group is the default behind every appearance prop: framedCard (what every preset ships with - a hairline border, gently rounded, not lifted) or flatCard - no border, no rounding, no shadow, no padding; blocks separated by whitespace alone. That is the IBCS SIMPLIFY ideal, and what a printed page wants. It composes with any palette, so paper is one override:

import { Report, flatCard, tokenPresets } from "ibcs-react";

<Report config={report} tokens={{ card: flatCard }} />;

// flat blocks in the Ocean palette
<Report config={report} tokens={{ ...tokenPresets.ocean, card: flatCard }} />;

A component's own appearance prop still wins per instance: <KpiCard appearance={{ shadow: true }} /> lifts one card in an otherwise flat theme.

What each token controls

TokenTypeWhat it drives
color.neutralstringActual step bars (add / subtract waterfall steps).
color.totalstringEmphasised actual bars - subtotals and results.
color.goodstringFavorable variance (impact, not sign).
color.badstringUnfavorable variance.
color.zerostringZero / no-change marks.
color.axis, color.gridlinestringAxis line and gridlines.
color.text, color.textMutedstringLabels and secondary text.
color.rowBorderstringTable row rules and hairlines.
color.surface, color.surfaceMutedstringOpaque component background (cards, tooltips, sticky cells, hollow plan fills) and its subtle tint.
color.onFillstringInk drawn on a solid scenario bar - in-bar value labels.
scenario.AC / PY / PL / FCIbcsScenarioStylePer-scenario fill, stroke and variant ("solid", "frame", "hatch").
font.familystringFont stack for component chrome and SVG text.
card.radius, card.border, card.borderWidth, card.shadow, card.paddingnumber, boolean | string, number, boolean, number | stringHow cards and report blocks are framed - the defaults behind every appearance prop. framedCard / flatCard are ready-made sets.

Where to next

  • IBCS & ISO 24896 - why the fills and colours mean what they mean before you change them.
  • Components - every component takes the same tokens prop.
ibcs-react is an independent open-source library and is not affiliated with, certified by, or endorsed by the IBCS Association or ISO. It follows the IBCS® notation rules (the basis of ISO 24896); IBCS® is a registered trademark of the IBCS Association.

On this page