Frontend Interactivity & View Scripts
View Scripts
View scripts run on the frontend only, perfect for interactive features. Define in block.json:
{
"viewScript": "file:./view.js"
}Basic View Script
// src/view.js
document.addEventListener( 'DOMContentLoaded', () => {
const blocks = document.querySelectorAll( '.wp-block-theme-testimonial' );
blocks.forEach( ( block ) => {
const readMore = block.querySelector( '.read-more-btn' );
const content = block.querySelector( '.testimonial-full' );
if ( readMore && content ) {
readMore.addEventListener( 'click', () => {
content.classList.toggle( 'expanded' );
readMore.textContent = content.classList.contains( 'expanded' )
? 'Read Less'
: 'Read More';
} );
}
} );
} );Passing Data to View Scripts
Use data-* attributes in your Save component:
// save.js
export default function save( { attributes } ) {
const { rating, animationDuration } = attributes;
return (
<div
{ ...useBlockProps.save() }
data-rating={ rating }
data-animation-duration={ animationDuration }
>
{/* content */}
</div>
);
}// view.js
document.querySelectorAll( '.wp-block-theme-testimonial' ).forEach( ( block ) => {
const rating = parseInt( block.dataset.rating, 10 );
const duration = parseInt( block.dataset.animationDuration, 10 );
// Use the data
animateStars( block, rating, duration );
} );Accordion Example
Full interactive accordion block:
// save.js
export default function save( { attributes } ) {
const { items } = attributes;
return (
<div { ...useBlockProps.save() }>
{ items.map( ( item, index ) => (
<div className="accordion-item" key={ index }>
<button
className="accordion-header"
aria-expanded="false"
aria-controls={ `panel-${ index }` }
>
{ item.title }
<span className="accordion-icon">+</span>
</button>
<div
className="accordion-panel"
id={ `panel-${ index }` }
hidden
>
{ item.content }
</div>
</div>
) ) }
</div>
);
}// view.js
document.addEventListener( 'DOMContentLoaded', () => {
document.querySelectorAll( '.wp-block-theme-accordion' ).forEach( ( accordion ) => {
const headers = accordion.querySelectorAll( '.accordion-header' );
headers.forEach( ( header ) => {
header.addEventListener( 'click', () => {
const panel = document.getElementById(
header.getAttribute( 'aria-controls' )
);
const isOpen = header.getAttribute( 'aria-expanded' ) === 'true';
// Close all other panels (optional - for single open mode)
headers.forEach( ( h ) => {
h.setAttribute( 'aria-expanded', 'false' );
h.querySelector( '.accordion-icon' ).textContent = '+';
document.getElementById( h.getAttribute( 'aria-controls' ) ).hidden = true;
} );
// Toggle current
if ( ! isOpen ) {
header.setAttribute( 'aria-expanded', 'true' );
header.querySelector( '.accordion-icon' ).textContent = 'โ';
panel.hidden = false;
}
} );
} );
} );
} );The Interactivity API (WordPress 6.5+)
The modern way to add interactivity with declarative directives:
// block.json
{
"supports": {
"interactivity": true
},
"viewScriptModule": "file:./view.js"
}// save.js
export default function save( { attributes } ) {
return (
<div
{ ...useBlockProps.save() }
data-wp-interactive="theme-blocks"
data-wp-context={ JSON.stringify( { isOpen: false } ) }
>
<button data-wp-on--click="actions.toggle">
<span data-wp-text="state.buttonText">Open</span>
</button>
<div data-wp-bind--hidden="!context.isOpen">
Content here...
</div>
</div>
);
}// view.js
import { store, getContext } from '@wordpress/interactivity';
store( 'theme-blocks', {
state: {
get buttonText() {
const context = getContext();
return context.isOpen ? 'Close' : 'Open';
},
},
actions: {
toggle() {
const context = getContext();
context.isOpen = ! context.isOpen;
},
},
} );Interactivity Directives
| Directive | Purpose |
|---|---|
data-wp-interactive | Namespace for the block |
data-wp-context | Local state for this instance |
data-wp-on--click | Event handler |
data-wp-bind--hidden | Bind attribute to state |
data-wp-text | Set text content |
data-wp-class--active | Toggle class |
data-wp-watch | Run callback on state change |
Counter Example with Interactivity API
// save.js
export default function save( { attributes } ) {
return (
<div
{ ...useBlockProps.save() }
data-wp-interactive="theme-counter"
data-wp-context='{ "count": 0 }'
>
<button data-wp-on--click="actions.decrement">โ</button>
<span data-wp-text="context.count">0</span>
<button data-wp-on--click="actions.increment">+</button>
</div>
);
}// view.js (as ES module)
import { store, getContext } from '@wordpress/interactivity';
store( 'theme-counter', {
actions: {
increment() {
const context = getContext();
context.count++;
},
decrement() {
const context = getContext();
if ( context.count > 0 ) {
context.count--;
}
},
},
} );
View Script Modules: The Interactivity API uses ES modules. Use
viewScriptModule instead of viewScript in block.json.
Conditional Script Loading
Only load scripts when block is present:
// In PHP - already handled by viewScript
// Scripts only load when block is on the page
// For custom conditional loading:
function theme_enqueue_block_assets() {
if ( has_block( 'theme/testimonial' ) ) {
wp_enqueue_script( 'theme-carousel', /* ... */ );
}
}
add_action( 'wp_enqueue_scripts', 'theme_enqueue_block_assets' );Next Steps
In the final lesson, we'll learn how to package and distribute your blocks.
๐ฏ Lesson Complete! You can now add powerful frontend interactivity to your blocks.