Skip to content
ExponaBLOG
WORDPRESS DEVELOPMENT & PLUGINS · DAY 022 / 100

Block attributes and controls: best practices explained

Make blocks configurable.

Expona Admin7 min read

Block attributes and controls: best practices explained

Custom WordPress blocks become truly powerful when they're configurable—when site editors can adjust colors, text, layouts, and behavior without touching code. Understanding block attributes and controls: best practices explained is the difference between shipping rigid components and flexible tools that teams actually want to use. In this post, we'll walk through storing attributes in block comments, adding inspector controls for sidebar settings, handling rich text editing, and validating defaults to keep everything stable.

Attributes stored in block comments

Every WordPress block serializes its state as JSON inside HTML comments. When you register a block with attributes, you're declaring the shape of data that persists in the post content. This is fundamental to how the block editor works—your JavaScript state lives in memory during editing, but attributes are what gets written to the database.

Let's say we're building a testimonial block. We want to store the author name, role, and star rating. Here's how we declare those attributes:

php
register_block_type( 'expona/testimonial', array(
    'render_callback' => 'render_testimonial',
    'attributes'      => array(
        'authorName' => array(
            'type'    => 'string',
            'default' => 'John Doe',
        ),
        'authorRole' => array(
            'type'    => 'string',
            'default' => 'CEO',
        ),
        'rating'     => array(
            'type'    => 'integer',
            'default' => 5,
        ),
        'testimonialText' => array(
            'type'    => 'string',
            'default' => 'This is a testimonial.',
        ),
    ),
) );

Each attribute has a type and a default. WordPress uses the type to validate data during serialization—if you pass a string where an integer is expected, it gets coerced or rejected. In the database, this block looks like:

php
<!-- wp:expona/testimonial {"authorName":"Alice Chen","authorRole":"Product Lead","rating":5,"testimonialText":"Outstanding service."} -->
<div>...rendered HTML...</div>
<!-- /wp:expona/testimonial -->

The JSON is baked into the comment. WordPress parses it when loading the editor and passes it as props to your edit component. When you mutate an attribute in JavaScript, the JSON updates automatically before save. This immutable-by-default approach prevents sync bugs that plague other systems.

InspectorControls for sidebar settings

The inspector panel on the right side of the block editor is where your users adjust block behavior. We use the InspectorControls component from @wordpress/block-editor to register settings there. This is where you put options that don't belong in the block's main canvas area.

For our testimonial block, we might add a color picker, a dropdown for star style, and a toggle for showing the date. Here's the pattern:

php
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, ColorPicker, SelectControl, ToggleControl } from '@wordpress/components';

export default function Edit( { attributes, setAttributes } ) {
    const { authorName, rating, starStyle, showDate } = attributes;

    return (
        <>
            
                
                     setAttributes({ starStyle: value })}
                    />
                     setAttributes({ showDate: value })}
                    />
                
            

            <div>
                <p>Author: {authorName}</p>
            </div>
        
    );
}

Notice that InspectorControls wraps all sidebar content. Each control calls setAttributes with the new value. WordPress batches these updates and re-renders only what changed. The key principle: inspector controls handle metadata and configuration, while canvas content handles the main editable area.

Don't cram every option into the inspector. If an editor can adjust something directly on the canvas—like text content or an image—let them. Reserve the inspector for settings that are genuinely secondary, like color schemes, spacing options, or display toggles. This keeps the editing experience intuitive.

RichText for editable text

When your block needs to handle formatted text—bold, italic, links—use the RichText component. It gives you the full WordPress editor toolbar for that field and handles all the formatting complexity under the hood.

Let's update the testimonial to use RichText for the testimonial body:

php
import { RichText, InspectorControls } from '@wordpress/block-editor';
import { PanelBody } from '@wordpress/components';

export default function Edit( { attributes, setAttributes } ) {
    const { testimonialText, authorName } = attributes;

    return (
        <>
            
                
                    {/* Inspector controls here */}
                
            

            <div>
                 setAttributes({ testimonialText: value })}
                    placeholder="Enter the testimonial text..."
                    allowedFormats={['core/bold', 'core/italic', 'core/link']}
                />
                 setAttributes({ authorName: value })}
                    placeholder="Author name"
                    allowedFormats={[]}
                />
            </div>
        
    );
}

The tagName prop defines the HTML element the text renders into. The allowedFormats array restricts which formatting buttons appear in the toolbar—we're letting the body text use bold, italic, and links, but keeping the author name plain. This prevents editors from creating nonsensical formatting.

In your render callback (server-side), retrieve the stored content and output it safely:

php
function render_testimonial( $attributes ) {
    $testimonial_text = isset( $attributes['testimonialText'] ) 
        ? wp_kses_post( $attributes['testimonialText'] ) 
        : '';
    $author_name = isset( $attributes['authorName'] ) 
        ? sanitize_text_field( $attributes['authorName'] ) 
        : 'Anonymous';

    return sprintf(
        '<div><p>%s</p><strong>— %s</strong></div>',
        $testimonial_text,
        $author_name
    );
}

Always sanitize on output. wp_kses_post allows safe HTML tags in post content, while sanitize_text_field strips all HTML for plain text fields.

Validate defaults

Default values are the foundation of a good block experience. When a user inserts a fresh block, they should see something reasonable—not empty placeholders or broken layouts. But defaults also serve as documentation: they tell future maintainers what shape the attributes take.

Define defaults consistently in two places: your PHP registration and your JavaScript edit component. Keep them in sync:

php
// PHP (server)
$attributes = array(
    'authorName' => array(
        'type'    => 'string',
        'default' => 'Testimonial Author',
    ),
    'authorRole' => array(
        'type'    => 'string',
        'default' => 'Client',
    ),
    'rating' => array(
        'type'    => 'integer',
        'default' => 5,
    ),
    'starStyle' => array(
        'type'    => 'string',
        'enum'    => ['filled', 'outline', 'none'],
        'default' => 'filled',
    ),
);

In JavaScript, mirror this structure for predictability:

php
const DEFAULT_ATTRIBUTES = {
    authorName: 'Testimonial Author',
    authorRole: 'Client',
    rating: 5,
    starStyle: 'filled',
};

export default function Edit( { attributes, setAttributes } ) {
    const mergedAttrs = { ...DEFAULT_ATTRIBUTES, ...attributes };
    // Now use mergedAttrs
}

When an existing block loads from old content and is missing an attribute, WordPress fills in the default automatically. But if your code references an undefined attribute before the default is applied, you'll get undefined reference errors. Merging defaults in JavaScript prevents that.

Use the enum constraint in PHP to validate that only specific values are allowed:

php
'starStyle' => array(
    'type'    => 'string',
    'enum'    => ['filled', 'outline', 'none'],
    'default' => 'filled',
),

If someone tries to set starStyle to 'invalid', WordPress rejects it and uses the default. This prevents configuration corruption from buggy client code.

Common mistakes

  • Forgetting to sanitize on output. Attributes are stored as JSON in post content. Always sanitize when rendering. A malicious editor could inject scripts if you output attributes directly without sanitization.
  • Storing computed values as attributes. Attributes should hold user input only. Don't store derived state like "computed color contrast" or "formatted date." Calculate these in your render function. If you store computed values, they become stale when input changes.
  • Overloading InspectorControls. If the inspector panel becomes a wall of toggles and dropdowns, users won't find what they need. Group related settings in PanelBody components and consider hiding advanced options behind a disclosure triangle.
  • Not providing meaningful defaults. A block that inserts empty when first added feels broken. Defaults should make the block look decent immediately, so editors see what they're working with.
  • Mixing RichText with plain text fields. It's tempting to use RichText everywhere for consistency, but simple text fields should stay simple. The overhead of a full editor for a single-line title is unjustified.
  • Changing attribute types in production. If you shipped a block with 'rating' => array( 'type' => 'string' ) and later change it to 'integer', all old blocks have strings. Either migrate the data, or add a new attribute and deprecate the old one.

Key takeaways

  • Attributes are the contract between your block's state and the database. Declare them with types and defaults in PHP.
  • Use InspectorControls for sidebar settings, not canvas content. Keep the inspector focused on configuration, not editing the main content.
  • Use RichText for any text field that needs formatting. Restrict allowed formats to prevent nonsensical markup.
  • Validate defaults in both PHP and JavaScript. Merge defaults in your edit component to avoid undefined reference bugs.
  • Always sanitize attributes on output using wp_kses_post or sanitize_text_field depending on content type.
  • Treat attributes as immutable. Never store computed values; calculate them during render.
  • Document attribute shape early and keep PHP and JavaScript definitions in sync.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles