Block Kit
klehm/content-blocks-kit is an optional package of 19 ready-to-use block types for ContentBlocks. Install it and you get a full block palette out of the box — no configuration required.
The kit is self-contained: no Tailwind, no Bootstrap, no LiipImagine, no icon library. Every block renders neutral cb-kit-* markup styled by a single shipped stylesheet, so it drops into any host regardless of its CSS setup.
The 19 blocks
titletextrich_textimagegallerybuttonbutton_groupcardlisticonalertdivideraccordiontableembedvideobreadcrumbhtml_rawtabs| Block | What it is |
|---|---|
title | Heading with a visual size decoupled from its semantic tag |
text | Plain paragraph text with a palette color |
rich_text | WYSIWYG rich text, on TinyMCE or CKEditor |
image | Image with size, fit, align, link, caption, rounded corners |
gallery | Image grid or arrow slider |
button | Call-to-action button (variants, sizes, alignment) |
button_group | One to three buttons in a row, sharing size and alignment |
card | Image/title/text/button tiles as a grid or list |
list | Bulleted / checkmark / numbered list |
icon | A single icon from the shipped icon set |
alert | Info / success / warning / error callout |
divider | Horizontal rule (style + color) |
accordion | Collapsible panels (native <details>, zero JS) |
table | Columns + rows data table |
embed | Responsive YouTube / Vimeo embed |
video | Self-hosted video file in a native <video> player |
breadcrumb | Breadcrumb trail |
tabs | Tabbed panels |
html_raw | Raw-HTML escape hatch (disabled by default) |
Each block has its own reference page with its full configurable surface — see the sidebar.
Installation
composer require klehm/content-blocks klehm/content-blocks-kitThe blocks are auto-registered via Symfony autoconfiguration — no config needed to get all of them (except html_raw, which is disabled by default).
Front stylesheet (required)
Kit blocks render with neutral cb-kit-* classes styled by a stylesheet the kit serves at a public route. Include it once in your front layout (it also flows into the builder preview):
<link rel="stylesheet" href="{{ cb_kit_stylesheet_url() }}">The URL carries a hash of the file, so browsers and CDNs keep it for a year and fetch the new one after an upgrade. A plain path('content_blocks_kit_asset_css') works too, revalidated every five minutes.
Retheme by overriding the --cb-kit-* custom properties (set them on :root), or the classes in your own stylesheet.
Stimulus controllers
Two blocks need a Stimulus controller. Enable them in your host assets/controllers.json under the @klehm/content-blocks-kit package — rich_text has one per selectable editor, so enable the one you configured (or both, if you are unsure):
| Controller | Needed by |
|---|---|
cb-tinymce | rich_text on its default editor |
cb-ckeditor | rich_text when options.editor: ckeditor |
cb-gallery | gallery (slider layout only) |
Colors
Every color field in the kit — icon and divider colors, the title and text blocks' text color, and the rich-text editor's swatches — draws from the one core palette declared in content_blocks.palette. Add a named color there once and it appears everywhere:
# config/packages/content_blocks.yaml
content_blocks:
palette:
- { label: 'Brand', color: '#eb0540' }The kit's own tokens
The palette covers what an editor picks per block. What a developer sets once, for every kit block on the site, is a set of seven CSS custom properties declared by kit.css:
| Token | Default | Drives |
|---|---|---|
--cb-kit-primary | #4f46e5 | primary button fill, list markers, icon default, breadcrumb links |
--cb-kit-primary-contrast | #ffffff | text on a primary fill |
--cb-kit-secondary | #64748b | secondary button fill |
--cb-kit-secondary-contrast | #ffffff | text on a secondary fill |
--cb-kit-border | #d1d5db | divider, card, table and accordion rules, gallery arrows |
--cb-kit-text | #1f2937 | alert text |
--cb-kit-radius | 8px | corner radius across the kit |
They are declared once, on :where(:root), and :where() carries zero specificity. Set them on :root for the whole site, or on any container for part of it; either wins without !important, whatever order the stylesheets load in:
/* your front stylesheet */
:root {
--cb-kit-primary: #eb0540;
--cb-kit-radius: 2px;
}
.dark-band {
--cb-kit-border: #334155;
}An alert's colours come from its tone (info, success, warning, error), not from these tokens: restyle .cb-kit-alert--<tone> for those.
These names are public surface, covered by the package's semver guarantee. Note they style content, on the published page — they have nothing to do with the --cb-* tokens that theme the builder chrome (styling guide).
One block adds tokens of its own, because its colors have no equivalent elsewhere in the kit — tabs, whose open tab has to sit on the panel it opens:
| Token | Default | Drives |
|---|---|---|
--cb-kit-tabs-line | #e5e7eb | panel border, and the rule under the tab row |
--cb-kit-tabs-tab | #6b7280 | a closed tab's label |
--cb-kit-tabs-tab-active | #1f2937 | the open tab's label (and any hovered one) |
--cb-kit-tabs-panel-bg | #ffffff | panel fill, reused by the open tab so it joins the panel |
--cb-kit-tabs-accent | #4f46e5 | keyboard focus ring |
Same rule as above: declared on :where(:root), so :root or .cb-kit-tabs { --cb-kit-tabs-line: … } in your stylesheet wins.
Extending a kit block
Overriding a template changes what a block renders. When you need it to edit something the kit does not offer — one extra field, a different default, a narrower choice set — subclass the block instead. This is a supported path: the 19 block classes are deliberately non-final, and their protected methods are covered by the package's semver guarantee.
Disable the kit's service and register yours in its place, keeping the same type id so stored content keeps working:
# config/packages/content_blocks_kit.yaml
content_blocks_kit:
blocks:
button: { enabled: false }#[AsContentBlock]
final class AppButtonBlock extends ContentBlocks\Kit\Block\ButtonBlock
{
public function buildForm(FormBuilderInterface $builder, array $data): void
{
parent::buildForm($builder, $data);
$builder->add('trackingId', TextType::class, ['required' => false, 'data' => $data['trackingId'] ?? '']);
}
protected function defaults(): array
{
return parent::defaults() + ['trackingId' => ''];
}
}Four things to know before you do:
getDefaultData()isfinal. It mergesdefaults()with the host'scontent_blocks_kit.blocks.<type>.defaultsconfig, so replacing it would silently drop that config. Growdefaults()instead.Keep
getType()inherited, and disable the kit's service. Two services claiming one type id is a silent conflict — the registry keeps whichever was registered last, and which one that is depends on container order.The config still applies.
enabled: falseonly un-registers the kit's service;options,choicesanddefaultsdeclared under that same type reach your subclass, merged over its own coded defaults. So subclassing to add one field does not cost you the configuration of everything else:yamlcontent_blocks_kit: blocks: divider: enabled: false # yours takes over choices: { style: { solid: 'Solid', double: 'Double' } } defaults: { style: 'double' }A subclass that gives itself a new
getType()is configured under that new id instead — the config is keyed by the type, never by the class.The type is read while the container is compiled, before any service exists, so a kit subclass's
getType()must return a constant: it cannot depend on its constructor.In tests, take the block from the registry. Since the config arrives through the constructor,
new YourBlock()yields the coded surface — the block as its class defines it, with none of your YAML. That is the right object for a unit test ofdefaults()orchoiceFields(), and the wrong one for asserting anything you configured: the assertion then describes the class, not the application, and passes or fails for a reason that has nothing to do with your config. AskBlockTypeRegistryinstead, and you get the instance the app actually renders with:php$block = self::getContainer()->get(BlockTypeRegistry::class)->get('divider');
The full contract, including choiceFields() and describe(), is in the package README.
Discovering the surface from the CLI
Every block's options, choice fields (default marked *) and data defaults are introspectable — read straight from the code, so the output never goes stale:
bin/console content-blocks-kit:blocks # all blocks, human-readable
bin/console content-blocks-kit:blocks button # one block
bin/console content-blocks-kit:blocks --format=json # machine-readable (drives these docs)The per-block reference pages in this section are generated from that JSON, so they always match the installed version.
Next
- Configuring blocks — the four per-block levers (
enabled,options,choices,defaults) and how they combine. - Individual block pages — see the sidebar for all 19.
- Overriding block templates — swap any block's markup.