WP Manifestindependent plugin directory
manifest / editor / reusable-gutenberg-block-styles

Reusable Gutenberg Block Styles

Registers reusable Gutenberg block styles (.is-style-* classes) for core and custom blocks from one central configuration file, loading only the CSS a page actually needs, on both the frontend and in the block editor.

by Studio Zonder Meer · github.com/yelbow/reusable-gutenberg-block-styles · website

★ 0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/yelbow/reusable-gutenberg-block-styles/archive/refs/heads/main.zip

One plugin that registers custom Gutenberg block styles (the choices in a block's Styles panel) for many sites at once, plus the default look of those blocks. Code lives in Git, styles live in CSS files, and every site can switch individual styles on or off without forking the plugin.

This repository is the plugin: reusable-gutenberg-block-styles.php sits at the top level, so you can zip the repo, upload it, or point the optional GitHub updater at it.

Group         →  Default · Card · Feature · Dark · Section · Full viewport · Padding S/M/L
Columns       →  Wide gap · Tight gap
Column        →  Card
Button        →  Default · Outline · Pill · Link
Buttons       →  Equal width
Image         →  Default · Rounded · Rounded corners · Shadow
Heading       →  Eyebrow
Paragraph     →  Lead
Cover         →  Panel
Query         →  Framed
Post Template →  Cards
List          →  Chips
Post Terms    →  Chips
Tag Cloud     →  Chips

The Button and Image lists include the styles core already ships (Default, Outline, Rounded) to show where they come from. This plugin deliberately does not re-register those, see Where this meets core's own styles.

Why this exists

The usual alternative is register_block_style() plus a pile of .is-style-* rules per site, in each child theme, copy-pasted forward. That drifts: four sites, four slightly different Cards, and every fix has to be made four times.

Here the registration lives in one configuration array, the CSS lives in one folder per block, and a site only ever decides which styles it wants. Updating the plugin updates the design system on every site that has it installed; templates and content stay in the site's own theme where they belong.

It uses register_block_style() and nothing else. No editor UI is rebuilt, no block patterns, no block variations, no block templates, no settings screen.

Install on a site

  1. Copy this folder into wp-content/plugins/reusable-gutenberg-block-styles/ (the folder name matters for updates), or upload a zip via Plugins → Add New → Upload Plugin.
  2. Activate.
  3. Open any Group block, look at Styles: Card, Feature, Dark, Section, Full viewport and the Padding variants are there.

Nothing else is needed. The plugin creates no database tables, no options, no custom post types, no roles, and has no admin screen. Deactivate to remove it; uninstall.php only clears the update-check transient.

Package it as a zip

bin/build-zip.sh          # writes dist/reusable-gutenberg-block-styles-1.0.0.zip

The script reads the version from the plugin header, so bumping Version: is the only step needed before a release. .distignore decides what is left out (Git metadata, the dist folder itself, editor config).

Updates across sites

By default the plugin never phones home. To update sites from GitHub releases instead of uploading zips by hand, point it at a repository, either in the site's wp-config.php:

define( 'RGBS_GITHUB_REPO', 'studiozondermeer/reusable-gutenberg-block-styles' );

or from a mu-plugin / child theme:

add_filter( 'rgbs_github_repo', fn() => 'studiozondermeer/reusable-gutenberg-block-styles' );

From then on the repository's latest release shows up in that site's Plugins/Updates screen exactly like a WordPress.org plugin, and updating is a normal one-click update from wp-admin. Publish a release with a version tag (v1.0.0) and put reusable-gutenberg-block-styles.zip in the release assets, or let it use GitHub's own zip of the tag. Private repositories and API rate limits are handled with an optional define( 'RGBS_GITHUB_TOKEN', '...' ), or the rgbs_github_token filter.

This is deliberately small and dependency-free. If you want the same behaviour with a maintained third-party library, the Plugin Update Checker library used in the SZM Admin Menu Manager does the same job with more polish.

The configuration file

Everything you normally touch is in config/styles.php. It returns one array:

return array(
    'block_styles'   => array(
        'core/group'  => array(
            array( 'name' => 'card', 'label' => __( 'Card', 'reusable-gutenberg-block-styles' ) ),
        ),
    ),
    'default_styles' => array(
        'core/group' => array( 'css' => array( 'frontend' => 'blocks/group/default.css' ) ),
    ),
);

A flat array keyed by block name also works ('core/group' => array( ... ) with no block_styles wrapper); it is treated as the selectable styles.

Each style accepts:

Key What it does
name Required. Becomes the CSS class .is-style-{name}.
label Shown in the Styles panel. Pass a translated string. Omitted: derived from name.
enabled false switches the style off for this site. Default true.
css array( 'frontend' => ..., 'editor' => ... ), or a single string. Paths are relative to assets/css/, or absolute.
inline_style Raw CSS string instead of a file.
style_data theme.json-style array (WP 6.6+); WordPress generates the CSS from your design tokens.
is_default Marks this style as the one the editor treats as active when no style class is set.
override Allow replacing a style that is already registered for that block. See below.
args Anything else you want to pass straight to register_block_style().

A bare slug works too: 'card' is the same as array( 'name' => 'card' ), with the label derived from the slug.

Add a style in three steps

  1. Add an entry in config/styles.php:
'core/group' => array(
    // ...
    array(
        'name'  => 'sticky-note',
        'label' => __( 'Sticky note', 'reusable-gutenberg-block-styles' ),
    ),
),
  1. Create assets/css/blocks/group/sticky-note.css. The path is derived from the block and the style name, so you only need to remember the pattern.

  2. Scope the selector to the block, never to the class alone:

.wp-block-group.is-style-sticky-note {
    background-color: var(--wp--preset--color--accent-4, #fff8d6);
    padding: var(--wp--preset--spacing--40, 1.5rem);
    rotate: -1deg;
}

.is-style-sticky-note on its own would also hit a Column, an Image or anything else using the same slug. Every file in this plugin is scoped that way.

Where the CSS files go

assets/css/
├── blocks/
│   ├── <block>/<style>.css           frontend + editor canvas
│   ├── <block>/<style>-editor.css    editor only
│   └── ...
└── templates/                        examples that are not loaded

<block> is the block name without its namespace (core/group → group). For a custom block, my-plugin/fancy-card becomes my-plugin--fancy-card, or point css at a path of your own.

A missing file is not an error. A style can be label-only, or carry its CSS through inline_style or style_data. Two blocks that should share one stylesheet can both point at the same file:

'core/column' => array(
    array(
        'name' => 'card',
        'label' => __( 'Card', 'reusable-gutenberg-block-styles' ),
        'css'  => array( 'frontend' => 'shared/card.css' ),
    ),
),

Styles without a CSS file (style_data)

Core 6.6+ accepts style_data, a theme.json-shaped array, and generates the CSS from the site's global styles. It is supported and passed straight through, and it is the most native way to define a style that should also be editable in the Site Editor's Global Styles panel:

array(
    'name'       => 'lead',
    'label'      => __( 'Lead', 'reusable-gutenberg-block-styles' ),
    'style_data' => array(
        'typography' => array(
            'fontSize'   => 'var:preset|font-size|large',
            'lineHeight' => '1.6',
        ),
    ),
),

The catch, and the reason the shipped styles do not use it: style_data names preset slugs, so it only looks as designed on a theme that uses those slugs. That makes it a good fit for a style belonging to one site and a poor fit for a design system that ships to several. Use it where the slugs are known; use a CSS file with the token chain above where the style has to travel.

Switch a style off per site

Keep the entry, set enabled to false. The style is not registered, no CSS is loaded for it, and the label disappears from the editor:

array(
    'name'    => 'dark',
    'label'   => __( 'Dark', 'reusable-gutenberg-block-styles' ),
    'enabled' => false,
),

That is fine for one site. For several sites running the same plugin checkout, leave the config alone and decide per site from a small mu-plugin, so the plugin files stay identical everywhere:

add_filter(
    'rgbs_block_styles',
    function ( $styles ) {
        foreach ( $styles['core/group'] as $i => $definition ) {
            if ( 'dark' === $definition['name'] ) {
                $styles['core/group'][ $i ]['enabled'] = false;
            }
        }

        return $styles;
    }
);

Extend it from another plugin or theme

Add a style for a block that this plugin does not configure:

add_filter(
    'rgbs_block_styles',
    function ( $styles ) {
        $styles['core/quote'][] = array(
            'name'  => 'bordered',
            'label' => __( 'Bordered', 'my-textdomain' ),
            'css'   => array( 'frontend' => 'blocks/quote/bordered.css' ),
        );

        return $styles;
    }
);

Definitions added here are normalised the same way the config file is, so slugs, labels, enabled, css, inline_style and style_data all behave identically.

Register a style from PHP directly, without any configuration:

\RGBS\BlockStyles::register( 'core/group', 'sticky-note', 'Sticky note' );

\RGBS\BlockStyles::register(
    array(
        'block' => 'core/group',
        'name'  => 'sticky-note',
        'label' => 'Sticky note',
    )
);

Both are safe at any point in the request: before init the call is queued and processed with the rest, after init it happens immediately.

Hooks

Hook Type Purpose
rgbs_config filter The whole normalised configuration.
rgbs_config_path filter Load the configuration from a different file.
rgbs_block_styles filter Add, change or disable selectable styles per block.
rgbs_default_styles filter Add or change default block styling.
rgbs_style_files filter Point a style's CSS at files elsewhere (a theme, a shared folder).
rgbs_style_properties filter Last stop before register_block_style(); change any argument.
rgbs_register_style filter Return false to keep one style out.
rgbs_style_handle filter Change the stylesheet handle of a style.
rgbs_block_slug filter Change the directory slug used for a block's files.
rgbs_stylesheet_url filter Supply a URL for a CSS path WordPress cannot map itself.
rgbs_booted action All components hooked.
rgbs_style_registered action One style registered.
rgbs_style_disabled action A style was skipped because enabled is false.
rgbs_style_conflict action A style was refused because the slug is taken.
rgbs_styles_registered action All styles done, with a map of what registered.
rgbs_github_repo / rgbs_github_token filter Enable and configure the optional updater.

Design tokens: nothing is hard-coded

This is the rule that keeps the plugin portable, so it is worth stating plainly: no stylesheet in this plugin assumes a palette entry, a spacing preset or a font size slug. Every value resolves through the active theme, in this order:

--wp--style--block-gap            the theme's own rhythm (theme.json), emitted by WordPress
  ↓ not set?
--wp--preset--spacing--50         a preset, used only as a fallback
  ↓ not set?
1.5rem                            a literal, only when the theme defines nothing

Colour follows the same idea but differently, because a palette entry is a choice the theme made and this plugin has no business making it:

  • fills and tints are mixed from the text colour the block inherited (color-mix(in srgb, currentColor 5%, transparent)), so a card is a raised surface on a light palette, on a dark palette and inside an inverted section, without naming a colour;
  • borders use currentColor;
  • sizes are em where they should follow the theme's type scale, so Lead is 1.15x whatever the theme's body size is, and an Eyebrow is never larger than the heading it sits on.

Measured on a block theme with --wp--style--block-gap: 1.2rem and body text at 21.76px: card padding 27.2px (the 1.25em floor, because the 19.2px rhythm was tighter than the text), chip gap 9.6px (half the rhythm), Lead 25.03px, and a Group → Dark resolving to the theme's own contrast/base pair. Change the theme's theme.json and every one of those numbers follows, without touching this plugin.

Radius and shadow are literals. They are what a Card is: WordPress has no radius or shadow scale to inherit, and "1rem, 10/30 shadow" is the style's identity rather than a theme decision. The one exception to the colour rule is Group → Dark, which needs two contrasting colours and therefore reads the theme's contrast/base pair, with currentColor as a fallback.

A single spacing knob is exposed per style if you want it tighter or roomier, without editing the plugin:

.wp-block-group.is-style-card { --rgbs-space: 2rem; }

How the CSS is loaded

The short version: WordPress loads a style's CSS when the block it belongs to is on the page, on the frontend as well as in the editor. Nothing loads for blocks that are not there, and you do not write a single wp_enqueue_style() call.

Concretely, a style that resolves to a CSS file is registered as a stylesheet handle (rgbs-group-card) and handed to WordPress twice:

  • as the style_handle of the registered block style, the native declaration that says "this style needs this stylesheet";
  • through wp_enqueue_block_style(), which does the same job through a path that is registered earlier in the request.

Both point at the same handle, so the file is printed once. The second one is not belt-and-braces for its own sake: as of WordPress 7.0, core adds its style_handle loading from enqueue_block_assets, which runs after the template content has already been rendered, so a style declared only that way never loads on a block-theme frontend. That was measured, not assumed; the evidence is in DECISIONS.md.

What that means per block, in practice:

  • A page with a Group loads the Group stylesheets; a page without one loads none of them.
  • A Group loads all of its stylesheets, not only the one you picked, because the decision is made per block, not per class. Files are small and this matches what core does with its own per-block stylesheets. Reading the content to get per-class precision would break dynamic blocks, template parts and query loops, which is why it is not done.
  • The editor loads every registered style's CSS, so picking a style shows its real appearance immediately. That is core behaviour for block styles and it is what makes the editor match the frontend.

Editor-only CSS (`