Skip to content
Transtyle v0.1.0-alpha.5
ok: build 10:17 UTC31 pages main · 7ed21c5

Alpha — breaking changes ship without a deprecation cycle. What is built vs. planned

Bootstrap exporter

See it live

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.json carries 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 (dropped structure and derivation knobs, unsupported IR 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 is approximated with the note sRGB 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 !default chains 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.css gains [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: more also follows prefers-contrast: more until data-contrast is set.
  • Sass path. Sass compiles one theme, so _variables.transtyle.scss and _maps.transtyle.scss come once per brand (_variables.transtyle.globex.scss): build one Bootstrap per brand. They hold the standard-contrast values; load bootstrap-theme.css after the build for the contrast blocks (the report’s (mode:contrast) row says so).
  • Motion. Bootstrap’s own $enable-reduced-motion already removes every transition under prefers-reduced-motion: reduce: native when the design system’s reduced durations are all 0ms, approximated when it only shortens them.