Polish

Styling and Embedding the Widget Site-Wide

⏱ 15 min

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.

What you'll learn in this lesson
Wrapping the widget in a shortcode
[site_chat_widget], so a site owner controls exactly where it appears.
Loading assets only where the shortcode is used
Instead of enqueuing on every single page load, regardless of whether the widget is present.
Keyboard navigation
Opening, closing, and sending a message without touching a mouse.
ARIA live regions for new messages
So a screen reader announces the assistant's reply as it arrives, not just silently updates the DOM.
Prerequisites

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.

A block wrapper works the same way

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>
A live region only works if the container already exists in the DOM

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

Manual accessibility check
Tab to the toggle button with no mouse
It should receive visible focus and open on Enter or Space.
Press Escape while the panel is open
It should close and return focus to the toggle button, not leave focus stranded.
Send a message with a screen reader running
The reply should be announced automatically, not require manually navigating to find it.
Confirm the shortcode only loads assets where it's placed
Check the network tab on a page without the shortcode, chat-widget.js should not load at all.

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

← Human Escalation Patterns Finish ✓