Block Development Best Practices: Lessons from Building 43 Custom Blocks

Lessons Learned from Building 43 WordPress Blocks

After developing 43 custom Gutenberg blocks and 11 extensions for DesignSetGo, I’ve learned valuable lessons about WordPress block development. Here are the best practices that will save you time, reduce bugs, and create better user experiences.

1. Always Use Block Supports Over Custom Controls

The Mistake: Building custom color pickers, spacing controls, and typography panels for every block.

The Better Way: WordPress Block Supports API provides built-in controls that:

  • Integrate with theme.json automatically
  • Reduce code by up to 70%
  • Ensure consistency across the editor
  • Stay updated with WordPress core

Example:

// block.json
{
  "supports": {
    "color": {
      "text": true,
      "background": true
    },
    "spacing": {
      "padding": true,
      "margin": true
    },
    "typography": {
      "fontSize": true,
      "lineHeight": true
    }
  }
}

Result: WordPress automatically adds all necessary controls to your block—no custom JavaScript required.

2. Future-Proof Your Components

The Problem: WordPress is evolving rapidly. Components from 6.3 may be deprecated in 6.5.

The Solution: Add future-compatibility props to all form components:

<RangeControl
  __next40pxDefaultSize
  __nextHasNoMarginBottom
  label="Font Size"
  value={fontSize}
  onChange={setFontSize}
/>

These props ensure your blocks won’t break when WordPress updates default styling.

3. Respect the Rendering Pipeline

Critical Pattern: Different code runs in the editor vs. frontend.

In edit.js:

  • Use React hooks (useState, useEffect)
  • Access WordPress data stores
  • Show placeholder UI for dynamic content

In save.js:

  • Pure HTML output only—no hooks, no interactivity
  • Must be serializable to database
  • Use data attributes for frontend JavaScript

For frontend interactivity:

  • Create separate view.js with vanilla JavaScript
  • Use data attributes to pass configuration
  • Attach event listeners after DOM loads

4. Test Responsive Behavior Early

Common Issue: Layouts look perfect at 1920px but break on mobile.

Testing Matrix:

  • 375px – Mobile (iPhone SE)
  • 768px – Tablet breakpoint
  • 1200px – Desktop
  • 1920px – Large desktop

Pro Tip: Use the responsive preview in the block editor toolbar. Test every new block at all breakpoints before committing.

5. Avoid Specificity Wars in CSS

Bad Practice:

.wp-block-designsetgo-stack.custom-class {
  display: flex; /* Specificity: 0,2,0 */
}

Best Practice: Use :where() to reduce specificity:

:where(.wp-block-designsetgo-stack) {
  display: flex; /* Specificity: 0,0,1 */
}

This makes your blocks easier to customize and prevents conflicts with theme styles.

6. Scope JavaScript to Your Blocks Only

Dangerous:

// Affects ALL accordions on page!
document.querySelectorAll('.accordion').forEach(...)

Safe:

// Only affects DesignSetGo accordions
document.querySelectorAll('[data-dsgo-accordion]').forEach(...)

Always use data attributes with your plugin prefix to avoid conflicts.

7. Plan for Deprecations from Day One

Reality Check: You WILL change your block structure. Prepare for it.

What triggers a deprecation:

  • Changing attribute names or types
  • Modifying saved HTML structure
  • Removing attributes

Solution: Create deprecated.js from the start:

const v1 = {
  attributes: { /* old schema */ },
  save: ({ attributes }) => { /* old output */ },
  migrate: (attrs) => ({ /* transform to new */ }),
};

export default [v1];

This ensures existing content doesn’t break when you update your block.

8. Leverage InnerBlocks for Composability

Design Principle: Container blocks (Stack, Flex, Grid) should use InnerBlocks to accept any content.

Critical Pattern: Always use useInnerBlocksProps():

// edit.js
const blockProps = useBlockProps();
const innerBlocksProps = useInnerBlocksProps(blockProps, {
  allowedBlocks: ['core/heading', 'core/paragraph'],
  template: [['core/paragraph', {}]],
});

return <div {...innerBlocksProps} />;

NEVER render InnerBlocks as a component—it breaks block validation.

9. Performance Matters: Minimize Bundle Size

Impact: DesignSetGo loads 43 blocks but keeps total bundle under 300KB (gzipped ~80KB).

Strategies:

  • Share utilities across blocks (one icon library, not 43 copies)
  • Use WordPress components instead of external libraries
  • Lazy-load heavy features (icon picker opens on-demand)
  • Tree-shake unused code with proper imports

10. Document with Examples, Not Just Code

What Developers Need:

  • Visual examples showing what the block creates
  • Common use cases and patterns
  • Troubleshooting for known issues
  • Migration guides for breaking changes

Pro Tip: Add an “example” property to block.json:

{
  "example": {
    "attributes": {
      "content": "Preview content here"
    }
  }
}

This shows a preview in the block inserter, helping users understand your block before inserting it.

Bonus: The Pre-Commit Checklist

Before every commit, verify:

  1. npm run build succeeds
  2. No console errors in editor OR frontend
  3. Changed blocks + related blocks tested
  4. Responsive preview (375/768/1200px)
  5. No unexpected file changes in build/

Conclusion

Building 43 blocks taught me that the best code is often the code you don’t write. Leverage WordPress core features, follow established patterns, and always prioritize the user experience over clever implementations.

These practices have saved hundreds of hours of debugging and refactoring in DesignSetGo. Apply them to your own block development, and you’ll create more maintainable, performant, and user-friendly blocks.

Want to see these principles in action? Check out the DesignSetGo source code on GitHub to see how they’re implemented across 43 real-world blocks.