Building a Reactive Frontend Framework inside Full Site Editing (FSE) Block Themes and theme.json Without Breaking Site Responsiveness
Leveraging `theme.json` for Reactive Frontend Logic in FSE Block Themes
Full Site Editing (FSE) and block themes have fundamentally shifted WordPress development towards a declarative, component-based approach. However, achieving truly reactive frontend experiences—where UI elements dynamically update based on state changes or user interactions—within this paradigm, especially when relying heavily on theme.json for styling and layout, presents unique challenges. This post dives into advanced techniques for building reactive frontend components that integrate seamlessly with FSE block themes and theme.json, ensuring site responsiveness and maintainability.
Understanding the `theme.json` Constraint and Opportunity
theme.json acts as the central configuration file for block themes, defining global styles, color palettes, typography settings, spacing, and layout constraints. While it excels at declarative styling, it’s not inherently designed for imperative frontend logic or state management. Our goal is to augment this declarative system with reactive capabilities without resorting to monolithic JavaScript bundles that bypass the FSE architecture.
The key is to leverage theme.json‘s structure to inform and control frontend JavaScript, rather than trying to embed complex logic directly within it. This involves strategically using theme.json‘s settings to pass data to JavaScript, and using JavaScript to dynamically modify DOM elements or trigger re-renders based on state.
Injecting Dynamic Data via `theme.json` Settings
One of the most effective ways to bridge the gap between theme.json and frontend logic is by defining custom settings within theme.json that can be accessed by JavaScript. These custom settings can represent dynamic data, feature flags, or configuration parameters that influence the behavior of your reactive components.
Consider a scenario where you want to conditionally display a “New Arrivals” badge on product blocks based on a site-wide setting. This setting can be managed within theme.json.
Example: Defining a Custom Setting in theme.json
Add a custom section to your theme.json file:
{
"version": 2,
"settings": {
"custom": {
"features": {
"showNewArrivalsBadge": true,
"newArrivalsThresholdDays": 7
},
"apiEndpoints": {
"products": "/wp-json/myplugin/v1/products"
}
}
},
// ... other theme.json settings
}
Accessing `theme.json` Settings in JavaScript
WordPress enqueues theme settings into the global JavaScript scope. You can access these values using the wp.data store, specifically the core/editor or core data stores, depending on the context (editor vs. frontend). For frontend access, these settings are often made available via a localized script handle.
First, ensure your JavaScript file is enqueued and localized with the theme settings. This is typically done in your theme’s functions.php or a dedicated plugin.
// In your theme's functions.php or a plugin file
function my_theme_enqueue_scripts() {
wp_enqueue_script(
'my-theme-reactivity',
get_template_directory_uri() . '/assets/js/reactivity.js',
array( 'wp-element', 'wp-data' ), // Dependencies for React and WP data API
filemtime( get_template_directory() . '/assets/js/reactivity.js' ),
true
);
// Localize theme settings
wp_localize_script(
'my-theme-reactivity',
'myThemeSettings',
array(
'showNewArrivalsBadge' => get_theme_mod( 'show_new_arrivals_badge', true ), // Example using theme mods, but ideally read from theme.json
'newArrivalsThresholdDays' => get_theme_mod( 'new_arrivals_threshold_days', 7 ),
'apiEndpoints' => array(
'products' => get_option( 'myplugin_products_api_endpoint', '/wp-json/myplugin/v1/products' ) // Example reading from options
)
)
);
// For direct theme.json access in the frontend, you might need a more direct approach
// or ensure it's passed via wp_add_inline_script if not automatically available.
// A common pattern is to expose it via a global JS variable.
$theme_json = json_decode( file_get_contents( get_template_directory() . '/theme.json' ), true );
if ( isset( $theme_json['settings']['custom'] ) ) {
wp_add_inline_script(
'my-theme-reactivity',
'const themeCustomSettings = ' . json_encode( $theme_json['settings']['custom'] ) . ';',
'before'
);
}
}
add_action( 'wp_enqueue_scripts', 'my_theme_enqueue_scripts' );
Now, in your reactivity.js file, you can access these settings:
// assets/js/reactivity.js
document.addEventListener( 'DOMContentLoaded', () => {
// Accessing settings localized via wp_localize_script (if not using themeCustomSettings)
// const showBadge = myThemeSettings.showNewArrivalsBadge;
// const threshold = myThemeSettings.newArrivalsThresholdDays;
// Accessing settings directly from theme.json via wp_add_inline_script
const showBadge = themeCustomSettings.features.showNewArrivalsBadge;
const threshold = themeCustomSettings.features.newArrivalsThresholdDays;
const productsApiEndpoint = themeCustomSettings.apiEndpoints.products;
if ( ! showBadge ) {
console.log( 'New arrivals badge feature is disabled.' );
return;
}
// Logic to fetch products and display badges
fetchProductsAndDisplayBadges( productsApiEndpoint, threshold );
} );
function fetchProductsAndDisplayBadges( apiUrl, thresholdDays ) {
fetch( apiUrl )
.then( response => response.json() )
.then( products => {
products.forEach( product => {
const productDate = new Date( product.date_created ); // Assuming 'date_created' field
const now = new Date();
const timeDiff = now.getTime() - productDate.getTime();
const daysDiff = Math.ceil( timeDiff / ( 1000 * 3600 * 24 ) );
if ( daysDiff <= thresholdDays ) {
const productElement = document.querySelector( `.product-id-${product.id}` ); // Target specific product element
if ( productElement ) {
const badge = document.createElement( 'span' );
badge.className = 'new-arrivals-badge';
badge.textContent = 'New!';
productElement.prepend( badge ); // Or append, depending on desired placement
}
}
} );
} )
.catch( error => console.error( 'Error fetching products:', error ) );
}
// Example of a reactive component using React (if you're using it in your theme)
// This would typically be part of a larger React application bundled for the theme.
/*
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
function ProductList() {
const [products, setProducts] = useState([]);
const showBadge = themeCustomSettings.features.showNewArrivalsBadge;
const threshold = themeCustomSettings.features.newArrivalsThresholdDays;
const productsApiEndpoint = themeCustomSettings.apiEndpoints.products;
useEffect(() => {
fetch(productsApiEndpoint)
.then(res => res.json())
.then(data => setProducts(data))
.catch(err => console.error("Error fetching products:", err));
}, []);
return (
{products.map(product => {
const productDate = new Date(product.date_created);
const now = new Date();
const timeDiff = now.getTime() - productDate.getTime();
const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24));
const isNew = showBadge && daysDiff <= threshold;
return (
{product.name}
{isNew && New!}
{product.price}
);
})}
);
}
// Assuming you have a div with id="product-list-container" in your template
// const container = document.getElementById('product-list-container');
// if (container) {
// ReactDOM.render( , container);
// }
*/
Dynamic Styling Based on `theme.json` and State
theme.json defines global styles, but reactive components often require dynamic styling that changes based on component state or user interaction. We can achieve this by:
- Using JavaScript to add/remove CSS classes to elements.
- Dynamically setting inline styles via JavaScript.
- Leveraging CSS Custom Properties (variables) defined in
theme.jsonand updated by JavaScript.
Example: Dynamic Badges with Themed Styles
Let’s enhance the “New Arrivals” badge. We want its color to be customizable via theme.json.
1. Define Color Variables in theme.json
{
"version": 2,
"settings": {
"color": {
"custom": {},
"palette": [
// ... existing palette
]
},
"custom": {
"features": {
"showNewArrivalsBadge": true,
"newArrivalsThresholdDays": 7
},
"apiEndpoints": {
"products": "/wp-json/myplugin/v1/products"
},
"badgeStyles": {
"newArrivalsColor": "var(--wp--preset--color--primary)", // Uses a theme preset
"newArrivalsBackgroundColor": "var(--wp--preset--color--secondary)"
}
}
},
// ...
}
Here, we’re referencing existing color presets. You could also define direct hex values.
2. Enqueue CSS for the Badge
Create a CSS file (e.g., assets/css/reactivity.css) and enqueue it.
/* assets/css/reactivity.css */
.new-arrivals-badge {
display: inline-block;
padding: 0.3em 0.6em;
font-size: 0.75em;
font-weight: bold;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.25rem;
margin-right: 0.5rem; /* Adjust spacing */
/* Dynamic styles will be applied via inline styles or CSS variables */
}
/* If using CSS variables directly */
.new-arrivals-badge {
color: var(--badge-text-color);
background-color: var(--badge-bg-color);
}
// In functions.php or plugin
function my_theme_enqueue_styles() {
// ... enqueue scripts ...
wp_enqueue_style(
'my-theme-reactivity-styles',
get_template_directory_uri() . '/assets/css/reactivity.css',
array(), // Dependencies
filemtime( get_template_directory() . '/assets/css/reactivity.css' )
);
}
add_action( 'wp_enqueue_scripts', 'my_theme_enqueue_styles' );
3. Apply Dynamic Styles with JavaScript
Modify the JavaScript to apply the styles. We can either set inline styles or, preferably, set CSS Custom Properties on the element.
// assets/js/reactivity.js (updated)
document.addEventListener( 'DOMContentLoaded', () => {
const showBadge = themeCustomSettings.features.showNewArrivalsBadge;
const threshold = themeCustomSettings.features.newArrivalsThresholdDays;
const productsApiEndpoint = themeCustomSettings.apiEndpoints.products;
if ( ! showBadge ) {
return;
}
// Get badge style settings from theme.json
const badgeStyles = themeCustomSettings.badgeStyles || {};
const textColor = badgeStyles.newArrivalsColor || 'var(--wp--preset--color--white)'; // Default to white text
const bgColor = badgeStyles.newArrivalsBackgroundColor || 'var(--wp--preset--color--primary)'; // Default to primary color
fetch( productsApiEndpoint )
.then( response => response.json() )
.then( products => {
products.forEach( product => {
const productDate = new Date( product.date_created );
const now = new Date();
const timeDiff = now.getTime() - productDate.getTime();
const daysDiff = Math.ceil( timeDiff / ( 1000 * 3600 * 24 ) );
if ( daysDiff <= threshold ) {
const productElement = document.querySelector( `.product-id-${product.id}` );
if ( productElement ) {
const badge = document.createElement( 'span' );
badge.className = 'new-arrivals-badge';
badge.textContent = 'New!';
// Option 1: Set CSS Custom Properties (Recommended)
badge.style.setProperty( '--badge-text-color', textColor );
badge.style.setProperty( '--badge-bg-color', bgColor );
// Option 2: Set inline styles directly (less flexible)
// badge.style.color = textColor;
// badge.style.backgroundColor = bgColor;
productElement.prepend( badge );
}
}
} );
} )
.catch( error => console.error( 'Error fetching products:', error ) );
} );
This approach allows users to customize the badge’s appearance through the WordPress Customizer or Site Editor, by modifying the corresponding color presets or by defining custom colors that are then referenced in theme.json. The JavaScript dynamically applies these settings.
Building Reactive Components with Block Filters and JavaScript
For more complex interactions or state management within the FSE context, you can leverage WordPress’s block filters. These filters allow you to modify block behavior, attributes, and even render output using JavaScript, effectively creating dynamic or reactive components.
Example: A “Read More” Toggle for Post Excerpts
Imagine you want to add a “Read More” button to post excerpts that expands the excerpt on click. This can be achieved by filtering the core core/post-excerpt block.
1. Registering a Block Filter
// assets/js/block-filters.js
wp.hooks.addFilter(
'blocks.getSaveElement', // Filter for the saved (rendered) output
'my-theme/excerpt-read-more',
function( element, blockType, attributes ) {
// Only apply to the core/post-excerpt block
if ( blockType.name === 'core/post-excerpt' ) {
// Check if the excerpt is short enough to warrant a "read more"
// This logic might need to be more sophisticated, perhaps checking content length
// or a custom attribute. For simplicity, let's assume we always add it.
// We need to ensure the excerpt content is available and potentially wrap it.
// The 'element' here is the React element representing the saved output.
// We'll need to modify its children or add a sibling.
// This is a simplified example. Real-world implementation might involve
// checking attributes, content length, and ensuring proper DOM structure.
// A more robust solution might involve a custom block or modifying the
// block's edit/save components directly if you're building custom blocks.
// For a simple toggle, we might add a button and use CSS/JS to control visibility.
// Let's assume the excerpt element has a specific class or structure.
// A common pattern is to add a wrapper div and control its height/overflow.
if ( element && element.props && element.props.children ) {
const excerptContent = element.props.children;
const wrapper = wp.element.createElement(
'div',
{
className: 'excerpt-wrapper',
style: { maxHeight: '100px', overflow: 'hidden', transition: 'max-height 0.3s ease-in-out' }
},
excerptContent
);
const readMoreButton = wp.element.createElement(
'button',
{
className: 'read-more-button',
onClick: () => {
const wrapperElement = document.querySelector('.excerpt-wrapper'); // Find the specific wrapper
if (wrapperElement) {
wrapperElement.style.maxHeight = wrapperElement.scrollHeight + 'px';
// Optionally hide the button after expanding
// document.querySelector('.read-more-button').style.display = 'none';
}
}
},
'Read More'
);
// Return a new element structure
return wp.element.createElement(
'div',
{ className: 'post-excerpt-container' },
wrapper,
readMoreButton
);
}
}
return element; // Return original element if not the target block
}
);
// Add filter for the editor view as well, to ensure consistency
wp.hooks.addFilter(
'editor.BlockEdit',
'my-theme/excerpt-read-more-edit',
function( BlockEdit ) {
return function( props ) {
if ( props.name === 'core/post-excerpt' ) {
// Logic to add the button in the editor might be different,
// potentially requiring modification of the block's edit component.
// This is often more complex than modifying the save output.
// For simplicity, we'll focus on the frontend rendering here.
}
return wp.element.createElement( BlockEdit, props );
};
}
);
2. Enqueue the JavaScript and CSS
Ensure block-filters.js is enqueued, along with corresponding CSS for .excerpt-wrapper and .read-more-button.
// In functions.php or plugin
function my_theme_enqueue_block_filters() {
wp_enqueue_script(
'my-theme-block-filters',
get_template_directory_uri() . '/assets/js/block-filters.js',
array( 'wp-hooks', 'wp-element', 'wp-editor' ), // Dependencies
filemtime( get_template_directory() . '/assets/js/block-filters.js' ),
true
);
// Enqueue corresponding CSS for styling the wrapper and button
}
add_action( 'enqueue_block_editor_assets', 'my_theme_enqueue_block_filters' ); // For editor
add_action( 'wp_enqueue_scripts', 'my_theme_enqueue_block_filters' ); // For frontend
This filter approach allows you to inject reactive behavior into existing blocks without creating entirely new custom blocks, promoting code reuse and adherence to the FSE structure. The key is understanding how block filters interact with the block’s save/edit functions and the underlying React components.
Advanced Diagnostics and Troubleshooting
When building reactive features within FSE themes, several issues can arise. Here’s a diagnostic checklist:
1. JavaScript Errors in the Console
Symptom: Features not working, unexpected behavior, errors in the browser’s developer console.
- Check Enqueuing: Verify that your JavaScript files are correctly enqueued using
wp_enqueue_scriptand that dependencies (likewp-element,wp-hooks,wp-data) are listed. Use browser developer tools (Network tab) to confirm the script is loaded. - Check Localization: Ensure any localized data (like theme settings) is correctly passed and accessible via the expected global variable (e.g.,
myThemeSettings,themeCustomSettings). Useconsole.log()liberally to inspect variables. - Scope Issues: Be mindful of JavaScript scope. Variables defined within one function might not be accessible in another. Ensure your event listeners (like
DOMContentLoaded) are correctly placed. - React/WP Element Errors: If using React components or
wp.element, ensure you’re using the correct API calls and that the necessary WordPress packages are loaded.
2. `theme.json` Settings Not Reflecting
Symptom: Features controlled by theme.json settings (like the badge visibility) are not behaving as expected.
- Cache Clearing: WordPress and browser caches can sometimes serve stale data. Clear all caches (WordPress, CDN, browser).
- Correct Path to
theme.json: Ensure the PHP code readingtheme.jsonis using the correct path (e.g.,get_template_directory() . '/theme.json'). - JSON Validity: Validate your
theme.jsonfile using an online JSON validator. Syntax errors can prevent it from being parsed correctly. - Access Method: Double-check how you’re accessing the settings in JavaScript. If using
wp_add_inline_script, ensure the variable name matches. If relying on automatic global exposure, confirm that mechanism is active and correct for your WordPress version. - Theme Mod vs. Direct Read: If you’re mixing
get_theme_mod()with directtheme.jsonreads, ensure consistency. For FSE, relying on direct reads or specific data stores is generally preferred over theme mods for global settings.
3. Responsiveness Issues
Symptom: Layout breaks on different screen sizes, elements overlap, or become unusable.
- CSS Specificity and Cascade: Ensure your custom CSS for reactive elements has sufficient specificity or is placed correctly in the cascade to override default theme styles. Use browser developer tools to inspect applied styles.
- Mobile-First Design: Apply responsive styles using media queries in your CSS. Ensure that dynamic elements (like badges or toggles) are designed to fit within smaller viewports.
- Viewport Units: Be cautious with viewport units (
vw,vh) if your reactive elements significantly alter layout. They can sometimes lead to unexpected results. - JavaScript-Driven Layout: If JavaScript is directly manipulating element dimensions or positions, ensure these calculations are robust and account for different screen sizes and content variations. Test thoroughly across devices or using browser emulation.
- `theme.json` Layout Settings: Ensure your JavaScript logic doesn’t conflict with layout constraints defined in
theme.json(e.g.,contentSize,wideSize).
Conclusion
Building reactive frontend experiences within FSE block themes requires a thoughtful integration of declarative styling from theme.json and imperative JavaScript logic. By strategically using theme.json to pass configuration, leveraging block filters for dynamic behavior, and employing robust CSS and JavaScript practices, developers can create sophisticated, responsive, and maintainable user interfaces. The key is to view theme.json as a powerful configuration layer that informs, rather than dictates, frontend interactivity, ensuring that the core principles of FSE are maintained while delivering modern web application experiences.