Every prior lesson enqueued the widget unconditionally on every front-end page. That’s fine for building and testing, it’s not how a site owner should have to install it. This closing lesson wraps the widget in a shortcode so it can be dropped into any page or post deliberately, only loads its assets where it’s actually used, and adds the accessibility basics a public-facing chat interface needs: keyboard navigation and ARIA live regions so new messages are actually announced, not just visually appended.
Every prior lesson in this course, this one assembles the final version of the widget built throughout.
Step 1: wrap the widget in a shortcode
// File: inc/chat-widget-shortcode.php
add_shortcode( 'site_chat_widget', function () {
my_plugin_enqueue_chat_widget_assets();
return '<div id="site-chat-widget-mount"></div>';
} );
function my_plugin_enqueue_chat_widget_assets() {
if ( wp_script_is( 'site-chat-widget', 'enqueued' ) ) {
return; // Already enqueued, e.g. the shortcode appears more than once on a page.
}
wp_enqueue_script(
'site-chat-widget',
plugins_url( 'assets/js/chat-widget.js', __DIR__ ),
array(),
'1.0.0',
true
);
wp_enqueue_style(
'site-chat-widget',
plugins_url( 'assets/css/chat-widget.css', __DIR__ ),
array(),
'1.0.0'
);
wp_localize_script( 'site-chat-widget', 'siteChatWidget', array(
'restUrl' => esc_url_raw( rest_url( 'my-plugin/v1/chat' ) ),
'nonce' => wp_create_nonce( 'wp_rest' ),
) );
}
A site owner now adds [site_chat_widget] to any page, a footer template, or a widget
area, and only pages that actually render it pay the cost of loading the script and
stylesheet. This replaces the unconditional wp_enqueue_scripts hook from Lesson 2
entirely, that version was a reasonable way to get started, not the shipping version.
If your site’s editorial workflow leans on the block editor rather than shortcodes,
register a simple block whose render_callback calls the same
my_plugin_enqueue_chat_widget_assets() function and returns the same mount point
markup. The enqueuing logic doesn’t change, only how an editor places it on a page does.
Step 2: keyboard navigation
The toggle button already receives keyboard focus and activates on Enter or Space for
free, since it’s a real <button> element rather than a styled <div>. Two things
still need to be added explicitly: closing the panel with Escape, and returning focus to
the toggle button when it closes, so keyboard users aren’t left with their focus stranded
on a hidden element.
// File: assets/js/chat-widget.js
document.addEventListener( 'keydown', function ( event ) {
if ( event.key === 'Escape' && ! panel.hidden ) {
panel.hidden = true;
toggle.focus();
}
} );
Step 3: ARIA live regions for new messages
Visually, a new message just appears in the message list. For a screen reader user, an
appended <div> is silent unless the container is marked as a live region, the browser
has to be told this content updates and should be announced:
// File: assets/js/chat-widget.js
messages.setAttribute( 'role', 'log' );
messages.setAttribute( 'aria-live', 'polite' );
messages.setAttribute( 'aria-relevant', 'additions' );
aria-live="polite" announces new content without interrupting whatever the user is
currently doing, appropriate for a reply arriving while someone might still be reading
the previous message. role="log" signals this is a running log of messages, which
several screen readers handle with sensible defaults for how new entries get announced.
<!-- File: assets/js/chat-widget.js (markup this generates) -->
<div id="site-chat-messages" role="log" aria-live="polite" aria-relevant="additions">
<!-- messages appended here are announced automatically -->
</div>
Setting aria-live on an element you’re about to create and immediately fill with
content in the same operation doesn’t reliably announce that first item in every screen
reader. Mount the empty container with its ARIA attributes already in place, then append
messages into it afterward, exactly the order Step 3’s code does.
Step 4: finish the accessible markup
Pulling the whole widget together with labels and roles in place:
// File: assets/js/chat-widget.js
function createWidget( mount ) {
mount.innerHTML =
'<button id="site-chat-toggle" aria-label="Open chat assistant" aria-expanded="false">Chat</button>' +
'<div id="site-chat-panel" role="dialog" aria-label="Chat with us" hidden>' +
' <div id="site-chat-messages" role="log" aria-live="polite" aria-relevant="additions"></div>' +
' <form id="site-chat-form">' +
' <label for="site-chat-input" class="screen-reader-text">Your message</label>' +
' <input id="site-chat-input" type="text" maxlength="500" placeholder="Ask a question" autocomplete="off" />' +
' <button type="submit">Send</button>' +
' </form>' +
'</div>';
// ...event listeners from Lesson 2, plus:
toggle.addEventListener( 'click', function () {
var isOpen = ! panel.hidden;
toggle.setAttribute( 'aria-expanded', isOpen ? 'true' : 'false' );
} );
}
var mount = document.getElementById( 'site-chat-widget-mount' );
if ( mount ) {
createWidget( mount );
}
aria-expanded on the toggle button and role="dialog" with a real aria-label on the
panel give assistive technology an accurate picture of what’s open and what it’s called,
on top of the live region handling new messages.
Test it: a full keyboard and screen-reader pass
Recap
The widget ships as a shortcode, [site_chat_widget], so a site owner decides exactly
where it appears rather than it loading unconditionally on every page, and its assets
only enqueue on pages that actually use it. Keyboard support, Escape to close and focus
returning to the toggle, and an ARIA live region on the message list so replies are
actually announced, are what make this a widget any visitor can use, not just one who’s
comfortable with a mouse and a screen. That closes out the course: from the
authentication model in Lesson 1 through a shipped, embeddable, accessible chat
experience here.
Resources & further reading
- Shortcode API reference, developer.wordpress.org
- ARIA live regions, MDN, MDN
- WAI-ARIA Authoring Practices: dialog pattern, W3C