Gutenberg Custom Blocks: Pick a Build Path, Then Scaffold in 5 Steps

We recommend scaffolding with @wordpress/create-block for most custom block projects, since it sets up a maintainable build process in minutes. Hand-edit block.json and server-side registration when you need fine-grained control over rendering or data handling. Use block patterns instead when the content team, not a developer, will manage the layout day to day.


TL;DR:

  • Use dynamic rendering for dates, counts, or external data; static blocks suit fixed layouts but can display stale content when underlying values change.
  • Register custom blocks in a plugin rather than a theme, so they remain available after theme changes; keep render.php beside its block.
  • Run npm run build before deployment because npm run start creates a development build, then test against the WordPress version your site actually uses.
  • Choose ACF blocks for straightforward content types when teams want PHP registration and familiar fields; native blocks suit custom controls and live previews.

tcosi
tcosi.com
Build Gutenberg Blocks With Confidence
tcosi develops custom WordPress websites and provides ongoing support, with a clear, collaborative process for your technical needs.

Discuss your WordPress project

Table of Contents

Methods at a glance: scaffold, hand-write, or pattern-based approaches

Three paths lead to a working custom block, and each fits a different situation.

  • Scaffold with @wordpress/create-block: fastest setup, best for new blocks that need a standard build pipeline.
  • Hand-write block.json and PHP registration: best when you’re adding a block to an existing plugin or need precise control over server rendering.
  • Use block patterns or reusable blocks: best when content editors need to reuse a layout without touching code.

Your choice depends on three things: how long the block needs to live, who will maintain it, and whether it needs dynamic server rendering. A block that displays a copyright year or pulls live data needs server-side logic. A block that just arranges a headline and a button might work fine as a pattern.

Before starting any of these, make sure Node.js and npm are installed, since wp-scripts (the build tool behind all of this) depends on them. A working local WordPress install with plugin access is also required for testing.

Scaffold a plugin with @wordpress/create-block

The @wordpress/create-block package is the WordPress-maintained tool for generating a new block plugin, and it builds the plugin bootstrap, block.json metadata, and the JS and CSS source files with a zero-configuration build setup already wired up.

Running it looks like this:

  1. Open a terminal in your plugins directory and run npx @wordpress/create-block@latest my-custom-block.
  2. Answer the prompts for a namespace, title, and description, or pass them as flags to skip the prompts entirely.
  3. Add --variant=dynamic when your block needs server-side rendering instead of a static saved markup.
  4. Change into the new folder and run npm install, then npm run start to launch the development build watcher.
  5. Activate the plugin from your WordPress admin and drop the block into a post to confirm it renders.

The scaffold produces a predictable layout: a PHP bootstrap file that registers the block, src/index.js as the entry point, edit.js for the editor interface, save.js for static output (skipped entirely in the dynamic variant), and style files split between editor-only and front-end CSS.

Two pitfalls catch people early. First, the dev server output (npm run start) is not production-ready, so always run npm run build before deploying. Second, block behavior can shift slightly between WordPress versions, so test against the version your site actually runs rather than assuming the latest docs match your environment.

Pro Tip: Run npm run build right after your first npm install, even before you write any custom code, so you confirm the toolchain works before debugging your own logic.

block.json explained: attributes, supports, editorScript, style, and render

block.json centralizes a block’s metadata so one file can register it on both the client and the server, which cuts down on duplicate registration code and keeps editor behavior and front-end output in sync.

A few properties do most of the work:

  • attributes: defines the data your block stores, including type (string, number, boolean) and how it maps between the editor’s Edit function and the saved markup.
  • supports: turns on core editor behaviors like color controls, spacing, or alignment without writing that UI yourself.
  • editorScript and style: point to your built JavaScript and CSS, typically using file: paths that reference the build directory rather than hardcoded handles.
  • render: introduced as part of block.json’s expanded properties, this points to a server-side PHP template for dynamic blocks, letting WordPress handle the render callback wiring for you.

Attributes persist by either saving into the post content as static HTML or by being read fresh on each render for dynamic blocks. Getting this distinction right early saves a lot of debugging later, since a mismatched attribute type is one of the most common sources of validation errors in the editor.

Custom blocks belong in a plugin, not a theme. WordPress’s file structure guidance recommends this specifically because blocks registered in a theme disappear the moment someone switches themes, while a plugin keeps the block available regardless of design changes.

The standard layout from create-block looks like this:

  • src/ holds your editable source files: index.js, edit.js, save.js, and styles.
  • build/ holds the compiled output that block.json actually references, generated by wp-scripts, the standard Node-based build tool used across block projects.
  • block.json sits at the plugin root alongside the main PHP bootstrap file.
  • render.php, when present, holds the server-side template referenced by the render property in block.json.

For registration, modern WordPress code typically calls register_block_type() pointed at the block’s block.json file, letting WordPress read the metadata and wire up both editor and server behavior from one source. Keep your render templates in the same plugin folder as the block that uses them so the whole thing stays portable if you ever move it to another site.

Rendering modes and save patterns: dynamic, static, and the fallback pattern

A static block saves its markup directly into post content through save(), while a dynamic block generates its markup on the server every time the page loads, which can be managed effectively using the Optimizely WordPress Plugin for A/B testing. Static output is faster to serve since there’s no PHP execution at render time, but it goes stale if the underlying data changes.

For values that shift over time, like a date or a live count, returning null from save() forces WordPress to use the server-side render callback instead of stored HTML, which keeps the displayed value current without requiring anyone to resave the post.

The block tutorial’s Copyright Date Block example shows a practical middle ground: a fallbackCurrentYear attribute saved at block creation time keeps the editor preview valid even though the live site recalculates the year on each load.

  • Use dynamic rendering for anything time-sensitive, personalized, or pulled from another data source.
  • Use static rendering for fixed layouts like a call-to-action or a pull quote.
  • Dynamic blocks are also easier to remove cleanly, since no stale markup is left behind in the database if the plugin is deactivated.

Pro Tip: If you’re unsure which mode to use, default to dynamic for anything that touches dates, counts, or external data, and reserve static output for content that won’t change once published.

A copyright date block is a good first project because it touches every part of the block API without requiring a database or external service.

  1. Scaffold it with npx @wordpress/create-block@latest copyright-date-block --variant=dynamic, following the official tutorial.
  2. In block.json, add attributes for fallbackCurrentYear, showStartingYear, and startingYear so the block can display either a single year or a range.
  3. In edit.js, build a sidebar panel using PanelBody and TextControl so editors can toggle the starting year and see a live preview.
  4. In the PHP render file, calculate the current year on the server rather than relying on the saved attribute, which keeps the displayed date accurate without ever needing a resave.
  5. Run npm run build, activate the plugin, insert the block into a post, and confirm the front end shows the correct year after the page reloads.

This pattern, scaffold, extend attributes, build the editor UI, then optimize the render, applies to almost any dynamic block you’ll build afterward.

Author perspective: when to build in-house and when to hire tcosi

We build custom Gutenberg blocks as part of ongoing website development work, and the decision to DIY or hire usually comes down to three questions: how complex is the integration, who maintains it after launch, and does it need to survive a theme change. When a block touches ACF fields, custom post types, or long-term maintenance commitments, bringing in a team that handles this daily tends to save more time than it costs.

Best practices for block accessibility and internationalization (i18n)

Accessibility in a custom block starts with the same rules that apply to any web component: use semantic HTML elements inside save() and render.php, label every form control in the editor UI, and make sure color combinations in your supports configuration don’t force low-contrast defaults on editors.

Keyboard navigation deserves specific attention in the Edit component. Native React components from @wordpress/components, like TextControl or ToggleControl, come with keyboard support built in, so leaning on those instead of custom-built form elements saves a lot of accessibility work.

For internationalization, wrap every visible string in your JavaScript with the __() function from @wordpress/i18n, and do the same in PHP render templates with WordPress’s standard translation functions. This includes placeholder text, button labels, and any error messages your block might display.

A few habits that pay off over time:

  • Add a text domain to every translatable string and keep it consistent with your plugin’s slug.
  • Test your block with a screen reader at least once before shipping, since editor-only controls are easy to overlook.
  • Avoid relying on color alone to convey state. Pair it with text or an icon.

None of this adds much development time if it’s built in from the scaffold stage, but retrofitting accessibility into a finished block usually takes longer than doing it right the first time.

Using React hooks and component state in Gutenberg blocks

The block editor runs on React, and the Edit function in a scaffolded block is a React component, which means standard hooks like useState and useEffect work exactly as they would in any React project.

useState is the most common one you’ll reach for, typically to manage local UI state that doesn’t need to persist, like whether a settings panel is expanded. For data that needs to save with the post, attributes handled through setAttributes do that job instead of component state, since attribute changes are what WordPress tracks for undo history and post saving.

useEffect comes in handy for side effects like fetching data from the WordPress REST API when the block loads, though it’s worth being careful about triggering unnecessary re-renders. A fetch call with no dependency array will run on every render, which can slow down the editor noticeably on a page with multiple instances of the same block.

For blocks with more complex state logic, useReducer can keep things organized once you’re juggling more than two or three related pieces of state. It’s not something every block needs, but it beats a tangle of separate useState calls once the editor UI grows past a simple form.

Custom hooks are also fair game. If several blocks in your plugin share logic, like fetching the same taxonomy list, wrapping that in a custom hook keeps the code in one place instead of duplicated across edit.js files.

Using React hooks and component state in Gutenberg blocks — overview diagram

Integrating custom blocks with Advanced Custom Fields (ACF)

ACF offers its own block registration approach that runs alongside native Gutenberg blocks, and it’s a common choice for teams that want custom block functionality without writing React.

An ACF block is registered through a PHP function call rather than a block.json and JavaScript bundle, and the fields themselves come from ACF’s existing field group builder, which content teams are often already comfortable using. Rendering happens through a PHP template, similar to the render.php pattern used in native dynamic blocks.

The trade-off is flexibility versus speed. A native block built with create-block gives you full control over the editor UI, down to custom controls and live previews. An ACF block gets you a working custom block faster, especially for straightforward content types like a staff bio or a testimonial, but the editor experience is generally a form rather than a true live preview.

For projects where a content team needs many similar blocks quickly and the visual editing experience matters less than fast turnaround, ACF blocks make sense. For a flagship block that needs tight editorial control over how it looks while being edited, a native block built from the scaffold usually serves readers and editors better.

How to handle block styling: CSS-in-JS vs traditional stylesheets

Block styling in WordPress typically happens through traditional stylesheets rather than CSS-in-JS libraries common in other React ecosystems, and that’s intentional. The create-block scaffold generates separate editor.scss and style.scss files, compiled by wp-scripts into CSS that block.json references directly.

This split exists for a reason: editor.scss styles apply only inside the block editor, letting you adjust spacing or add editor-only visual cues without affecting the front end, while style.scss loads on both the editor and the live site for styles that should match everywhere.

CSS-in-JS approaches, where styles live inside JavaScript component files, are possible in a Gutenberg block since it’s still a React environment underneath, but they’re uncommon in WordPress projects. The ecosystem’s tooling, documentation, and the supports configuration in block.json are all built around traditional stylesheets, so stepping outside that convention usually means losing some of the built-in editor integration that core styling supports provide for free, like color and spacing controls that generate their own CSS automatically.

For most projects, sticking with the generated SCSS files and leaning on supports flags for anything that core WordPress already handles, like padding or border radius, means less custom CSS to maintain and fewer conflicts with theme styles.

Managing block attributes and data persistence effectively

Attributes are how a block remembers anything, and getting their types right in block.json avoids most of the validation errors developers run into.

Each attribute needs a defined type, string, number, boolean, array, or object, and WordPress uses that definition to parse the attribute back out of saved post content every time the block loads. A mismatch between the declared type and what save() actually outputs is one of the most common causes of the “block contains unexpected or invalid content” warning in the editor.

For static blocks, attributes usually map to specific HTML in the saved markup, often pulled using a source like html or text tied to a CSS selector. For dynamic blocks, attributes are stored as JSON in an HTML comment and passed directly to the server-side render callback, which skips the markup-parsing step entirely.

Keep attribute names specific and avoid storing anything that changes frequently as static saved content, since that’s exactly the situation where dynamic rendering, discussed earlier with the null save() pattern, keeps data from going stale. For attributes that reference other content, like a selected post ID, store the ID rather than duplicating the referenced content, so edits to the original stay reflected everywhere the block appears.

Testing and debugging Gutenberg blocks

Most block bugs surface in one of three places: the browser console, the editor’s own validation warnings, or a mismatch between what save() produces and what the render callback outputs.

Start with the browser console open whenever you’re testing a new block. React errors, failed REST API calls, and most JavaScript issues show up there before they show up anywhere else. The editor’s “attempt block recovery” prompt is also worth reading carefully. It usually points directly at the attribute or markup mismatch causing the problem.

For dynamic blocks, test by deactivating and reactivating the plugin, and by switching themes temporarily, to confirm the block survives both without breaking. This catches portability issues early, before they show up on a live site.

A few habits that catch problems before launch:

  • Test with WordPress’s debug mode enabled (WP_DEBUG set to true) to surface PHP warnings that stay silent otherwise.
  • Check the block in both the classic widescreen editor view and a narrower viewport to catch responsive issues in the editor UI.
  • Use a staging environment before pushing block changes to a live site, since a broken block can affect every post where it’s already been inserted.

Our guidance on plugin management and staging workflows covers this kind of pre-launch testing in more depth, particularly for teams managing multiple custom plugins across client sites.

Editorial take on scaffolding versus hand-building blocks

The real skill in custom block development isn’t writing React. It’s knowing which rendering mode a piece of content actually needs. Most of the validation errors and stale-data complaints we see trace back to a static block holding content that should have been dynamic from the start.

Conventional advice tends to treat create-block as a finish line rather than a starting point, as if running the scaffold command is the hard part. It isn’t. The scaffold takes thirty seconds. The decisions that matter, attribute types, render mode, whether this belongs in a plugin or should have been a pattern, happen after.

If we had to prioritize one thing for a developer building their first custom block, it wouldn’t be learning more React. It would be reading the block.json reference closely enough to understand what supports already gives you for free, so you spend your custom code on the parts that actually need it.

— Tommy Cosimano

Custom block development and ongoing support from tcosi

When a block needs tight integration with existing templates, ACF fields, or a maintenance plan that outlasts the initial build, that’s where we come in. We handle custom WordPress development alongside ongoing website support starting at $100 per month, so your blocks keep working as WordPress updates roll out.

tcosi

Reach out through our contact page to talk through what your block needs to do, and we’ll scope it from there.

FAQ

What is a Gutenberg block?

A Gutenberg block is a self-contained content element, like a paragraph, image, or custom component, that you insert and edit directly inside the WordPress block editor. Each block stores its own settings and content, which the editor assembles into the final page.

Which is better, Block Editor or Elementor?

The block editor is built into WordPress core, requires no additional plugin, and produces content stored as portable HTML rather than a proprietary format. Elementor offers a more visual, drag-and-drop page-building experience but ties your layout data to its own plugin, which can create lock-in if you ever remove it.

How do I create a WordPress block?

The fastest path is scaffolding with @wordpress/create-block, which generates the plugin files, block.json, and build setup for you. From there, you edit the edit.js and save.js files (or a render.php template for dynamic blocks) to define how the block looks and behaves.

What are the best Gutenberg Blocks plugins?

Plugin choice depends on your goal: Advanced Custom Fields is a common choice for teams that want custom block fields without writing JavaScript, while general block-library plugins add pre-built layout blocks for things like galleries or pricing tables. For a block that needs to match a specific design exactly, a custom block built from the official scaffold usually gives more control than a general-purpose block library.

Do I need coding experience to build a custom Gutenberg block?

Scaffolding a block with create-block still requires comfort with JavaScript and PHP to customize the generated files meaningfully. Teams without in-house development resources often use block patterns for simpler layout needs or bring in a developer for anything requiring custom attributes or server-side rendering.

Taking on new projects

Need Help With Your Website?

Tommy Cosimano is a New Jersey-based web designer and WordPress developer who helps businesses build, maintain, and improve websites that are fast, secure, and easy to manage. Through tcosi, he works with companies throughout New Jersey and beyond, providing website design, maintenance, SEO support, and ongoing website consulting.