Component Library 2.13.1 (configured with Dummy App)

Previews

      • Button
      • Hyper Link
      • Icon
        • Combobox
        • Email
        • Radio
        • Select
        • Telephone
        • Text
        • Textarea
      • Navigation Banner
      • Paginator
      • Article
        • Default
        • Hero Only
        • Multi Panel
      • Article Listing
      • Carousel
      • Fault Tolerance
        • Default
        • Mobile Accordion
      • Header
        • Alignment
        • Background Hero With Breadcrumbs
        • Default
        • Alignment
        • Default
      • Article Summary Card
        • Default
        • Error
        • Filled
        • No Legend
        • Broken Embed
        • Default
      • Image
      • List
        • Default
        • Quote On Surface
        • With Iframe Media
        • With Media Fit
        • Default
        • Quote On Surface
  • Style Guide

No matching results.

Pages

  • Developer Guide
  • Overview
  • Colours
  • Typography
  • Spacing
  • New Components
  • Interactive Behaviour
  • Component Variants
  • CSS Utilities
  • Extending Components
  • CSS Boundaries

No matching results.

CSS Boundaries

CSS boundaries in the library

The one rule: a container places its children and publishes context; it never styles their internals. This page is what that rule means when you write or change a stylesheet here (BOU-2863 to BOU-2902, released in 2.10.0). The host-facing half, what a mounting app may do to these styles, is the orchestrator's docs/gll/css-boundaries/ topic.

Ownership per tier

Stylesheet dir Owns May set on a child
panels/ the .panel section, its surface, .panel__header, the row/cell grid placement of cells only
patterns/ the pattern's box and every element inside it placement of the atoms it composes
components/ the atom and all of its variants nothing
styles/ base: resets, forms, typography defaults, :root tokens nothing
utilities/ mixins and functions; emits no CSS nothing
pages/ page-level composition (Lookbook docs, article page) placement only

Placement is grid-area, flex, order, the box you hand the child (width / height), gap, margin between children, alignment. A panel that writes color, font-*, padding or a .child__element selector for a pattern is reaching through. Move the rule into the pattern and, if the panel needs the pattern to look different there, declare a variant.

The cell

.panel__cell (panels/_panel.scss) is the container a pattern reads its width from: display: grid; min-width: 0; container-type: inline-size. Cards stack their media at @container (width < 300px). Every place that lays patterns out wraps each in a cell: Panels::PatternComponent, Layout::GridComponent, Panels::ArticleListingComponent. A new layout does the same; a pattern never assumes it is inside a .panel.

The surface contract

utilities/_mixins.scss: light-surface($background, $raised: null), dark-surface($background, $raised: null), scrim-surface, raised-surface($fallback), inherit-surface. They publish --surface-ink, --surface-headline, --text-link, --surface-accent, --surface-tint and optionally --surface-raised; :root (styles/_colours.scss) carries the light defaults so a pattern rendered bare still reads something sane.

  • Every painted surface calls a mixin: panels, footer, hero, the menu submenu, the language switcher, inputs, the combobox listbox, pagination, list backgrounds.
  • null publishes without painting, for a surface something else paints (a UA-painted control, a host-painted background).
  • Text over a photo is scrim-surface: the dark contract with headline and links kept at --text-light. The hero's background media and the image-background card use it. A painted dark panel uses dark-surface, whose headline is the accent tint.
  • A card on any surface is raised-surface($fallback): fills from --surface-raised when the surface publishes one, else the fallback, and publishes a light surface inside. Cards call it; panels never do.
  • Children read tokens. color: var(--text-dark) inside a pattern is a bug unless the element is a sibling of the surface it sits on (the combobox icons are the one known case; it is commented).
  • Retired for good: --link, --swiperbullet, per-panel [data-bgc] .child { color } blocks.

Declare, don't infer

A variant is a kwarg, validated with declared(name, value, allowed) on ApplicationComponent, emitted as data-*, read by the component's own file:

VARIANTS = %i[default bar stretch action menu compact attached].freeze
@variant = declared(:variant, variant, VARIANTS) # → data-variant unless default

Declared attributes today: data-variant, data-icon, data-dropdown, data-fit, data-leading, data-title-link, data-align, data-media-type, data-layout-style, data-media-alignment, data-media-size. Editor alignment (BOU-2939) emits data-align (left / center / right) on .panel__title and .panel__subtitle only when it overrides the header, and on .panel__body and .panel__button. Use :has() only for state unknown at render time (JS-toggled); never to detect what the Ruby already knows. BOU-2864 retired the :has(.material-symbols-outlined) icon inference, and two hosts' hand-written icon links lost their flex row because they declared nothing: an inferred rule hides who depends on it.

Variants belong to the component; containers name them

Two ways a container asks for a variant, both without touching the child's rules:

  1. It renders the child: pass the kwarg (ButtonComponent.new(button:, variant: :bar)).
  2. A host renders the child into the container's slot, so the container cannot pass a kwarg: the container includes the child's variant mixin on the slot.
.header__better-action-links .button { @include button-action; }

The mixins live in a CSS-free module (components/_button-variants.scss) that both _buttons.scss and the panel @use. Mixins, not %placeholders: @extend across modules fails the build ("target selector was not found"). CSS-free, because a partial that @uses a module with CSS re-emits that CSS inside the loading layer (pages/_component-ref.scss once duplicated the whole base sheet in the pages layer this way).

Cascade layers

application.scss declares @layer vendor, base, components, patterns, panels, pages, utilities and loads each directory with meta.load-css into its layer. Consequences:

  • Later layers win regardless of specificity, so a panel's placement rule reliably beats a pattern's default. That is the feature.
  • A generic element rule in a higher layer beats a class rule in a lower one: .panel span { display: inline } beat .icon { display: block } and cost the footer toggle 6px. Higher tiers write no bare element rules for things a lower tier owns. Icon rules live in components/_icons.scss for this reason.
  • !important is gone from the library and stays gone. Hosts are unlayered and beat us anyway; inside the library it only ever won a fight the ownership rule should have prevented.
  • utilities/_index.scss forwards mixins and functions only. CSS added there lands in the top layer and outranks everything.

Media: the card owns the box, the media owns the fit

.media-card__media is the card's box (size, aspect, flex: none). Inside it the image, video or iframe declares how it fills the box with fit: (auto | contain | cover | overlay), read by components/_images.scss and friends. A video keeps its own ratio and the box follows it. A card never sets object-fit on an img; an image never sets its own box. Patterns.cell_options(type) is where a pattern type asks its cell for special treatment.

Composition over duplication

The media card renders a TextCardComponent with layout_style: "nested" for its text half (BOU-2904, BOU-2909). One set of text-card rules, in the text card, keyed by the nested variant plus leading: / meta: / title_link: knobs. When a pattern needs another pattern's look, it composes that pattern; it never copies its rules.

Native controls

styles/_forms.scss resets <button> to a plain control (no background, border, padding, margin or font of its own; inherits its text); the skin is .button only. Icon-only buttons (burger, footer toggles, accordion headers) inherit their text and set only what they need. Do not restore button, .button skins.

Public hooks

What a host may select, and therefore what a rename must announce in CHANGELOG.md under "Markup / hooks": a component's root class, its declared data-* attributes, the surface and palette tokens, the utilities mixins. Element classes (.button-content, .media-card__media), DOM order and private mixins are internals. Every release entry leads with the hooks that changed; hosts read that list before they read anything else.

Verifying a stylesheet change

Grepping the sheet is not evidence. scripts/css_verify/ (README there) dumps computed style and geometry for every element on a URL list and diffs two dumps. Before and after, at 1300px and 400px:

  1. Dump the Lookbook scenarios and the dummy's live pages. Diff. Expect zero change except differences you can name and justify in the PR body.
  2. Drive hidden states first: open the mobile menu, expand a footer group, open a dropdown. display:none has no geometry on either side and diffs clean when broken.
  3. When a change moves boxes, run the geometry probe alongside the style dump.
  4. Rebuild the sheet (rails dartsass:build in spec/dummy) before dumping; the running dummy hot-reloads components but not the compiled CSS.
  5. The dummy does not render everything (no language switcher, no host-rendered action links). What it cannot show, a host must: cut a pre-release and ask each host for the same diff against its previous pin before the release proper.

Reviewing a stylesheet diff

Ask, for every added or moved rule:

  • Which tier owns the element this selects? Is the rule in that tier's file?
  • Does a container set anything on a child beyond placement?
  • Is any colour or type value a literal, or a --text-* token where a --surface-* token should be read?
  • Is a variant inferred from markup where the Ruby could declare it?
  • Is there a bare element selector in panels/ or pages/?
  • Does a partial @use anything other than utilities or a CSS-free mixin module?
  • Is there an !important, or a selector written to out-specify another library rule?
  • Did a public hook change, and is it in CHANGELOG.md?
  • Did the PR body list the computed-style differences and justify each?
Extending Components