Bootstrap exporter
Acme · Cathode · GOV.UK · Carbon — one page, four design systems, compiled to Bootstrap. All 32 demos →
Emits a Bootstrap ≥5.3 theme along both consumption paths the Bootstrap community actually uses — on Acme, 144 Sass variables and 215 CSS custom properties, over 714 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; replaces its sRGB tint-color()/shade-color() derivations with our OKLCH-derived subtle/emphasis values |
bootstrap-theme.css |
CSS variables | partial, documented — 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. Still out of reach: values with no runtime variable (Sass-only expressions, responsive re-sets). 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 |
$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) |
$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 — one coverage row per variable (driven, chained, or honestly dropped/unsupported) |
$grid-breakpoints, $display-font-sizes |
the catalog has these concepts | unsupported anyway — the ladders disagree (only md/768px matches on breakpoints), and rebinding would move behavior, not theme it |
$box-shadow-inset, $container-max-widths |
— | unsupported, reported honestly — no IR counterpart |
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 — 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).- 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 inherited from Bootstrap itself, documented rather than hidden: button and input padding share a single root ($input-btn-padding-*), so authored button padding moves form fields too — the same coupling PrimeNG’s form-field theming expresses.
Dark mode follows Bootstrap’s own mechanism (data-bs-theme="dark") on both paths. One Sass-path caveat inherited from Bootstrap itself: $primary is a single value, so brand colors don’t flip per mode — 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.