This page describes how BlockSuite blocks take their look from the theme, their public markup and CSS hooks, and the JavaScript behaviour, so a theme can restyle or extend them.
Colors come from the theme
Every block root carries the bst-block class, which defines a small set of --bst-* custom properties. Each one reads a --theme-* token when the theme provides it, and otherwise falls back to the browser's system colors (Canvas, CanvasText, LinkText), so blocks suit any light or dark theme even with no tokens at all.
| BlockSuite token | Reads | Fallback | Used for |
|---|---|---|---|
--bst-surface |
--theme-surface |
Canvas |
Cards, inputs, slider buttons |
--bst-surface-alt |
--theme-surface-alt |
6% CanvasText on Canvas |
Tinted panels, image placeholders, secondary buttons |
--bst-text |
--theme-text |
CanvasText |
Text |
--bst-text-muted |
--theme-text-muted |
72% CanvasText |
Descriptions, labels, inactive tabs |
--bst-accent-text |
--theme-primary-text |
--bst-text |
Statistics figures, timeline dates |
--bst-border |
--theme-border |
20% CanvasText |
Card and list borders |
--bst-border-strong |
--theme-border-strong |
55% CanvasText |
Input and button borders (3:1 for form controls) |
--bst-accent |
--theme-primary |
LinkText |
Primary buttons, active tab, hero fill, timeline markers |
--bst-on-accent |
--theme-on-primary |
Canvas |
Text on --bst-accent |
--bst-dark |
--theme-dark |
#1b1f24 |
Dark call to action panel |
--bst-on-dark |
--theme-on-dark |
#fff |
Text on --bst-dark |
--bst-link |
--theme-link |
LinkText |
Outline buttons, suggestion links |
--bst-radius |
--theme-radius |
8px |
Corners (buttons use --theme-radius-button first) |
--bst-gap |
1.5rem |
Space between cards | |
--bst-touch |
2.75rem |
Minimum size of buttons and tabs (44px touch targets) |
For theme authors
To theme every BlockSuite block at once, define the --theme-* tokens in your theme, for example on :root. Make each text/background pair meet WCAG contrast: --theme-text, --theme-text-muted, --theme-link and --theme-primary-text at 4.5:1 on --theme-surface and --theme-surface-alt, --theme-on-primary on --theme-primary, --theme-on-dark on --theme-dark, and --theme-border-strong at 3:1. The Vitrine theme computes all of these from its color settings.
Custom properties resolve where they are declared, so if your theme re-colors a section (a dark band, a colored background), declare the --theme-* tokens again on that section with the section's colors. Blocks inside then follow it automatically.
To restyle BlockSuite alone, set --bst-* tokens instead, on .bst-block or on one block type:
.bst-block { --bst-radius: 0; --bst-gap: 2rem; }
.bst-cta { --bst-dark: #20123a; --bst-on-dark: #fff; }
Assets
See Configuration for which stylesheet loads with which block. All styles are plain, unlayered CSS with low specificity (.bst-* classes), so a theme stylesheet loaded later overrides them.
Shared pieces
| Class | Element |
|---|---|
.bst-block |
Root element of every block; defines the tokens |
.bst-button |
Button link; modifiers --secondary, --outline, --small. Hover underlines rather than changing colors, so contrast holds in any theme |
.bst-buttons |
Row of buttons (<p>) |
.bst-sr-only |
Visually hidden text for screen readers |
html.bst-js |
Added by a one-line head script when JavaScript runs; without it, tab and accordion panels show open and sliders scroll |
Card Slider and Item Slider
<div class="bst-block card-slider-block" role="region" aria-roledescription="carousel" aria-label="…"
data-autoplay="true" data-interval="4000" data-visible="3" style="--cards-visible: 3"
data-label-position="Showing %1$s to %2$s of %3$s" data-label-pause="…" data-label-play="…">
<h2 class="card-slider-block__heading">…</h2>
<button class="card-slider-block__rotation bst-button bst-button--small bst-button--outline" type="button">Stop automatic slide show</button>
<div class="card-slider-block__viewport">
<div class="card-slider-block__track">
<div class="card-slider-block__card" role="group" aria-roledescription="slide" aria-label="1 of 5">
<div class="card-slider-block__inner">…</div>
</div>
</div>
</div>
<button class="card-slider-block__prev" type="button" aria-label="Previous slide">‹</button>
<button class="card-slider-block__next" type="button" aria-label="Next slide">›</button>
<div class="card-slider-block__live bst-sr-only" aria-live="polite" aria-atomic="true"></div>
</div>
In grid display the wrapper gets card-slider-block--grid, the cards are <li class="card-slider-block__card"> in a <ul class="card-slider-block__grid">, and there are no buttons or carousel roles. The Item Slider adds item-slider-block to the wrapper.
| Class | Element |
|---|---|
.card-slider-block__track |
Flex row with gap: var(--bst-gap), moved with transform |
.card-slider-block__card |
One card; (100% - gaps) / var(--cards-visible) wide |
.card-slider-block__inner |
Card surface (--bst-surface, --bst-border, --bst-radius) |
.card-slider-block__image |
Card Slider image, 4:3, object-fit: cover |
.card-slider-block__content, __title, __desc, __link |
Card Slider text and button |
.item-slide__thumbnail, .item-slide__placeholder |
Item image, or a plain placeholder panel |
.item-slide__meta |
Item properties |
.bst-item-prop, .item-prop--dcterms-date, … |
One property line, with a per-term modifier (.item-prop is kept for older theme CSS) |
.item-slide__title |
The item title |
.bst-item-prop__label, .bst-item-prop__value |
Property label and value (.prop-label, .prop-value kept too); machine dates are a <time datetime> inside the value |
.item-slide__link |
Item button |
Behaviour (sliders only):
- The script measures the block's width and shows as many cards as fit at about 15rem each, up to
data-visible, by setting--cards-visibleon the wrapper. It re-measures when the block resizes. - Previous, next, ArrowLeft/ArrowRight (while focus is in the slider), swipes on touch screens and autoplay all move one card and wrap around at the ends.
- Off-screen cards get the
inertproperty. - The live region announces
data-label-positionafter a visitor moves the slider; it is set toaria-live="off"while autoplay runs. - Autoplay pauses on hover and stops when keyboard focus enters the slider; the rotation button toggles it. It never starts when
prefers-reduced-motion: reducematches.
Tabs (Tabs (HTML) and Tabs (Blocks))
<div class="bst-block bst-tabs-block" id="bst-tabs-{blockId}">
<h2 class="bst-tabs-block__heading">…</h2>
<div class="bst-tabs-block__intro">…</div> <!-- Tabs (Blocks) only -->
<div role="tablist" class="bst-tabs-block__tablist" aria-label="…">
<button role="tab" id="bst-tab-{blockId}-0" aria-controls="bst-panel-{blockId}-0"
aria-selected="true" tabindex="0" class="bst-tabs-block__tab" type="button">…</button>
</div>
<div role="tabpanel" id="bst-panel-{blockId}-0" aria-labelledby="bst-tab-{blockId}-0"
class="bst-tabs-block__panel bst-tabs-block__panel--active" tabindex="0">…</div>
<div role="tabpanel" id="bst-panel-{blockId}-1" … class="bst-tabs-block__panel" hidden tabindex="0">…</div>
</div>
A Tabs (Blocks) container adds bst-tabs-block--group and is wrapped in the standard block <div> (with grid classes on grid pages). The blocks inside keep their own block wrappers.
Clicking a tab, or ArrowLeft, ArrowRight (wrapping), Home and End on a focused tab, activates it. Each .bst-tabs-block manages only its own tabs, so tab blocks nest. The default tab is set in the markup, so the page renders correctly before the script runs.
Accordion display
<div class="bst-block bst-accordion" id="…" data-allow-multiple="false">
<h2 class="bst-tabs-block__heading bst-accordion__heading">…</h2>
<div class="bst-accordion__item">
<h3 class="bst-accordion__header">
<button type="button" class="bst-accordion__trigger" id="…-button-0" aria-expanded="true" aria-controls="…-panel-0">
<span class="bst-accordion__label">…</span><span class="bst-accordion__icon" aria-hidden="true"></span>
</button>
</h3>
<div class="bst-accordion__panel" id="…-panel-0" role="region" aria-labelledby="…-button-0">
<div class="bst-accordion__content">…</div>
</div>
</div>
<!-- closed items: <div class="bst-accordion__panel" … hidden="until-found"> -->
</div>
Item headings are <h3> under a block heading and <h2> otherwise. Panels are regions only when there are six items or fewer. A Tabs (Blocks) accordion adds bst-accordion--group.
Closed panels use hidden="until-found": browsers that support it keep the panel's text findable with find-in-page and fire beforematch, which opens the section. Do not set display: none on .bst-accordion__panel[hidden="until-found"] in your theme, or find-in-page breaks; style spacing on .bst-accordion__content instead. ArrowUp, ArrowDown, Home and End move between section buttons.
Columns (Blocks)
<div class="block block-bstColumns …">
<div class="bst-block bst-columns" id="bst-columns-{blockId}" data-gap="medium" data-align="stretch">
<h2 class="bst-columns__heading">…</h2>
<div class="bst-columns__intro">…</div>
<div class="bst-columns__grid" style="--bst-column-tracks: repeat(3, minmax(0, 1fr))">
<div class="bst-columns__column">…blocks…</div>
</div>
</div>
</div>
Below 48em the grid is a single column; from 48em it uses --bst-column-tracks. data-gap is small, medium or large (--bst-col-gap), data-align is stretch, start, center or end.
Each .bst-columns__column is a flex column that stacks its blocks with gap: var(--bst-gap); the blocks' own vertical margins are removed inside columns. With data-align="stretch", the last block in each column (and a BlockSuite block that is its only content) grows to the column's full height.
Hero
<section class="bst-block bst-hero bst-hero--image" data-height="standard" data-align="start" data-scrim="standard" aria-labelledby="bst-hero-{blockId}">
<img class="bst-hero__image" src="…" alt="…" style="object-position: center center">
<div class="bst-hero__content">
<p class="bst-hero__eyebrow">…</p>
<h2 class="bst-hero__heading" id="bst-hero-{blockId}">…</h2>
<p class="bst-hero__text">…</p>
<p class="bst-buttons bst-hero__buttons">…</p>
</div>
</section>
data-height is compact (40vh), standard (60vh), tall (80vh) or screen (100svh), capped at 48rem except screen. With an image, ::before lays a gradient between 60% and 80% black (data-scrim="strong": 74% to 88%) over the photo; keep it at least 60% black if you restyle it, so white text stays at 4.5:1. Without an image the hero uses --bst-accent and --bst-on-accent.
Call to Action, Statistics, Timeline, Search Box
| Block | Root | Hooks |
|---|---|---|
| Call to Action | .bst-cta[data-background][data-align] |
__heading, __text, .bst-cta__buttons. data-background is none, tinted, accent or dark; on accent and dark the buttons, links and focus ring switch to the panel's text color |
| Statistics | .bst-stats[data-style][data-align] |
__heading, __intro, __list (<ul>), __item, __value, __label, __description. data-style is plain or cards |
| Timeline | .bst-timeline |
__heading, __intro, __list (<ol>), __event (marker in ::before), __date (with <time datetime> for years and ISO dates), __body, __title, __image, __text, __link |
| Search Box | .bst-search |
__heading, __text, __form (role="search"), __label, __row, __input (type="search", name="fulltext_search"), __button, __suggestions |
JavaScript
asset/js/bst.js is plain JavaScript with no dependencies, loaded with defer. It initializes every slider, tab block and accordion on the page once (each root gets data-bst-ready).
| API | Description |
|---|---|
BlockSuite.init(root) |
Initializes blocks inside root (default document). Call it after inserting BlockSuite markup into the page. |
BlockSuite.panelShown(panel) |
Fires the bst:panel-shown event on panel and a window resize event. |
bst:panel-shown event |
Bubbles from a tab panel or accordion panel when it opens; event.detail.panel is the panel. Maps, charts and sliders inside hidden panels can listen for it (or for resize) to redraw at their visible size. |
BlockSuite.version |
The script version. |
Overriding templates
Copy any template from view/common/block-layout/ or view/common/blocksuite/ into the same path in your theme. The variables each template receives are listed in Block Data. If you keep the class names, role attributes and id patterns, the bundled script keeps working.
For the non-container blocks, a theme can instead declare alternative templates under block_templates in its theme.ini (Omeka S 4.1 block templates); editors then choose one under Configure layout, and the template receives the same variables.