Alpha — breaking changes ship without a deprecation cycle. What is built vs. planned
Bootstrap exporter
Acme · Cathode · GOV.UK · Carbon: one page, four design systems, compiled to Bootstrap. All 44 demos →
Emits a Bootstrap ≥5.3 theme along both consumption paths the Bootstrap community uses. On Acme, that’s 146 Sass variables and 251 CSS custom properties, over 721 classified rows:
| File | Path | Fidelity |
|---|---|---|
_variables.transtyle.scss |
Sass | full. Imported before Bootstrap, so $primary, radii, fonts, spacers compile into every component |
_maps.transtyle.scss |
Sass | full. Imported after Bootstrap’s variables, it replaces its sRGB tint-color()/shade-color() derivations with our OKLCH-derived subtle/emphasis values |
bootstrap-theme.css |
CSS variables | partial, documented. It rethemes the token tier (--bs-*, utilities, body, borders, [data-bs-theme=dark] block) and the per-component variable layer: structure blocks (button/badge/toast paddings, radii, transitions) plus .btn-<variant> state colors from the role grid in both modes, so .btn-primary backgrounds/hovers follow your brand even without Sass. Values with no runtime variable (Sass-only expressions, responsive re-sets) stay out of reach. Use it when you have no Sass build |
"targets": { "bootstrap": { "output": "dist/bootstrap" } }
// your main.scss — the generated usage.md carries this exact order
@import 'bootstrap/scss/functions';
@import '../dist/bootstrap/variables.transtyle';
@import 'bootstrap/scss/variables';
@import 'bootstrap/scss/variables-dark';
@import '../dist/bootstrap/maps.transtyle';
@import 'bootstrap/scss/bootstrap';
The interesting part: we out-derive Bootstrap on its own turf#
Bootstrap generates -bg-subtle / -border-subtle / -text-emphasis per theme color by sRGB tinting. This exporter overrides those maps with the engine’s OKLCH derivations. <role>.on-tint is the AA-checked “on-brand walk”, <role>.tint a perceptual mix toward your surface, and <role>.outline (the border-subtle source) is now a first-class role-grid cell rather than a private exporter formula. So alerts and subtle badges stay perceptually consistent across all roles and both modes.
Mapping highlights#
| Bootstrap variable | Comes from | Note |
|---|---|---|
$primary…$danger |
the same-named roles’ .solid |
secondary…danger usually derived on minimal systems |
$light / $dark |
neutral.tint / neutral.text-strong |
exporter convention; Bootstrap’s grayscale pseudo-roles have no IR slot; with no neutral.text-strong (a text.base bound to a role cell, TST1205), $dark stays Bootstrap’s |
$theme-colors-text |
<role>.on-tint |
the native binding the Phase 0 exercise predicted |
$theme-colors-bg-subtle / -border-subtle |
<role>.tint / <role>.outline |
cartesian-OKLab mixes, both engine-owned grid cells |
$body-*, $border-color (+ -dark variants) |
elevation.{0,1}.surface / text.base / text.muted / neutral.tint / border |
|
$link-color, $focus-ring-color |
primary, ring |
dark-mode links ← ring[dark] (visibility-lightened) |
$focus-ring-width |
ring.width |
only when you author it (itself, or through border-width.medium): the halo’s spread, where other targets draw an outline. The offset is dropped, since a halo has no gap |
$border-radius{,-sm,-lg,-xl,-pill} |
the radius.* scale |
sm/lg/xl derived from your md |
$font-family-*, type scale, $spacers, $box-shadow* |
fonts, the engine’s type.*/space.* scales, scrim alpha ramps |
scales report derived until you author them |
$btn-*, $modal-*, … (the 657-variable component tier) |
component.* tokens + semantic defaults |
AL1: bound by meaning per the checked-in surface inventory, with one coverage row per variable (driven, chained, or dropped/unsupported) |
$grid-breakpoints, $display-font-sizes |
the catalog has these concepts | unsupported anyway, because the ladders disagree (only md/768px matches on breakpoints), and rebinding would move behavior, not theme it |
$box-shadow-inset, $container-max-widths |
— | unsupported and reported as such, since there’s no IR counterpart |
The six theme colours ($primary…$danger) carry their .solid slot’s $description and $deprecated as // lines above the declaration in _variables.transtyle.scss.
Component theming#
Bootstrap 5.3’s _variables.scss has 952 top-level variables; 657 are component-scoped, and every one of them is classified against a checked-in surface inventory (packages/exporter-bootstrap/surface-inventory.json, drift-guarded in CI). To answer “which $btn-* variables does Transtyle drive, and via which path?” for any variable:
report.jsoncarries one coverage row per variable. The row gives its source slot (component.button.radius,semantic.space.2, …), its class (native/derived/approximated), or why it isn’t driven (droppedstructure and derivation knobs,unsupportedIR gaps like icon assets and state opacities, each with a note). A colour that doesn’t fit sRGB is written clamped to hex, and its row isapproximatedwith the notesRGB gamut clamp during oklch → hex.- Sass path: bound variables are emitted in the component section of
_variables.transtyle.scss; everything else follows Bootstrap’s own!defaultchains from the roots Transtyle drives. - CSS path: variables with a runtime
--bs-*counterpart get selector-scoped overrides, and.btn-<variant>blocks carry state colors from the role grid.
Authoring is optional refinement. An empty component tier compiles forever from semantic defaults (Carbon’s buttons come out square because its radius.control is 0), and an authored token wins on both paths. Acme authors component.button.{radius,padding-x,padding-y} and gets pill buttons in the demo.
One coupling comes from Bootstrap itself, and it’s documented rather than hidden. Button and input padding share a single root ($input-btn-padding-*), so authored button padding moves form fields too. PrimeNG’s form-field theming expresses the same coupling.
Dark mode follows Bootstrap’s own mechanism (data-bs-theme="dark") on both paths. One Sass-path caveat comes from Bootstrap itself: $primary is a single value, so brand colors don’t flip per mode. That’s exactly how Bootstrap’s own dark mode behaves.
See it running on real Bootstrap components: npm run dev -w acme-demo-bootstrap (or cathode-demo-bootstrap for the CRT version) in the examples.
Contrast, motion and brand#
For a design system declaring contrast, motion or brand:
- CSS path.
bootstrap-theme.cssgains[data-contrast="more"]and[data-brand="globex"]blocks, for the--bs-*variables and for the button variants ([data-brand="globex"] .btn-primary), plus compound blocks like[data-bs-theme="dark"][data-brand="globex"]where the separate ones would be wrong on a dark page.contrast: morealso followsprefers-contrast: moreuntildata-contrastis set. - Sass path. Sass compiles one theme, so
_variables.transtyle.scssand_maps.transtyle.scsscome once per brand (_variables.transtyle.globex.scss): build one Bootstrap per brand. They hold the standard-contrast values; loadbootstrap-theme.cssafter the build for the contrast blocks (the report’s(mode:contrast)row says so). - Motion. Bootstrap’s own
$enable-reduced-motionalready removes every transition underprefers-reduced-motion: reduce:nativewhen the design system’s reduced durations are all0ms,approximatedwhen it only shortens them.