Dynamic Attribute and Style Binding with Interactivity API Directives

The WordPress Interactivity API provides powerful binding directives that connect your state to DOM attributes, styles, classes, and content. These declarative bindings eliminate manual DOM manipulation, creating UIs that automatically reflect state changes while maintaining clean, readable code.

In this article, we’ll explore the complete suite of binding directives—data-wp-binddata-wp-classdata-wp-style, and data-wp-text—and learn how to create dynamic, reactive interfaces that respond instantly to user interactions and data updates.

Understanding Binding Directives

Binding directives establish one-way data flow from your state to the DOM. When state changes, the directives automatically update the corresponding DOM properties without requiring imperative code.

The Four Core Binding Directives

<div data-wp-interactive="myPlugin">
    <!-- data-wp-bind: General attribute binding -->
    <input
        data-wp-bind--value="state.username"
        data-wp-bind--disabled="state.isLoading"
        data-wp-bind--placeholder="state.placeholderText"
    />

    <!-- data-wp-class: Conditional CSS class binding -->
    <div
        data-wp-class--active="state.isActive"
        data-wp-class--error="state.hasError"
    >
        Status indicator
    </div>

    <!-- data-wp-style: Inline style binding -->
    <div
        data-wp-style--background-color="state.bgColor"
        data-wp-style--width="state.widthPercent"
    >
        Styled element
    </div>

    <!-- data-wp-text: Text content binding -->
    <span data-wp-text="state.message"></span>
</div>

Key Principles:

  • Directives follow the pattern data-wp-[type]--[property]="state.value"
  • Bindings are reactive—updates propagate automatically
  • Multiple bindings can coexist on the same element
  • Expressions are evaluated in the context of the current store and local context

data-wp-bind: Universal Attribute Binding

The data-wp-bind directive connects state to any HTML attribute.

Common Attribute Bindings

// view.js
import { store } from '@wordpress/interactivity';

store( 'myPlugin', {
    state: {
        isLoading: false,
        imageUrl: '/default-image.jpg',
        linkUrl: '/home',
        buttonDisabled: true,
        inputValue: '',
        ariaLabel: 'Search products'
    }
} );
<div data-wp-interactive="myPlugin">
    <!-- Image src binding -->
    <img
        data-wp-bind--src="state.imageUrl"
        data-wp-bind--alt="state.imageAlt"
    />

    <!-- Link href binding -->
    <a data-wp-bind--href="state.linkUrl">
        Dynamic Link
    </a>

    <!-- Input value binding -->
    <input
        type="text"
        data-wp-bind--value="state.inputValue"
        data-wp-bind--placeholder="state.placeholder"
    />

    <!-- Button disabled state -->
    <button data-wp-bind--disabled="state.buttonDisabled">
        Submit
    </button>

    <!-- Accessibility attributes -->
    <button
        data-wp-bind--aria-label="state.ariaLabel"
        data-wp-bind--aria-expanded="state.menuOpen"
    >
        Toggle Menu
    </button>

    <!-- Data attributes -->
    <div
        data-wp-bind--data-user-id="state.userId"
        data-wp-bind--data-status="state.userStatus"
    >
        User card
    </div>
</div>

Boolean Attributes

For boolean attributes like disabledcheckedreadonly, the binding evaluates the expression as a boolean:

store( 'myPlugin', {
    state: {
        formValid: false,
        termsAccepted: false,
        isReadOnly: true
    }
} );
<div data-wp-interactive="myPlugin">
    <!-- Boolean bindings -->
    <button data-wp-bind--disabled="!state.formValid">
        Submit Form
    </button>

    <input
        type="checkbox"
        data-wp-bind--checked="state.termsAccepted"
    />

    <input
        type="text"
        data-wp-bind--readonly="state.isReadOnly"
    />
</div>

Important: For boolean attributes, if the expression evaluates to false, the attribute is removed entirely. If true, the attribute is added.

Dynamic Form Controls

Create responsive form interfaces with synchronized state:

<?php
// render.php
wp_interactivity_state( 'myPlugin', array(
    'formData' => array(
        'username' => '',
        'email' => '',
        'country' => 'us',
        'notifications' => true,
    ),
    'errors' => array(),
    'isSubmitting' => false,
) );
?>

<form data-wp-interactive="myPlugin" data-wp-on--submit="actions.handleSubmit">
    <!-- Text Input -->
    <input
        type="text"
        data-wp-bind--value="state.formData.username"
        data-wp-on--input="actions.updateUsername"
        data-wp-bind--aria-invalid="state.errors.username ? 'true' : 'false'"
    />
    <span
        data-wp-text="state.errors.username"
        data-wp-bind--hidden="!state.errors.username"
    ></span>

    <!-- Email Input -->
    <input
        type="email"
        data-wp-bind--value="state.formData.email"
        data-wp-on--input="actions.updateEmail"
    />

    <!-- Select Dropdown -->
    <select
        data-wp-bind--value="state.formData.country"
        data-wp-on--change="actions.updateCountry"
    >
        <option value="us">United States</option>
        <option value="uk">United Kingdom</option>
        <option value="ca">Canada</option>
    </select>

    <!-- Checkbox -->
    <input
        type="checkbox"
        data-wp-bind--checked="state.formData.notifications"
        data-wp-on--change="actions.toggleNotifications"
    />

    <!-- Submit Button -->
    <button
        type="submit"
        data-wp-bind--disabled="state.isSubmitting"
    >
        <span data-wp-text="state.isSubmitting ? 'Submitting...' : 'Submit'"></span>
    </button>
</form>
store( 'myPlugin', {
    actions: {
        updateUsername( event ) {
            const { state } = store( 'myPlugin' );
            state.formData.username = event.target.value;

            // Clear error when typing
            if ( state.errors.username ) {
                state.errors = { ...state.errors, username: '' };
            }
        },

        updateEmail( event ) {
            const { state } = store( 'myPlugin' );
            state.formData.email = event.target.value;
        },

        updateCountry( event ) {
            const { state } = store( 'myPlugin' );
            state.formData.country = event.target.value;
        },

        toggleNotifications( event ) {
            const { state } = store( 'myPlugin' );
            state.formData.notifications = event.target.checked;
        }
    }
} );

data-wp-class: Conditional CSS Class Binding

The data-wp-class directive conditionally applies CSS classes based on state.

Basic Class Binding

store( 'myPlugin', {
    state: {
        isActive: false,
        hasError: false,
        isLoading: true,
        userType: 'premium'
    }
} );
<div data-wp-interactive="myPlugin">
    <!-- Single class binding -->
    <div data-wp-class--active="state.isActive">
        Item
    </div>

    <!-- Multiple class bindings -->
    <div
        data-wp-class--loading="state.isLoading"
        data-wp-class--error="state.hasError"
        data-wp-class--success="!state.hasError && !state.isLoading"
    >
        Status indicator
    </div>

    <!-- Complex conditions -->
    <div
        data-wp-class--premium="state.userType === 'premium'"
        data-wp-class--basic="state.userType === 'basic'"
    >
        User badge
    </div>
</div>

How It Works:

  • If the expression evaluates to true, the class is added
  • If false, the class is removed
  • Existing classes in the class attribute are preserved
  • Multiple data-wp-class directives can target different classes on the same element

UI State Patterns

Common patterns for visual feedback:

store( 'myPlugin', {
    state: {
        items: [],
        selectedItemId: null,
        expandedPanelId: null,
        theme: 'light',

        get hasItems() {
            return this.items.length > 0;
        }
    },

    actions: {
        selectItem( itemId ) {
            const { state } = store( 'myPlugin' );
            state.selectedItemId = itemId;
        },

        togglePanel( panelId ) {
            const { state } = store( 'myPlugin' );
            state.expandedPanelId = state.expandedPanelId === panelId ? null : panelId;
        }
    }
} );
<div
    data-wp-interactive="myPlugin"
    data-wp-class--dark-theme="state.theme === 'dark'"
    data-wp-class--light-theme="state.theme === 'light'"
>
    <!-- Empty state -->
    <div
        data-wp-class--visible="!state.hasItems"
        data-wp-class--hidden="state.hasItems"
        class="empty-state"
    >
        No items found
    </div>

    <!-- Item list with selection -->
    <template data-wp-each="state.items">
        <div
            data-wp-class--selected="state.selectedItemId === context.item.id"
            data-wp-on--click="actions.selectItem"
            data-wp-bind--data-item-id="context.item.id"
        >
            <span data-wp-text="context.item.name"></span>
        </div>
    </template>

    <!-- Expandable panels -->
    <template data-wp-each="state.panels">
        <div class="panel">
            <button
                data-wp-on--click="actions.togglePanel"
                data-wp-bind--data-panel-id="context.item.id"
                data-wp-class--expanded="state.expandedPanelId === context.item.id"
            >
                <span data-wp-text="context.item.title"></span>
            </button>
            <div
                data-wp-class--open="state.expandedPanelId === context.item.id"
                data-wp-class--closed="state.expandedPanelId !== context.item.id"
                class="panel-content"
            >
                <span data-wp-text="context.item.content"></span>
            </div>
        </div>
    </template>
</div>

Animation and Transition Classes

Combine with CSS transitions for smooth animations:

/* styles.css */
.fade-enter {
    opacity: 0;
    transform: translateY(-10px);
}

.fade-enter-active {
    opacity: 1;
    transform: translateY(0);
    transition: opacity 300ms, transform 300ms;
}

.fade-exit {
    opacity: 1;
}

.fade-exit-active {
    opacity: 0;
    transition: opacity 300ms;
}
store( 'myPlugin', {
    state: {
        modalVisible: false,
        isEntering: false,
        isExiting: false
    },

    actions: {
        showModal() {
            const { state } = store( 'myPlugin' );
            state.modalVisible = true;
            state.isEntering = true;

            setTimeout( () => {
                state.isEntering = false;
            }, 300 );
        },

        hideModal() {
            const { state } = store( 'myPlugin' );
            state.isExiting = true;

            setTimeout( () => {
                state.isExiting = false;
                state.modalVisible = false;
            }, 300 );
        }
    }
} );
<div data-wp-interactive="myPlugin">
    <button data-wp-on--click="actions.showModal">
        Open Modal
    </button>

    <div
        data-wp-class--visible="state.modalVisible"
        data-wp-class--hidden="!state.modalVisible"
        data-wp-class--fade-enter="state.isEntering"
        data-wp-class--fade-enter-active="state.isEntering"
        data-wp-class--fade-exit="state.isExiting"
        data-wp-class--fade-exit-active="state.isExiting"
        class="modal"
    >
        <div class="modal-content">
            <h2>Modal Title</h2>
            <button data-wp-on--click="actions.hideModal">
                Close
            </button>
        </div>
    </div>
</div>

data-wp-style: Inline Style Binding

The data-wp-style directive dynamically applies inline styles based on state.

Basic Style Binding

store( 'myPlugin', {
    state: {
        bgColor: '#3498db',
        textColor: '#ffffff',
        widthPercent: '50%',
        opacity: 0.8,
        fontSize: '16px'
    }
} );
<div data-wp-interactive="myPlugin">
    <!-- Color binding -->
    <div
        data-wp-style--background-color="state.bgColor"
        data-wp-style--color="state.textColor"
    >
        Colored box
    </div>

    <!-- Dimension binding -->
    <div
        data-wp-style--width="state.widthPercent"
        data-wp-style--height="state.height"
    >
        Sized element
    </div>

    <!-- Opacity and transforms -->
    <div
        data-wp-style--opacity="state.opacity"
        data-wp-style--transform="state.transform"
    >
        Animated element
    </div>
</div>

Note: CSS property names use kebab-case (e.g., background-color, not backgroundColor) in the directive, but the state value should include units where applicable (e.g., '50%''16px').

Dynamic Theming

Create theme switchers and customizable interfaces:

<?php
// render.php
wp_interactivity_state( 'myPlugin', array(
    'theme' => array(
        'primaryColor' => '#3498db',
        'secondaryColor' => '#2ecc71',
        'textColor' => '#333333',
        'fontSize' => '16px',
    ),
    'darkMode' => false,
) );
?>

<div
    data-wp-interactive="myPlugin"
    data-wp-style--color="state.theme.textColor"
    data-wp-style--font-size="state.theme.fontSize"
>
    <!-- Theme controls -->
    <div class="theme-controls">
        <label>
            Primary Color:
            <input
                type="color"
                data-wp-bind--value="state.theme.primaryColor"
                data-wp-on--input="actions.updatePrimaryColor"
            />
        </label>

        <label>
            Font Size:
            <input
                type="range"
                min="12"
                max="24"
                data-wp-bind--value="state.theme.fontSize"
                data-wp-on--input="actions.updateFontSize"
            />
        </label>

        <label>
            <input
                type="checkbox"
                data-wp-bind--checked="state.darkMode"
                data-wp-on--change="actions.toggleDarkMode"
            />
            Dark Mode
        </label>
    </div>

    <!-- Themed content -->
    <button
        data-wp-style--background-color="state.theme.primaryColor"
        data-wp-style--color="state.darkMode ? '#ffffff' : '#000000'"
    >
        Primary Button
    </button>

    <button
        data-wp-style--background-color="state.theme.secondaryColor"
        data-wp-style--color="state.darkMode ? '#ffffff' : '#000000'"
    >
        Secondary Button
    </button>
</div>
store( 'myPlugin', {
    actions: {
        updatePrimaryColor( event ) {
            const { state } = store( 'myPlugin' );
            state.theme = {
                ...state.theme,
                primaryColor: event.target.value
            };
        },

        updateFontSize( event ) {
            const { state } = store( 'myPlugin' );
            state.theme = {
                ...state.theme,
                fontSize: `${ event.target.value }px`
            };
        },

        toggleDarkMode( event ) {
            const { state } = store( 'myPlugin' );
            state.darkMode = event.target.checked;

            if ( state.darkMode ) {
                state.theme = {
                    ...state.theme,
                    textColor: '#ffffff'
                };
            } else {
                state.theme = {
                    ...state.theme,
                    textColor: '#333333'
                };
            }
        }
    }
} );

Progress Indicators and Animations

Create dynamic progress bars, loaders, and animated elements:

store( 'myPlugin', {
    state: {
        uploadProgress: 0,
        loadingRotation: 0,
        sliderValue: 50
    },

    actions: {
        startUpload() {
            const { state } = store( 'myPlugin' );
            state.uploadProgress = 0;

            const interval = setInterval( () => {
                state.uploadProgress += 10;

                if ( state.uploadProgress >= 100 ) {
                    clearInterval( interval );
                }
            }, 500 );
        },

        startLoadingAnimation() {
            const { state } = store( 'myPlugin' );

            const animate = () => {
                state.loadingRotation = ( state.loadingRotation + 5 ) % 360;
                requestAnimationFrame( animate );
            };

            animate();
        },

        updateSlider( event ) {
            const { state } = store( 'myPlugin' );
            state.sliderValue = event.target.value;
        }
    }
} );
<div data-wp-interactive="myPlugin">
    <!-- Progress bar -->
    <div class="progress-container">
        <div
            class="progress-bar"
            data-wp-style--width="state.uploadProgress + '%'"
        ></div>
        <span data-wp-text="state.uploadProgress + '%'"></span>
    </div>

    <!-- Loading spinner -->
    <div
        class="spinner"
        data-wp-style--transform="'rotate(' + state.loadingRotation + 'deg)'"
    ></div>

    <!-- Interactive slider visualization -->
    <input
        type="range"
        min="0"
        max="100"
        data-wp-bind--value="state.sliderValue"
        data-wp-on--input="actions.updateSlider"
    />
    <div
        class="slider-indicator"
        data-wp-style--left="state.sliderValue + '%'"
    ></div>
</div>

data-wp-text: Text Content Binding

The data-wp-text directive sets the text content of an element based on state.

Basic Text Binding

store( 'myPlugin', {
    state: {
        username: 'John Doe',
        itemCount: 42,
        lastUpdated: '2025-10-28',
        message: 'Hello, World!'
    }
} );
<div data-wp-interactive="myPlugin">
    <!-- Simple text binding -->
    <h1 data-wp-text="state.username"></h1>

    <!-- Number binding -->
    <p>Items: <span data-wp-text="state.itemCount"></span></p>

    <!-- Date binding -->
    <p>Last updated: <span data-wp-text="state.lastUpdated"></span></p>

    <!-- Dynamic message -->
    <div data-wp-text="state.message"></div>
</div>

Expressions and Computed Text

Combine state properties and use expressions:

store( 'myPlugin', {
    state: {
        firstName: 'John',
        lastName: 'Doe',
        items: [ 1, 2, 3, 4, 5 ],
        isLoggedIn: true,
        unreadCount: 5,

        get fullName() {
            return `${ this.firstName } ${ this.lastName }`;
        },

        get itemCountText() {
            return `${ this.items.length } item${ this.items.length !== 1 ? 's' : '' }`;
        }
    }
} );
<div data-wp-interactive="myPlugin">
    <!-- Computed text with getter -->
    <p>Welcome, <span data-wp-text="state.fullName"></span>!</p>

    <!-- Pluralization -->
    <p data-wp-text="state.itemCountText"></p>

    <!-- Conditional text -->
    <p data-wp-text="state.isLoggedIn ? 'Logged In' : 'Logged Out'"></p>

    <!-- Number formatting -->
    <p data-wp-text="state.unreadCount > 0 ? state.unreadCount + ' unread messages' : 'No unread messages'"></p>
</div>

Dynamic Lists with Text Binding

Combine data-wp-text with data-wp-each for dynamic content rendering:

store( 'myPlugin', {
    state: {
        tasks: [
            { id: 1, title: 'Learn Interactivity API', completed: true },
            { id: 2, title: 'Build interactive block', completed: false },
            { id: 3, title: 'Deploy to production', completed: false }
        ],

        get completedCount() {
            return this.tasks.filter( task => task.completed ).length;
        },

        get completionText() {
            return `${ this.completedCount } of ${ this.tasks.length } completed`;
        }
    }
} );
<div data-wp-interactive="myPlugin">
    <!-- Summary text -->
    <h2 data-wp-text="state.completionText"></h2>

    <!-- Task list -->
    <ul>
        <template data-wp-each="state.tasks">
            <li data-wp-class--completed="context.item.completed">
                <span data-wp-text="context.item.title"></span>
                <span data-wp-text="context.item.completed ? '✓' : '○'"></span>
            </li>
        </template>
    </ul>
</div>

See [Article 6: Building Dynamic Lists](Building Dynamic Lists and Collections with data-wp-each.md) for more on combining directives with list rendering.

Real-World Implementation: Dynamic Dashboard

Let’s combine all binding directives into a complete interactive dashboard:

<?php
// render.php
wp_interactivity_state( 'myPlugin', array(
    'metrics' => array(
        'visitors' => 1234,
        'pageviews' => 5678,
        'revenue' => 9876.50,
        'conversionRate' => 3.45,
    ),
    'timeRange' => 'week',
    'isLoading' => false,
    'lastUpdated' => current_time( 'mysql' ),
    'chartData' => array(
        array( 'date' => '2025-10-21', 'value' => 120 ),
        array( 'date' => '2025-10-22', 'value' => 150 ),
        array( 'date' => '2025-10-23', 'value' => 130 ),
        array( 'date' => '2025-10-24', 'value' => 180 ),
        array( 'date' => '2025-10-25', 'value' => 200 ),
    ),
) );
?>

<div
    data-wp-interactive="myPlugin"
    class="dashboard"
    data-wp-class--loading="state.isLoading"
>
    <!-- Time Range Selector -->
    <div class="time-range-selector">
        <button
            data-wp-on--click="actions.setTimeRange"
            data-wp-bind--data-range="day"
            data-wp-class--active="state.timeRange === 'day'"
        >
            Day
        </button>
        <button
            data-wp-on--click="actions.setTimeRange"
            data-wp-bind--data-range="week"
            data-wp-class--active="state.timeRange === 'week'"
        >
            Week
        </button>
        <button
            data-wp-on--click="actions.setTimeRange"
            data-wp-bind--data-range="month"
            data-wp-class--active="state.timeRange === 'month'"
        >
            Month
        </button>
    </div>

    <!-- Metrics Cards -->
    <div class="metrics-grid">
        <!-- Visitors Card -->
        <div
            class="metric-card"
            data-wp-style--border-color="'#3498db'"
        >
            <h3>Visitors</h3>
            <p class="metric-value" data-wp-text="state.metrics.visitors"></p>
            <span
                class="metric-change"
                data-wp-text="state.metrics.visitorsChange + '%'"
                data-wp-class--positive="state.metrics.visitorsChange > 0"
                data-wp-class--negative="state.metrics.visitorsChange < 0"
            ></span>
        </div>

        <!-- Pageviews Card -->
        <div
            class="metric-card"
            data-wp-style--border-color="'#2ecc71'"
        >
            <h3>Pageviews</h3>
            <p class="metric-value" data-wp-text="state.metrics.pageviews"></p>
        </div>

        <!-- Revenue Card -->
        <div
            class="metric-card"
            data-wp-style--border-color="'#e74c3c'"
        >
            <h3>Revenue</h3>
            <p class="metric-value" data-wp-text="'$' + state.metrics.revenue.toFixed(2)"></p>
        </div>

        <!-- Conversion Rate Card -->
        <div
            class="metric-card"
            data-wp-style--border-color="'#f39c12'"
        >
            <h3>Conversion Rate</h3>
            <p class="metric-value" data-wp-text="state.metrics.conversionRate + '%'"></p>
        </div>
    </div>

    <!-- Chart -->
    <div class="chart-container">
        <h2>Traffic Trend</h2>
        <div class="chart">
            <template data-wp-each="state.chartData">
                <div class="chart-bar">
                    <div
                        class="bar"
                        data-wp-style--height="(context.item.value / state.maxChartValue * 100) + '%'"
                        data-wp-style--background-color="state.chartBarColor"
                        data-wp-bind--title="context.item.date + ': ' + context.item.value"
                    ></div>
                    <span class="bar-label" data-wp-text="context.item.date"></span>
                </div>
            </template>
        </div>
    </div>

    <!-- Loading Overlay -->
    <div
        class="loading-overlay"
        data-wp-class--visible="state.isLoading"
        data-wp-style--opacity="state.isLoading ? '1' : '0'"
    >
        <div class="spinner"></div>
        <p data-wp-text="'Loading ' + state.timeRange + ' data...'"></p>
    </div>

    <!-- Last Updated -->
    <p class="last-updated">
        Last updated: <span data-wp-text="state.lastUpdated"></span>
    </p>
</div>
// view.js
import { store, getElement } from '@wordpress/interactivity';

store( 'myPlugin', {
    state: {
        chartBarColor: '#3498db',

        get maxChartValue() {
            if ( ! this.chartData || this.chartData.length === 0 ) {
                return 1;
            }
            return Math.max( ...this.chartData.map( d => d.value ) );
        }
    },

    actions: {
        async setTimeRange( event ) {
            const { state } = store( 'myPlugin' );
            const element = getElement();
            const range = element.ref.dataset.range;

            if ( state.timeRange === range ) {
                return;
            }

            state.isLoading = true;
            state.timeRange = range;

            try {
                // Fetch new data
                const response = await fetch(
                    `/wp-json/myplugin/v1/metrics?range=${ range }`
                );
                const data = await response.json();

                state.metrics = data.metrics;
                state.chartData = data.chartData;
                state.lastUpdated = new Date().toLocaleString();
            } catch ( error ) {
                console.error( 'Failed to fetch metrics:', error );
            } finally {
                state.isLoading = false;
            }
        }
    }
} );

This dashboard demonstrates:

  • data-wp-bind for dynamic attributes (data attributes, titles, ARIA)
  • data-wp-class for active states, conditional visibility, status indicators
  • data-wp-style for theme colours, dynamic chart heights, loading opacity
  • data-wp-text for metric values, dates, computed strings
  • Derived state for maximum chart value calculation
  • Event handling for time range switching (see [Article 7](Mastering Event Handling and DOM Interactions.md))
  • List rendering with data-wp-each for chart bars

Best Practices and Performance

Prefer CSS Classes Over Inline Styles

When possible, use data-wp-class with predefined CSS classes instead of data-wp-style:

<!-- ❌ Avoid: Multiple inline styles -->
<div
    data-wp-style--color="state.isError ? 'red' : 'green'"
    data-wp-style--font-weight="state.isError ? 'bold' : 'normal'"
    data-wp-style--border="state.isError ? '2px solid red' : 'none'"
>
    Message
</div>

<!-- ✅ Better: CSS class with styles defined in stylesheet -->
<div data-wp-class--error="state.isError">
    Message
</div>
.error {
    color: red;
    font-weight: bold;
    border: 2px solid red;
}

Why? CSS classes are more performant, easier to maintain, and enable browser caching.

Avoid Complex Expressions

Keep binding expressions simple. Move complex logic to getters:

// ❌ Avoid: Complex logic in binding
data-wp-text="state.items.filter(i => i.active).length > 0 ? 'Active items: ' + state.items.filter(i => i.active).length : 'No active items'"

// ✅ Better: Use derived state
state: {
    get activeItemsText() {
        const count = this.items.filter( i => i.active ).length;
        return count > 0 ? `Active items: ${ count }` : 'No active items';
    }
}
<p data-wp-text="state.activeItemsText"></p>

See [Article 2: Complex State Logic with Getters](Beyond the Basics_ Implementing Complex State Logic with Interactivity API Getters.md) for more on derived state patterns.

Use Descriptive State Property Names

Name state properties clearly to improve readability:

// ❌ Unclear naming
state: {
    x: true,
    y: '#ff0000',
    z: 50
}

// ✅ Clear naming
state: {
    modalVisible: true,
    primaryColor: '#ff0000',
    progressPercent: 50
}

Sanitize User-Generated Content

When binding user input to text or attributes, ensure proper sanitization:

actions: {
    updateUserBio( event ) {
        const { state } = store( 'myPlugin' );

        // Strip HTML tags for text content
        const sanitized = event.target.value.replace( /<[^>]*>/g, '' );
        state.userBio = sanitized;
    }
}

Conclusion

The Interactivity API’s binding directives provide a declarative, reactive approach to DOM manipulation. By connecting state directly to attributes, styles, classes, and content, you create UIs that automatically stay synchronized with your data without imperative DOM code.

Key Takeaways:

  • Use data-wp-bind for general attributes (src, href, disabled, aria-*)
  • Use data-wp-class for conditional CSS classes based on state
  • Use data-wp-style for dynamic inline styles (colours, dimensions, transforms)
  • Use data-wp-text for text content binding with expression support
  • Prefer CSS classes over inline styles when possible
  • Move complex logic to derived state getters
  • Combine binding directives with event handling and list rendering for complete interactivity
Wp block editor book nobg

A comprehensive guide


Master WordPress Block Development

Everything you need to master blocks in one place, from the first block to enterprise architecture.

60-Day 100% Money-Back Guarantee. Zero risk.