Loading Gutenberg block CSS only when needed

One of the decisions I made while building One Base was to give each part of a page responsibility for its own CSS. A reusable theme can contain quite a few components. That does not mean every visitor should download styling for all of them.

The approach on this website is fairly straightforward: keep the shared foundation small, connect block styles to their block types, and discover optional pattern styles from the blocks being rendered. Here is how I approach it, including the bits that can make an apparently simple optimisation unreliable.

Based on One Base, the Gutenberg theme used by this portfolio. Source and examples checked on 30 September 2026. The current project runs WordPress 7.1 and requires PHP 8.3; the standalone loading example uses APIs available in WordPress 6.8 and later.

What we’re building

We will attach a stylesheet to the native Details block, so a page containing that block receives the stylesheet and a page without it does not. We will also look at custom block metadata and the extra layer I use for patterns.

This is conditional loading during server rendering. It does not wait for the visitor to scroll to the block, and it does not fetch CSS through JavaScript. The examples are reduced adaptations of the approach in One Base, rather than the complete theme implementation.

Before you start

Work in a development copy with an existing block theme and access to its PHP and CSS build. Take a page with a Details block and another without one. Remember that a block in the shared header or footer still counts as part of both pages.

There are two related WordPress settings: loading Core block CSS as separate files, and loading block assets only when rendered. WordPress 6.8 introduced a distinct filter for the second decision. Block themes opt in to both by default. I make both choices explicit in this standalone example so its intention is clear. See the WordPress 6.8 explanation.

1. Keep the foundation separate

My global stylesheet still loads on every page. It covers things such as box sizing, focus outlines and sensible wrapping for long text. The palette, typography and layout settings also belong in theme.json.

I do not split a handful of genuinely shared rules into dozens of files. The useful question is whether a rule is part of the foundation or belongs to a particular component. An accordion, gallery or specialist listing usually has a clearer boundary than the site’s basic typography.

2. Attach CSS to a native block

In One Base, BlockStyles.php discovers files named core-*.css and associates them with the corresponding Core blocks. For one block, the registration can be much smaller.

Theme functions.php, or a theme-owned asset registration file loaded by it.

<?php
add_action('after_setup_theme', static function (): void {
    add_filter('should_load_separate_core_block_assets', '__return_true');
    add_filter('should_load_block_assets_on_demand', '__return_true');
});

add_action('init', static function (): void {
    $relative = 'assets/blocks/core-details.css';
    $path = get_theme_file_path($relative);

    if (!is_file($path)) {
        return;
    }

    wp_enqueue_block_style('core/details', [
        'handle' => 'tutorial-core-details',
        'src'    => get_theme_file_uri($relative),
        'path'   => $path,
        'ver'    => (string) filemtime($path),
    ]);
});

The block name identifies what needs the asset. The unique handle lets WordPress manage it without adding another copy for every Details block. The URL points to the built CSS, while the filesystem path lets WordPress inspect the file and potentially inline it. The modification time changes the version after a rebuild. The function reference documents these arguments.

assets/blocks/core-details.css

.wp-block-details {
    border-block: 1px solid currentColor;
    padding-block: 1rem;
}

.wp-block-details > summary {
    cursor: pointer;
    font-weight: 600;
}

.wp-block-details[open] > summary {
    margin-block-end: 1rem;
}

These rules are scoped to the block. They do not change every summary or every element on the page. I also leave the native disclosure behaviour alone, rather than replacing it with click handlers.

In One Base, that file is generated from src/styles/blocks/core-details.css. I edit the source and run npm run build:css:core from the project root. Editing only the generated file would lose the change on the next build.

Registration should happen before the relevant blocks render. I use init here, as the theme does. I would not put this registration inside enqueue_block_assets just because that hook sounds relevant: the function installs the loading callbacks itself.

3. Let custom blocks declare their assets

For a custom block, I prefer keeping the connection in block.json. My Margin media block already separates its shared CSS from editor-only CSS. This fragment shows that same arrangement:

{
    "style": "file:./style.css",
    "editorStyle": "file:./editor.css"
}

Merge these properties into a complete block definition, then register that definition on the server with register_block_type(). One Base discovers the metadata from its block directories, which avoids maintaining a second list of asset registrations.

style is available to the frontend and editor; editorStyle is for the editor. If styling is genuinely frontend-only, viewStyle is available from WordPress 6.5. On-demand frontend loading depends on the active loading policy. The block metadata documentation covers those fields.

4. Give patterns a separate discovery rule

A pattern is a composition of blocks. Once an ordinary pattern is inserted, the content can be edited independently, so I do not assume it retains a live connection to the original PHP pattern file.

In One Base I put a marker such as one-202x-pattern-minimal-about-tools on the outer block. During rendering, PatternAssets.php reads its saved classes and looks up a matching built CSS file. The marker survives insertion as ordinary editable blocks.

The lookup uses an indexed set of files and validates the marker’s slug. It does not turn an arbitrary class into a filesystem path. The index is built once per loader instance, and a stable stylesheet handle prevents duplicate loading when the same pattern appears twice.

This is a theme convention, not a WordPress feature that automatically recognises pattern filenames. Removing the marker removes that connection. I also load the pattern styles separately in the editor so clients can preview patterns before inserting them.

Check the behaviour

I checked the existing Details registration in the local WordPress runtime: its stylesheet was not enqueued before rendering the block, and was enqueued afterwards. That confirms the registration path. It is not a performance benchmark or a substitute for checking the final page.

  • Rebuild CSS, clear page caches, and compare the two pages while logged out.
  • Check Network for the stylesheet and page source for inline CSS. An absent CSS request does not prove the styles are absent.
  • Add a second Details block. Confirm the styles are included once and both disclosures work with a keyboard.
  • Insert, save and reopen the block in the editor. Compare spacing, focus states and mobile layout.
  • Repeat the checks for blocks inside template parts and synced patterns, not just the main post content.

Things to watch

Do not judge loading from has_block() against the main post alone. That can miss content supplied by templates, synced patterns or dynamic rendering. Connecting assets to the rendering path follows what the page actually includes.

A block-level stylesheet is conditional on the block type, not necessarily on a particular style variation. If a large optional design uses a class on a common Group block, registering everything against core/group can still load it on most pages.

Older classic themes need extra care over when render-discovered CSS is printed. WordPress 6.9 introduced output buffering that moves late styles into the document head. I would check the actual markup and first paint on the target version rather than assume this block-theme example behaves identically everywhere. The frontend performance field guide explains that change.

The payoff is keeping the theme’s asset boundaries understandable. Pages request the optional styles they use, and the editor still provides a useful preview. I measure loading time, transferred CSS and visual stability afterwards. Splitting files is only an improvement when the finished site loads and behaves better.

References

More about me