WP Manifestindependent plugin directory
manifest / editor / stats-block-kit

Block Kit

Three custom Gutenberg blocks built with React, sharing one set of responsive UI components.

by Maria · github.com/lolifoks/stats-block-kit

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/lolifoks/stats-block-kit/archive/refs/heads/main.zip

Three custom Gutenberg blocks built with React, sharing one set of responsive UI components.

I built this to work through block editor development end to end rather than one block in isolation: metadata-driven registration, the edit/save contract, InnerBlocks, block context, static versus dynamic rendering, and a front end script that loads only where it is needed.

The blocks

Block Type What it covers
Stat Grid static, container InnerBlocks with a restricted child list and a starting template, providesContext
Stat static, child RichText with source: "html", usesContext, a viewScript count-up animation
Recent Resources dynamic render.php, useSelect against @wordpress/core-data, loading and empty states

Stat Grid and Stat are static: their markup is written into post content at save time, which is right for text an author typed into that post. Recent Resources is dynamic: it saves only its settings and builds its markup in PHP on every request, so the list never goes stale when a listed post is renamed or a new one is published.

Responsive without generated CSS

Every block that lays content out in a grid stores one attribute:

"columns": { "type": "object", "default": { "mobile": 1, "tablet": 2, "desktop": 3 } }

src/utils/responsive.js turns that object into CSS custom properties, written to the block wrapper as an inline style:

--bk-columns-mobile: 1; --bk-columns-tablet: 2; --bk-columns-desktop: 3;

The stylesheet reads those variables inside mobile-first media queries:

grid-template-columns: repeat(var(--bk-columns-mobile, 1), minmax(0, 1fr));

@media (min-width: 40rem) {
    grid-template-columns: repeat(var(--bk-columns-tablet, 2), minmax(0, 1fr));
}

@media (min-width: 64rem) {
    grid-template-columns: repeat(var(--bk-columns-desktop, 3), minmax(0, 1fr));
}

So there is no per-instance CSS, nothing injected into the head, and no has-3-columns-desktop style class names. A hundred grids on one page share a single stylesheet and differ only by three inline values.

The same helper runs in edit.js, in save.js, and (clamped and sanitised) in render.php, so all three produce identical output.

The shared pieces

src/components/ holds what more than one block needs:

  • responsive-columns-control.js is one inspector control that edits the whole { mobile, tablet, desktop } object: a device toggle plus a single slider bound to whichever breakpoint is active. Stat Grid and Recent Resources both use it, so the responsive behaviour is written and tested once. It takes value and onChange props rather than attributes and setAttributes, so it is not welded to any one block.

src/utils/responsive.js holds the breakpoint definitions and the columns to CSS variable conversion. Stored attributes are merged over defaults before use, so a block saved under an older version of the schema still renders.

Block context, and its limit

Stat Grid provides blockkit/cardStyle to its children, so one setting on the parent applies to the whole set without duplicating an attribute on each child.

Card styling itself is done with descendant selectors scoped to the parent's class, because block context is not available inside a static save(). That is a real constraint worth knowing: context reaches edit and dynamic render.php, not saved markup. The child uses it for editor behaviour instead, adjusting which controls make sense for the parent's current style.

Dynamic rendering

Recent Resources has no save() function at all. render.php treats every attribute as untrusted input, because post content can be hand-edited and this file runs on every page view: the post type is checked with post_type_exists() before it reaches WP_Query, counts are clamped, and column values are clamped to 1 to 4 before being written into a style attribute. Output is escaped at the point of use, and get_block_wrapper_attributes() handles the wrapper class and alignment.

The editor preview is fetched with useSelect against the core data store and handles three distinct states: resolving, resolved but empty, and populated.

Front end script

The Stat block can animate its number when it scrolls into view. That script is declared as viewScript in block.json, so WordPress enqueues it only on pages that contain a stat with the option switched on. It is plain JavaScript with no jQuery and no React, uses IntersectionObserver rather than a scroll listener, parses the prefix and suffix out of values like 1,240 or 99.9%, and exits early when the visitor prefers reduced motion.

The only link between the block and the script is a data-count-up attribute in the saved markup.

Install

Requires WordPress 6.5+, PHP 7.4+, Node 18+.

git clone https://github.com/lolifoks/block-kit.git
cd block-kit
npm install
npm run build

Then activate the plugin. build/ is generated output and is not committed, so a fresh clone needs one build step. The plugin shows an admin notice if you forget.

For development, npm start rebuilds on save.

Structure

block-kit.php                  Registers all blocks from build/*/block.json
src/
  components/
    responsive-columns-control.js
  utils/
    responsive.js              Breakpoints and the columns to CSS variable helper
  stat-grid/                   block.json, index, edit, save, styles
  stat/                        block.json, index, edit, save, view.js, styles
  recent-resources/            block.json, index, edit, render.php, styles

There is no wp_enqueue_script call anywhere in the plugin. Every script and stylesheet is wired up by register_block_type() reading the compiled block.json.

Screenshots

Editor Stat Grid selected, responsive control open
Mobile Front end at 375px
Tablet Front end at 768px
Desktop Front end at 1440px

License

GPL-2.0-or-later