| name | backpack-css-vars-migration |
| description | Migrate a Backpack component's SCSS mixin from static SASS tokens to CSS custom
properties (CSS vars) for light/dark mode support, while preserving SASS tokens
as fallbacks. Use when: (1) a component needs light/dark mode theming via the new
CSS var system, (2) a component still uses bare SASS token values (e.g.
tokens.$bpk-text-primary-day) instead of var() declarations, (3) you need to
wire up BpkThemeProvider overrides to the new CSS var names. Covers the full
migration: SCSS mixins, themeAttributes.tsx, stories, and tests.
|
| author | Claude Code |
| version | 1.2.0 |
| date | "2026-06-24T00:00:00.000Z" |
Backpack CSS Vars Migration
Problem
Backpack components use static SASS tokens (e.g. tokens.$bpk-text-primary-day) that are
hardcoded to light-mode values. The new CSS var system (theme-backpack-light.css /
theme-backpack-dark.css / primitives.css) defines custom properties that flip
automatically with the active theme, but components need to opt in by using var().
Context / Trigger Conditions
- Component SCSS uses bare
tokens.$bpk-*-day values with no var() wrapping
- Component has a mixin file using
@include utils.bpk-themeable-property(...) with old --bpk-{component}-* var names
- Stories have themed examples that no longer apply overrides correctly
themeAttributes.tsx lists camelCase keys that map to old var names not present in SCSS
Key Files
| File | Purpose |
|---|
packages/backpack-web/src/bpk-mixins/_{component}.scss | Main mixin — where colour/spacing/typography tokens live |
packages/backpack-web/src/bpk-stylesheets/theme-backpack-light.css | Light theme CSS vars (--bpk-private-{component}-*, --bpk-text-*, etc.) |
packages/backpack-web/src/bpk-stylesheets/theme-backpack-dark.css | Dark theme CSS vars (same names, different values) |
packages/backpack-web/src/bpk-stylesheets/primitives.css | Global primitive vars (--bpk-spacing-*, --bpk-radius-*, --bpk-border-*) |
src/bpk-component-{name}/src/themeAttributes.tsx | Declares keys BpkThemeProvider injects as inline CSS vars |
src/bpk-component-{name}/src/{component}.stories.tsx | Themed story examples — must use new key names |
src/bpk-component-{name}/src/themeAttributes-test.tsx | Tests for exported themeAttribute arrays |
Solution
Step 1: Discover available CSS vars
Check what vars exist for the component across all three CSS files:
grep -i "{component}" packages/backpack-web/src/bpk-stylesheets/theme-backpack-light.css
grep -i "{component}" packages/backpack-web/src/bpk-stylesheets/primitives.css
Also check global semantic vars that may apply (colours, spacing, radii):
grep -E "bpk-text-|bpk-core-|bpk-status-|bpk-surface-" packages/backpack-web/src/bpk-stylesheets/theme-backpack-light.css
grep -E "bpk-spacing-|bpk-radius-|bpk-border-" packages/backpack-web/src/bpk-stylesheets/primitives.css
Step 2: Migrate the SCSS mixin
Replace every bare token and bpk-themeable-property call with a plain var():
@include utils.bpk-themeable-property(
background-color,
--bpk-badge-normal-background-color,
tokens.$bpk-private-badge-background-normal-day
);
background-color: var(--bpk-private-badge-colour-bg-default, tokens.$bpk-private-badge-background-normal-day);
Pattern: property: var(--new-css-var, tokens.$sass-fallback);
No nested fallbacks. No cascade chains. One CSS var, one SASS fallback.
Colour properties
Map each token to its CSS var equivalent:
| SASS token | CSS var |
|---|
$bpk-text-primary-day | --bpk-text-primary |
$bpk-text-on-light-day | --bpk-text-on-light (avoid for status/info badges — Figma uses text-primary there) |
$bpk-text-on-dark-day | --bpk-text-on-dark |
$bpk-text-primary-inverse-day | --bpk-text-inverse |
$bpk-core-primary-day | --bpk-core-primary |
$bpk-core-accent-day | --bpk-core-accent |
$bpk-surface-default-day | --bpk-surface-default |
$bpk-status-warning-spot-day | --bpk-status-warning-spot |
$bpk-status-success-spot-day | --bpk-status-success-spot |
$bpk-status-danger-spot-day | --bpk-status-danger-spot |
$bpk-private-{component}-background-* | --bpk-private-{component}-colour-bg-* |
Spacing / dimensions
padding: var(--bpk-spacing-sm, tokens.bpk-spacing-sm()) var(--bpk-spacing-md, tokens.bpk-spacing-md());
padding: var(--bpk-spacing-sm, tokens.bpk-spacing-sm()) var(--bpk-private-{component}-dimension-padding-horizontal-default, tokens.bpk-spacing-md());
Border radius
border-radius: var(--bpk-radius-xs, tokens.$bpk-border-radius-xs);
Border width
Match by pixel value to the primitive:
border: var(--bpk-border-1, tokens.$bpk-border-size-sm) solid ...;
Typography
Replace @include typography.bpk-footnote (and similar) with explicit declarations so each property can have its own var.
Check for --bpk-private-{component}-typography-tmp-* vars first — these are transitional names that exist in theme-backpack-light/dark.css but will be removed. If one exists, use it with a SASS fallback. If not (or if the var is marked tmp and removal is planned), use the SASS token directly — no CSS var wrapper.
font-size: var(--bpk-private-{component}-typography-stable-name, tokens.$bpk-font-size-sm);
font-size: tokens.$bpk-font-size-xs;
font-weight: var(--bpk-{component}-font-weight, tokens.$bpk-font-weight-book);
line-height: var(--bpk-{component}-line-height, tokens.$bpk-line-height-sm);
When dropping a tmp font-size var, also remove the corresponding camelCase key (e.g. badgeFontSize) from themeAttributes and its test — it's no longer themeable via that route.
Typography mixins bpk-text and bpk-caption: bpk-text only sets margin: 0 (already in the base mixin). bpk-caption only sets font-size, line-height, and font-weight via _bpk-text-factory — no font-family or letter-spacing. Our three explicit var() declarations fully replace them.
RTL / logical properties
Replace physical properties + bpk-rtl mixin with CSS logical properties:
right: 0;
@include utils.bpk-rtl { right: inherit; left: 0; }
inset-inline-end: 0;
top → block-start bottom → block-end
left → inline-start right → inline-end
border-top-left-radius → border-start-start-radius
border-top-right-radius → border-start-end-radius
border-bottom-left-radius → border-end-start-radius
border-bottom-right-radius → border-end-end-radius
Step 3: Clean up unused imports
After migrating, remove any @use imports that are no longer referenced:
grep "utils\.\|typography\.\|radii\." packages/backpack-web/src/bpk-mixins/_{component}.scss
Step 4: Update themeAttributes.tsx
BpkThemeProvider converts camelCase keys → --bpk-{kebab-case} inline styles. Inline styles have higher specificity than stylesheets, so themed overrides win automatically.
Only expose --bpk-private-{component}-* vars as themeable keys. Global semantic vars (--bpk-text-primary, --bpk-core-primary, --bpk-radius-xs, etc.) are theme-level concerns — they apply across all components and should be set globally, not per-component in a badge's themeAttributes.
Update per-variant arrays to only list private component-scoped keys:
export const badgeNormalThemeAttributes = ['privateBadgeColourBgDefault'];
export const badgeNormalThemeAttributes = ['privateBadgeColourBgDefault', 'textPrimary'];
Camelcase conversion rule: strip --bpk-, convert kebab to camelCase.
e.g. --bpk-private-badge-colour-bg-default → privateBadgeColourBgDefault
Note: BpkThemeProvider has a digit handler so --bpk-border-1 → key border1.
allBadgeThemeAttributes should be a deduplicated flat array of all private badge vars — not a spread of per-variant arrays (which repeat shared vars). Since normal/warning/success/critical all share privateBadgeColourBgDefault, the combined array should only list it once:
export const allBadgeThemeAttributes = [
'privateBadgeColourBgDefault',
'privateBadgeColourBgInverse',
'privateBadgeColourBgOutline',
'privateBadgeColourStrokeOutline',
'privateBadgeColourBgSubtle',
];
Step 5: Update stories
Replace old theme keys in BpkThemeProvider story instances:
<BpkThemeProvider
theme={{ badgeNormalBackgroundColor: coreAccentDay }}
themeAttributes={['badgeNormalBackgroundColor']}
>
<BpkThemeProvider
theme={{ privateBadgeColourBgDefault: coreAccentDay }}
themeAttributes={['privateBadgeColourBgDefault']}
>
Step 6: Update themeAttributes-test.tsx
Update all .toEqual([...]) assertions to match the new key names.
Verify token choices against Figma
Always cross-check colour tokens against the Figma component. Use get_design_context with disableCodeConnect: true to get the generated code — it includes the CSS var names Figma uses (e.g. --text/primary, --component/badge/colour/bg-default). Strip the slashes and prepend --bpk- to get the SCSS var name.
The Figma-generated code is the source of truth for which colour goes on which variant. Example from badge:
- Normal / Subtle / Inverse / Warning / Success / Critical text:
--text/primary → --bpk-text-primary
- Strong / Outline text:
--text/on-dark → --bpk-text-on-dark
- Brand text:
--text/inverse → --bpk-text-inverse
Verification
grep -n "var(--bpk-{component}-" packages/backpack-web/src/bpk-mixins/_{component}.scss
npx jest themeAttributes-test --testPathPattern={component}
Notes
- No nested fallbacks:
var(--new-var, sass-fallback). Never var(--old-hook, var(--new-var, sass-fallback)).
- Specificity wins for theming: BpkThemeProvider injects inline styles — no cascade tricks needed.
--bpk-private-* vars: Component-scoped vars from the theme files. Prefer them for backgrounds. Use global semantic vars (--bpk-text-*, --bpk-core-*) for text and icon colours.
- The outline box-shadow fallback line: Keep a bare SASS token
box-shadow declaration before the var() one — for browsers without CSS var support.
- Dimension vars: Component-specific dimension vars (e.g.
--bpk-private-{component}-dimension-padding-horizontal-default) take precedence over generic spacing vars where they exist.
bpk-themeable-property is gone: All colour, spacing, radius and typography properties should use plain var(). The mixin is only still appropriate for legacy theme hooks that have no CSS var equivalent.