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-bind, data-wp-class, data-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 disabled, checked, readonly, 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
classattribute are preserved - Multiple
data-wp-classdirectives 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-bindfor dynamic attributes (data attributes, titles, ARIA)data-wp-classfor active states, conditional visibility, status indicatorsdata-wp-stylefor theme colours, dynamic chart heights, loading opacitydata-wp-textfor 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-eachfor 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-bindfor general attributes (src, href, disabled, aria-*) - Use
data-wp-classfor conditional CSS classes based on state - Use
data-wp-stylefor dynamic inline styles (colours, dimensions, transforms) - Use
data-wp-textfor 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
