Engineering Highlights
From Recent Projects
A closer look at specific technical problems I've solved — block system architecture, editor extensions, accessibility patterns, and CSS utilities — across WordPress theme development work.
ACF Block Registration & Organization
A convention-over-configuration block registration system for a custom WordPress parent/child theme framework. Blocks self-register from directory structure — no manual registration calls needed.
Key Design Decisions
- ▸Convention-over-config: Drop a
block.jsonin a folder and the block auto-registers — no index files or manual calls - ▸Parent/child inheritance: Child theme blocks override parent blocks of the same name via
array_uniqueafter merging both directories - ▸Per-block isolation: Each block directory owns its
functions.php, AJAX handler, and editor-only script/style variants - ▸Cache-busting: Assets use
filemtime()as the version parameter — no manual version bumps during development
ACF JSON Co-location
ACF field group JSON files are automatically saved into each block's own acf-json/ directory — keeping block code, template, and field configuration together in the same place.
- ▸JSON filename derived from the block slug automatically
- ▸Directory auto-created if it doesn't exist on first save
- ▸All block
acf-json/paths registered as load points on init
<?php
// Auto-discovers all blocks in parent + child theme directories.
// Child blocks override parent blocks of the same name via array_unique.
function get_block_dirs(): array|false {
$root = get_template_directory() . '/blocks';
$block_dirs = array_map(
fn($dir) => basename($dir),
array_filter(glob($root . '/*'), 'is_dir')
);
if (is_child_theme()) {
$child_root = get_stylesheet_directory() . '/blocks';
$child_dirs = array_map(
fn($dir) => basename($dir),
array_filter(glob($child_root . '/*'), 'is_dir')
);
// Child blocks override parent — duplicates removed in merge
$block_dirs = array_unique(array_merge($block_dirs, $child_dirs));
}
return $block_dirs;
}
// Priority 3: register before default init hooks.
add_action('init', function () {
$block_dirs = get_block_dirs();
foreach ($block_dirs as $block_dir) {
$block_json = locate_template('/blocks/' . $block_dir . '/block.json');
if (!$block_json) continue;
// Register editor-only script/style variants (-editor, -init)
foreach (['-editor', '-init'] as $variant) {
$path = '/blocks/' . $block_dir . '/dist/' . $block_dir . $variant . '.min.js';
if ($script = locate_template($path)) {
wp_register_script(
'theme-block-js-' . $block_dir . $variant,
get_theme_file_uri($path),
[],
filemtime($script) // cache-bust by file modification time
);
}
}
// Per-block: auto-load functions.php if it exists
locate_template('blocks/' . $block_dir . '/functions.php', true, true);
// Per-block: register AJAX nonce if ajax.php exists
if (locate_template('blocks/' . $block_dir . '/ajax.php', true, true)) {
wp_localize_script(
'theme-blocks-scripts',
'theme_block_' . str_replace('-', '_', $block_dir) . '_ajax',
[
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('theme-block-' . $block_dir . '-nonce'),
]
);
}
register_block_type($block_json);
}
}, 3); <?php
// Route ACF JSON saves to the block's own acf-json/ subdirectory,
// co-locating field group configuration with the block files it belongs to.
add_filter('acf/json/save_paths', function ($paths, $post) {
if (($post['location'][0][0]['param'] ?? false) === 'block') {
$dir = explode('/', $post['location'][0][0]['value'])[1] ?? '_default';
$paths = [get_stylesheet_directory() . '/blocks/' . $dir . '/acf-json'];
// Auto-create the directory if it doesn't exist yet
if (!file_exists($paths[0])) {
mkdir($paths[0], 0755, true);
}
}
return $paths;
}, 10, 2);
// Name each JSON file after the block slug (e.g., hero.json, banner.json)
add_filter('acf/json/save_file_name', function ($filename, $post) {
if (($post['location'][0][0]['param'] ?? false) === 'block') {
$block_name = explode('/', $post['location'][0][0]['value'])[1] ?? '_default';
return $block_name . '.json';
}
return $filename;
}, 10, 3);
// Load ACF JSON from the parent theme, child theme,
// AND each block's own acf-json/ directory.
add_filter('acf/settings/load_json', function ($paths) {
if (is_child_theme()) {
$paths[0] = get_template_directory() . '/acf-json'; // parent
$paths[1] = get_stylesheet_directory() . '/acf-json'; // child overrides
}
foreach (get_block_dirs() as $block_dir) {
if ($block_path = locate_template('/blocks/' . $block_dir . '/acf-json')) {
$paths[] = $block_path;
}
}
return $paths;
}); Block Editor Thread Decoration System
A full-stack feature that adds decorative SVG “thread” elements to any supported block — built without modifying core blocks, using WordPress’s block filter API across three lifecycle phases.
Three-Phase Architecture
- 1 Attribute registration —
blocks.registerBlockTypefilter injects athreadDecorationsarray attribute onto supported blocks - 2 Editor UI — a higher order component wraps
BlockEditto add an inspector panel with controls for thread type, layer, position, and mobile behavior - 3 Save serialization — decoration config is serialized as a
data-threadsJSON attribute on the saved block HTML
PHP Server Render
- ▸Uses WordPress 6.2+
WP_HTML_Tag_Processorfor safe, spec-compliant HTML manipulation — no regex on HTML - ▸Reads the
data-threadsattribute, strips it from the output, then injects thread<div>elements at the correct layer positions - ▸Background threads inserted after the opening tag; overlay threads inserted before the closing tag
- ▸SVG assets are Vite-versioned at build time, injected as
background-imageURLs via a JS asset map
Why Block Filters?
Using addFilter rather than modifying block templates means thread decoration support can be added to any block — including core blocks like core/group — without forking or patching them. It stays cleanly separated from block business logic and can be maintained or removed independently.
// Three-phase WordPress block filter system — extends any block non-destructively.
// Phase 1: Register attribute → Phase 2: Editor UI → Phase 3: Save serialization
// Phase 1: Add threadDecorations attribute to supported block types
addFilter('blocks.registerBlockType', 'theme/thread-attributes', (settings, name) => {
if (!BLOCKS_WITH_THREAD_SUPPORT.includes(name)) return settings;
return {
...settings,
attributes: {
...settings.attributes,
threadDecorations: { type: 'array', default: [] },
},
};
});
// Phase 2: HOC injects a Thread Decorations panel into each block's inspector
const withThreadInspectorControls = createHigherOrderComponent((BlockEdit) => {
return (props) => {
if (!supportsThreads(props.name)) return <BlockEdit {...props} />;
const { attributes, setAttributes } = props;
const { threadDecorations = [] } = attributes;
const addThread = () => setAttributes({
threadDecorations: [
...threadDecorations,
{ id: Date.now(), thread: 'bottom-right', layer: 'background',
position: 'default', mobileBehavior: 'display' },
],
});
return (
<Fragment>
<BlockEdit {...props} />
<InspectorControls>
<PanelBody title="Thread Decorations" initialOpen={false}>
{threadDecorations.map((decoration) => (
<div key={decoration.id}>
<SelectControl label="Thread Type"
value={decoration.thread}
options={availableThreads}
onChange={(v) => updateThread(decoration.id, 'thread', v)}
/>
<SelectControl label="Layer"
value={decoration.layer}
options={[
{ value: 'background', label: 'Background (behind content)' },
{ value: 'overlay', label: 'Overlay (in front of content)' },
]}
onChange={(v) => updateThread(decoration.id, 'layer', v)}
/>
<SelectControl label="Mobile Behavior"
value={decoration.mobileBehavior || 'display'}
options={mobileBehaviorOptions}
onChange={(v) => updateThread(decoration.id, 'mobileBehavior', v)}
/>
<Button isDestructive variant="secondary"
onClick={() => removeThread(decoration.id)}>
Remove Thread
</Button>
</div>
))}
<Button variant="secondary" onClick={addThread}>
Add Thread Decoration
</Button>
</PanelBody>
</InspectorControls>
</Fragment>
);
};
}, 'withThreadInspectorControls');
// Phase 3: Serialize decoration config as data-threads JSON on saved block HTML.
// PHP then reads this attribute server-side to inject the actual SVG elements.
addFilter('blocks.getSaveContent.extraProps', 'theme/thread-save',
(extraProps, blockType, { threadDecorations = [] }) => {
if (!supportsThreads(blockType.name) || !threadDecorations.length) {
return extraProps;
}
return { ...extraProps, 'data-threads': JSON.stringify(threadDecorations) };
}
); <?php
// PHP side: reads the serialized data-threads attribute from saved block HTML
// and injects the actual SVG thread elements using WP_HTML_Tag_Processor.
public function render(string $block_content, array $block): string
{
$thread_decorations = $block['attrs']['threadDecorations'] ?? null;
if (!$thread_decorations && !str_contains($block_content, 'data-threads')) {
return $block_content;
}
// Extract decoration config from the data-threads attribute
if (!$thread_decorations) {
$processor = new WP_HTML_Tag_Processor($block_content);
if ($processor->next_tag(['tag_name' => 'div'])) {
$threads_attr = $processor->get_attribute('data-threads');
if ($threads_attr) {
$thread_decorations = json_decode($threads_attr, true);
}
}
}
return $this->addThreadDecorations($block_content, $thread_decorations ?? []);
}
protected function addThreadDecorations(string $block_content, array $decorations): string
{
$processor = new WP_HTML_Tag_Processor($block_content);
if (!$processor->next_tag('div')) return $block_content;
// Clean up — data-threads is no longer needed in the final HTML
$processor->remove_attribute('data-threads');
$html = $processor->get_updated_html();
$tag_end_pos = strpos($html, '>');
$background_threads = $overlay_threads = '';
foreach ($decorations as $decoration) {
$html_fragment = $this->renderThreadDecoration(
$decoration['thread'] ?? '',
$decoration['layer'] ?? 'background',
$decoration['position'] ?? 'default',
$decoration['mobileBehavior'] ?? 'display'
);
$decoration['layer'] === 'background'
? $background_threads .= $html_fragment
: $overlay_threads .= $html_fragment;
}
$before = substr($html, 0, $tag_end_pos + 1);
$after = substr($html, $tag_end_pos + 1);
// Overlay threads inserted before the block's closing </div>
if (!empty($overlay_threads)) {
$pos = strrpos($after, '</div>');
$after = substr($after, 0, $pos) . $overlay_threads . substr($after, $pos);
}
// Background threads inserted immediately after the opening tag
return $before . $background_threads . $after;
} Video Player Block
An ACF Composer block that auto-detects poster images from YouTube, Vimeo, or Wistia, then delivers an accessible lightbox or inline player with proper focus management and DOM cleanup.
PHP: ACF Composer Block
- ▸Class-based block registration via ACF Composer — fields defined in PHP, data processed in
item(), passed to Blade template viawith() - ▸No custom poster?
getVideoServicePoster()auto-fetches from YouTube (direct URL construction), Vimeo (oEmbed API), or Wistia (oEmbed API) - ▸Display mode selectable per block: Lightbox (modal overlay) or Inline (replaces poster in place)
JS: Accessibility-First Modal
- ▸
inertattribute on all background content — traps focus and prevents screen reader navigation outside the modal - ▸Focus management: saves the triggering element, shifts focus to the close button on open, restores focus on close
- ▸DOM position restored after close — modal moved to
bodyend for z-index, then returned to its original position - ▸Full cleanup via
destroy()— stores bound handlers for safe removal, prevents memory leaks on re-initialization - ▸Keyboard support: Enter/Space to open, Escape to close, consistent with ARIA dialog pattern
<?php
// ACF Composer class-based block — fields() defines the ACF schema,
// item() processes the data, with() passes it to the Blade template.
public function fields(): array
{
$fields = Builder::make('video_player');
$fields
->addUrl('video_url', [
'label' => 'Video URL',
'instructions' => 'YouTube, Vimeo, or Wistia links.',
'required' => true,
])
->addImage('poster_image', [
'instructions' => 'Upload a custom poster, or leave blank to auto-fetch from the video service.',
'return_format' => 'id',
])
->addRadio('display_mode', [
'choices' => ['lightbox' => 'Lightbox Modal', 'inline' => 'Play In Place'],
'default_value' => 'lightbox',
]);
return $fields->build();
}
public function item(): array
{
$poster = get_field('poster_image');
$video = get_field('video_url');
$mode = get_field('display_mode') ?: 'lightbox';
// No custom poster? Auto-fetch the thumbnail from the video service.
$poster_image = $poster
? ['url' => wp_get_attachment_image_url($poster, 'large'),
'alt' => get_post_meta($poster, '_wp_attachment_image_alt', true) ?: '']
: $this->getVideoServicePoster($video);
return compact('video', 'poster_image', 'mode');
}
// Detects YouTube, Vimeo, or Wistia URLs and fetches their thumbnails.
private function getVideoServicePoster(?string $url): array
{
if (!$url) return ['url' => null, 'alt' => ''];
// YouTube — construct thumbnail URL directly, no API call needed
if (preg_match('/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]+)/', $url, $m)) {
return ['url' => "https://img.youtube.com/vi/{$m[1]}/maxresdefault.jpg",
'alt' => 'YouTube video poster'];
}
// Vimeo — fetch via oEmbed JSON endpoint
if (preg_match('/vimeo\.com\/\d+/', $url)) {
$data = json_decode(wp_remote_retrieve_body(
wp_remote_get("https://vimeo.com/api/oembed.json?url=" . urlencode($url))
), true);
if (!empty($data['thumbnail_url'])) {
return ['url' => $data['thumbnail_url'], 'alt' => $data['title'] ?? 'Vimeo video poster'];
}
}
// Wistia — fetch via fast.wistia.com oEmbed endpoint
if (preg_match('/wistia\.(?:com|net)/', $url)) {
$data = json_decode(wp_remote_retrieve_body(
wp_remote_get("https://fast.wistia.com/oembed?url=" . urlencode($url))
), true);
if (!empty($data['thumbnail_url'])) {
return ['url' => $data['thumbnail_url'], 'alt' => $data['title'] ?? 'Wistia video poster'];
}
}
return ['url' => null, 'alt' => ''];
} // Accessibility-first lightbox with focus trapping, DOM restoration, and cleanup.
export default class VideoPlayer {
constructor() {
this.modal = document.getElementById('videoPlayerModal');
this.container = document.getElementById('videoPlayerContainer');
this.closeBtn = document.getElementById('videoPlayerClose');
this.overlay = document.querySelector('.video-player-overlay');
// Store bound handlers so they can be removed on destroy()
this.boundHandlers = { escapeKey: null, closeBtn: null, overlay: null,
lightbox: [], inline: [] };
this.lastFocusedElement = null;
this.bindEvents();
}
openModal(trigger) {
const videoUrl = trigger.querySelector('.video-data')
?.getAttribute('data-embed-code');
if (!videoUrl) return;
// Remember what was focused so we can restore it on close
this.lastFocusedElement = document.activeElement;
// Track original DOM position so we can restore it after close
this.originalParent = this.modal.parentNode;
this.originalNextSibling = this.modal.nextSibling;
// Move to body end for reliable z-index stacking context
document.body.appendChild(this.modal);
this.container.innerHTML = this.generateEmbedCode(videoUrl);
this.modal.classList.replace('hidden', 'block');
document.body.classList.add('overflow-hidden');
// Animate in on the next frame
setTimeout(() => {
this.overlay?.classList.add('opacity-100');
this.container.classList.add('scale-100', 'opacity-100');
}, 10);
// Mark all other body children inert — traps focus + prevents interaction
this.setBackgroundInert(true);
// Shift keyboard focus to the close button for screen reader users
setTimeout(() => this.closeBtn?.focus(), 100);
}
closeModal() {
this.overlay?.classList.remove('opacity-100');
this.container.classList.remove('scale-100', 'opacity-100');
setTimeout(() => {
this.modal.classList.replace('block', 'hidden');
this.container.innerHTML = '';
document.body.classList.remove('overflow-hidden');
// Restore modal to its original position in the DOM
this.originalNextSibling
? this.originalParent.insertBefore(this.modal, this.originalNextSibling)
: this.originalParent.appendChild(this.modal);
// Re-enable background content
this.setBackgroundInert(false);
// Return focus to the element that triggered the modal
this.lastFocusedElement?.focus();
this.lastFocusedElement = null;
}, 300);
}
setBackgroundInert(isInert) {
Array.from(document.body.children)
.filter(child => child !== this.modal)
.forEach(child => isInert
? child.setAttribute('inert', '')
: child.removeAttribute('inert')
);
}
// Full cleanup for SPA-style re-initialization
destroy() {
this.boundHandlers.lightbox.forEach(({ trigger, clickHandler, keyHandler }) => {
trigger.removeEventListener('click', clickHandler);
trigger.removeEventListener('keydown', keyHandler);
});
if (this.boundHandlers.escapeKey) {
document.removeEventListener('keydown', this.boundHandlers.escapeKey);
}
}
} Composable Clip-Path Utility System
A Tailwind v4 @utility system for CSS polygon clip-paths where each corner is individually controlled via CSS custom properties — composable, responsive, and sub-pixel accurate.
Tailwind v4 @utility API
- ▸Custom properties as composition layer:
--clip-y-tl,--clip-y-tr, etc. let individual utilities set just one corner without affecting others - ▸
clip-quadreads all four CSS custom properties and applies theclip-path: polygon()— utilities stack composably - ▸Utilities accept both spacing scale integers (
clip-tl-4) and arbitrary lengths (clip-tl-[2rem])
Sub-Pixel & Gap Handling
- ▸1px bleed: each polygon corner is nudged 1px outward to prevent hairline gaps caused by browser sub-pixel rendering
- ▸
clip-pull-top/bottomutilities usecalc(-1 * max(...))to close the visible gap between adjacent clipped sections
Smart Auto-Pull with :has()
A CSS general sibling rule detects when a clipped-bottom section is directly followed by a clipped-top section and automatically applies the negative margin — no extra utility class required on the second element.
Usage
<!-- Angled bottom-right corner -->
<section class="clip-quad clip-br-14">...</section>
<!-- Angled top and bottom, both sides -->
<section class="clip-quad clip-tl-14 clip-br-14">...</section>
<!-- Auto-pull: no extra class needed when sections are adjacent -->
<section class="clip-quad clip-br-14">Section A</section>
<section class="clip-quad clip-tl-14">Section B</section> /* Tailwind v4 @utility API — composable CSS clip-path system.
* Each corner is independently controllable via CSS custom properties.
*
* Usage example:
* <div class="clip-quad clip-tl-0 clip-tr-14 clip-bl-0 clip-br-14">
*/
/* Corner offset utilities — accept spacing scale integers or arbitrary lengths */
@utility clip-tl-* {
--clip-y-tl: --spacing(--value(integer));
--clip-y-tl: --value([length]);
}
@utility clip-tr-* { --clip-y-tr: --spacing(--value(integer)); --clip-y-tr: --value([length]); }
@utility clip-bl-* { --clip-y-bl: --spacing(--value(integer)); --clip-y-bl: --value([length]); }
@utility clip-br-* { --clip-y-br: --spacing(--value(integer)); --clip-y-br: --value([length]); }
/* Base utility — applies the clip-path polygon using the four CSS custom properties */
@utility clip-quad {
--clip-x-tl: 0px; --clip-y-tl: 0px;
--clip-x-tr: 0px; --clip-y-tr: 0px;
--clip-x-bl: 0px; --clip-y-bl: 0px;
--clip-x-br: 0px; --clip-y-br: 0px;
/* 1px adjustment per corner compensates for sub-pixel rendering inaccuracies */
clip-path: polygon(
calc(var(--clip-x-tl) - 1px) calc(var(--clip-y-tl) - 1px),
calc(100% - var(--clip-x-tr) + 1px) calc(var(--clip-y-tr) - 1px),
calc(100% - var(--clip-x-br) + 1px) calc(100% - var(--clip-y-br) + 1px),
calc(var(--clip-x-bl) - 1px) calc(100% - var(--clip-y-bl) + 1px)
);
}
/* Pull utilities close the visual gap between adjacent clipped sections */
@utility clip-pull-top { margin-top: calc(-1 * max(var(--clip-y-tl, 0), var(--clip-y-tr, 0))); }
@utility clip-pull-bottom { margin-bottom: calc(-1 * max(var(--clip-y-bl, 0), var(--clip-y-br, 0))); }
/* Auto-pull using :has() — if a clipped-bottom block precedes a clipped-top block,
* apply the negative margin automatically without extra utility classes */
:is(.clip-quad):where([class*="clip-b"],[class*="clip-y-b"]):not(.clip-pull-none)
+ :is(.clip-quad):where([class*="clip-t"],[class*="clip-y-t"]):not(.clip-pull-none) {
@apply clip-pull-top;
}
} Want to see what these blocks power?
View Featured Projects