Skip to content

src/core - 4 files, 14,271 B gzip

Core

The only mandatory import. A page that loads these four files and nothing else is already themed, readable and keyboard operable, without a single class in the markup.

The markup panels below are read out of the live examples by site.js. With scripting off the examples still render and the panels stay empty.

Every number on this page is measured in your browser rather than transcribed into it. The pixel sizes under the type scale and the contrast ratio on each swatch are read out of the live cascade by src/behaviour/token-probe.js, in whichever theme you are in, at whatever width this window is, so a figure here cannot go stale while a token moves underneath it.

Reset

Deliberately small. It removes inherited inconsistency, it does not impose a look. Border-box inherits from html so a subtree can opt back out. Margin is owned here so a component never has to defend against a browser margin it did not ask for, and list padding is left alone so markers survive.

Two !important declarations exist in the whole framework and both are here, because layers beat specificity and only importance reverses layer order. One keeps [hidden] hidden even against a component rule. The other clamps every animation and transition under prefers-reduced-motion to 0.01ms, framework wide, so an author who forgets the media query cannot ship motion a reader has asked not to see. 0.01ms rather than zero, so transitionend still fires and a state machine waiting on it does not stall.

The reset also floors every button at the WCAG 2.2 SC 2.5.8 target size, pins textarea resize to the block axis so a reader cannot drag one past its container, and stops iOS inflating text on rotate.

Theme

Three states, not two. Light is the default, so an unstyled first load with no class and no system preference is a light page. A pinned choice writes .theme-light or .theme-dark on the document element and beats prefers-color-scheme in both directions. Clearing the choice removes the class and hands control back to the operating system, which is a state most implementations drop and most readers eventually want.

Change it, then read the ratios below
Theme

Right now this page is .

Show codeHide code

The control is a radio group and not a two-state toggle, because a toggle cannot express "follow the system" without a third press that reads as a bug. What makes the pinned class win is not source order: the prefers-color-scheme: dark block targets :root:not(.theme-light), so a pinned light theme is never matched by it in the first place, and the pinned blocks are two-class selectors that outrank the plain :root default wherever they sit.

Surfaces

Eight surfaces, re-declared per theme. They are lifted verbatim from the products this framework was extracted from and are not open to redesign. Every ink token below is measured against all eight of them, in both themes, and the worst case is what decides whether a pairing ships.

Body ink on each surface, measured live
  • --s-main

  • --s-card

  • --s-top

  • --s-side

  • --s-prev

  • --s-hover

  • --s-rail

  • --s-rail-foot

Show codeHide code

Each ratio is --bs-text against that surface, composited the way the browser paints it and compared against the 4.5:1 body floor. The two rail surfaces are the ones to watch when you pin the theme: they are the darkest and the lightest of the eight, which is why they decide most of the worst cases the token comments record.

Ink

Six theme-aware ink tokens, and the reason there are only six is that these are the values that clear AA against every surface. A raw palette colour is not a text colour until it has been measured.

Each one set on the card surface
  • Body and headings

    --bs-text

  • Supporting copy

    --bs-text-secondary

  • Labels and captions

    --bs-text-muted

  • --bs-text-link

  • Positive state

    --bs-text-ok

  • Negative state

    --bs-text-danger

Show codeHide code

These are measured on the card surface only, which is the friendliest of the eight. The figures in the table below are the worst case across all eight in both themes, and the worst case is what decided the token. Pin the theme to light and watch the link and status inks change value rather than merely change shade: they are different colours per theme for exactly this reason.

Semantic ink tokens and the worst case each one holds.
Token Role Why it is not the obvious value
--bs-text Body and headings 20.47:1 on the dark page, 13.82:1 on the worst dark surface.
--bs-text-secondary Supporting copy Uses the tertiary value, not the secondary one.
--bs-text-muted Labels, captions, markers The source secondary measures 4.47:1 on a card and 4.03:1 on hover, so the muted role is filled by a different value at 5.82:1 worst case.
--bs-text-link Links The brand iris tops out at 4.13:1 on the dark page, so links are periwinkle in dark and a darkened iris in light.
--bs-text-ok Positive state as text The fill colour is 1.37:1 as text on light. This is its text-weight sibling.
--bs-text-danger Negative state as text Same story: the fill is 2.28:1 as text on light.

Two borders, for the same reason. --bs-border-color is a decorative hairline at 1.55:1 and may only separate. --bs-border-strong clears the 3:1 non-text floor and is what identifies a control, which is what SC 1.4.11 asks for.

Spacing scale

Eleven steps, drawn here at the size they actually resolve to. Steps 1 to 4 are a linear 4px run because control padding, icon gaps and the boxes that sit next to 1px hairlines have to land on whole device pixels; a geometric run down there produces 6.4px and 10.2px and blurs the hairline it abuts. From step 4 the run turns geometric, alternating 3:2 and 4:3, so the scale doubles every two steps and every value is still an integer pixel at a 16px root.

Bar width is the token, not a picture of it
  • --bs-space-0

  • --bs-space-1

  • --bs-space-2

  • --bs-space-3

  • --bs-space-4

  • --bs-space-5

  • --bs-space-6

  • --bs-space-7

  • --bs-space-8

  • --bs-space-9

  • --bs-space-10

Show codeHide code

Step 0 draws nothing, which is the point of having it: a zero in the scale is what lets a component keep one property and vary the step rather than switch between declaring a gap and not declaring one. The measurements are in pixels because that is what the browser resolved, and they double if you raise your browser's font size, because the scale is in rem.

Radius scale

Each step is roughly the sum of the two below it, so subtracting one padding step from an outer radius lands on the next radius down and nested corners stay concentric instead of drifting. Step 3 is the source project's card radius, which is why every card on this page has that corner.

Real corners at real values
  • --bs-radius-0

  • --bs-radius-1

  • --bs-radius-2

  • --bs-radius-3

  • --bs-radius-4

  • --bs-radius-5

  • --bs-radius-full

Show codeHide code

The last one is a pill and not a circle: a large radius on a rectangle rounds to the shorter side, so it reports 9999px and draws the height. The border on each box is --bs-border-strong rather than the decorative hairline, because a corner you cannot see is not a demonstration of a corner.

Scales

The global scales and the reasoning behind each shape.
Scale Steps Shape
--bs-space-* 0 to 10 Linear 4px to step 4, then geometric. Control padding and icon gaps have to land on whole device pixels or they blur the hairline they abut.
--bs-radius-* 0 to 5, full Each step is roughly the sum of the two below it, so subtracting one padding step from an outer radius lands on the next radius down and nested corners stay concentric.
--bs-z-* 10 bands 100 apart. A component places its own internals at band plus 1 to 99, so no component needs to know another's z value.
--bs-duration-* 1 to 6 80ms to 600ms on a 1.5 ratio. Perceived duration is closer to logarithmic than linear, so a geometric run spaces the steps evenly to the eye.
--bs-container-* 3xs to 2xl, prose In rem so they scale with the reader's root font size. The prose width is in ch because comfortable line length is a function of the font.

Breakpoints are not custom properties, because a media query cannot read var(). The canonical values every file hardcodes are 30em, 48em, 64em, 80em and 90em, in em so they track font scaling rather than device pixels.

Element defaults

Bare selectors only, no classes. Links get an underline by default because SC 1.4.1 forbids colour as the only thing distinguishing a link from surrounding text. Focus is :focus-visible and never :focus, and the ring is drawn outside the box so moving focus never changes layout.

No class on anything below

A heading with no class

A paragraph, an underlined link, some inline code, a Ctrl keycap and small print. Select this sentence to see the selection colour, or press Tab to see the focus ring on the link.


  • Markers keep their colour from the muted ink token.
  • List padding is untouched so the markers are not orphaned.
Show codeHide code

Type scale

Fluid, with no media queries. Every step interpolates from a 320px base to a 1440px cap, so the mobile value is the authored value and widening is continuous rather than stepped. Both ends are bounded: nothing collapses on a phone and nothing keeps growing on a 5K display.

Two ratios, not one. Body-adjacent steps sit near 1.06 to 1.23 so body, lead and h6 stay in the same conversation, while heading steps open to about 1.25 and display jumps 1.42 to buy hierarchy. The ratio also widens with the viewport, because a phone cannot spend 48px on an h2.

Resize the window and watch it interpolate

Display

bs-text-display now on , authored 34 to 68

Heading 1

bs-text-h1 now on , authored 30 to 48

Heading 2

bs-text-h2 now on , authored 26 to 38

Heading 3

bs-text-h3 now on , authored 22 to 30

Heading 4

bs-text-h4 now on , authored 19 to 24

Heading 5

bs-text-h5 now on , authored 17 to 20

Heading 6

bs-text-h6 now on , authored 16 to 18

Body

bs-text-body now on , authored 15 to 17

Small

bs-text-small now on , authored 13 to 14

Caption

bs-text-caption now on , authored 12 to 13

Show codeHide code

The two figures on each line are read out of the live cascade, so at 320px they are the authored mobile values and at 1440px they are the caps. Drag the window across and they change continuously, because there is not a media query in the file.

A step is a size, a leading and a tracking together. Setting the size on its own is what produces 48px type on 1.6 leading. Line height falls as size rises, and tracking goes negative for display and positive for captions, because type set large needs tighter tracking than type drawn for text sizes.

Type utilities

Everything below lives in bare.utility, the last layer, so no utility in this framework needs !important to beat a component.

Named roles

Eyebrow, uppercase and tracked wide

Display

A lead paragraph. Larger, looser, and capped at a shorter measure than body copy so it reads as an introduction rather than as a first paragraph.

Body copy is capped at 66ch. Below about 45ch the eye returns too often, above about 75ch it loses the start of the next line, and line length is the largest single readability lever that almost nothing ships.

Monospace, optically corrected against the sans face.

Tabular figures 1234567890 for columns that must align.

Caption, the quiet role.

Show codeHide code

Also available: bs-measure-wide, bs-measure-lead, bs-measure-heading, bs-measure-display, bs-measure-none, the bs-leading-* and bs-tracking-* families, bs-weight-body|medium|heading, bs-balance, bs-pretty, and bs-flow-none and bs-flow-tight as rhythm escape hatches.

The framework self-hosts nothing. Inter is named first in the sans stack and a metric-matched fallback face sits directly behind Inter, so a page where neither webfont is present does not reflow when text renders.