15 Useful WordPress JavaScript Snippets
JavaScript can add useful functionality to a WordPress website without requiring a substantial plugin or changes to PHP templates. Features such as reading progress indicators, searchable resource lists, copy-to-clipboard buttons and interactive content controls can often be implemented with a relatively small amount of browser-side code.
Unlike PHP, which executes on the server, JavaScript runs in the visitor’s browser. It can respond to clicks, update page elements, inspect rendered content and modify the interface without reloading the page. It cannot directly access WordPress database functions or protected server-side information.
The practical difficulty with WordPress JavaScript is usually integration rather than the JavaScript itself. Themes, page builders and plugins generate different HTML structures, scripts may execute before their target elements exist, and optimisation plugins can change script loading behaviour.
The following examples use standard browser APIs and avoid jQuery or external JavaScript libraries. They are suitable for conventional WordPress pages, provided the required HTML elements and classes are present. Each snippet is independent and can be adapted to an existing theme or page builder.
Adding JavaScript to WordPress
WordPress does not provide a universal equivalent of the Additional CSS editor for arbitrary JavaScript. The appropriate installation method depends on whether the script is a small site-specific customisation or part of a reusable plugin.
For a few small customisations, a code snippets plugin that explicitly supports JavaScript may be convenient. Such plugins vary in how they inject code, where scripts execute and whether they automatically include HTML <script> elements.
For a more maintainable arrangement, JavaScript can be stored in a separate file and loaded through WordPress’s script-enqueueing system.
A small functionality plugin can register the file using wp_enqueue_script():
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_script(
'site-custom-javascript',
plugin_dir_url( __FILE__ ) . 'assets/js/site-custom.js',
array(),
'1.0.0',
array(
'strategy' => 'defer',
'in_footer' => true,
)
);
} );
This example assumes the PHP code is placed in the main file of a custom plugin and that the JavaScript file exists at assets/js/site-custom.js relative to that plugin file.
The final argument uses the modern script-loading options supported by WordPress. The requested deferred loading strategy allows the browser to download the script without blocking HTML parsing, subject to WordPress’s dependency and loading-strategy handling.
A child theme can also enqueue JavaScript, but the path should be constructed using the appropriate child-theme directory function rather than plugin_dir_url().
For the examples below, the JavaScript should normally be placed in a file or JavaScript-specific code editor without surrounding <script> tags, unless the insertion method explicitly requires them.
Most examples use DOMContentLoaded or a check for document readiness to ensure their target elements have been created before the code runs.
Understanding the Selectors
Several examples use custom class names such as .js-copy-code and .js-filter-input.
These are not WordPress core classes. They identify elements that the JavaScript should control.
The js- prefix is a naming convention that helps distinguish JavaScript hooks from classes used purely for styling.
When using the block editor, custom classes can often be assigned through the Advanced settings of a block. Page builders may provide a separate CSS class field.
Where the examples require particular HTML structure, the necessary markup is provided. The CSS classes can be renamed, provided the JavaScript and markup remain consistent.
Navigation and Reading Enhancements
1. Add a Back-to-Top Button
A back-to-top button is useful on long tutorials, documentation pages and reference articles.
The following script creates its own button, so no additional HTML is required.
document.addEventListener('DOMContentLoaded', function () {
const button = document.createElement('button');
button.type = 'button';
button.textContent = 'Back to top';
button.className = 'js-back-to-top';
button.hidden = true;
document.body.appendChild(button);
function updateVisibility() {
button.hidden = window.scrollY < 400;
}
button.addEventListener('click', function () {
const reducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
window.scrollTo({
top: 0,
behavior: reducedMotion ? 'instant' : 'smooth'
});
});
window.addEventListener('scroll', updateVisibility, {
passive: true
});
updateVisibility();
});
The button remains hidden until the visitor has scrolled more than 400 pixels.
The scrollY property supplies the vertical scroll position. The scrollTo() method returns the viewport to the top of the document.
The reduced-motion check avoids smooth scrolling when the visitor has expressed a preference for less animation.
The script creates a functional button, but its final appearance depends on CSS. The following styling fixes it near the bottom-right corner of the viewport.
.js-back-to-top {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 1000;
padding: 12px 16px;
border: 0;
border-radius: 6px;
background: #1d4ed8;
color: #ffffff;
cursor: pointer;
}
.js-back-to-top[hidden] {
display: none;
}
.js-back-to-top:focus-visible {
outline: 3px solid #f59e0b;
outline-offset: 3px;
}
The explicit [hidden] rule prevents the custom display styling from accidentally overriding the button’s hidden state.
On websites with a fixed mobile footer, cookie notice or floating support widget, adjust the position to prevent overlapping controls.
2. Display Article Reading Progress
A reading progress indicator shows how far a visitor has progressed through a long article.
The implementation below measures progress through a selected article container rather than the entire document.
Add a class named js-reading-article to the article’s main content container.
document.addEventListener('DOMContentLoaded', function () {
const article = document.querySelector(
'.js-reading-article'
);
if (!article) {
return;
}
const progress = document.createElement('progress');
progress.className = 'js-reading-progress';
progress.max = 100;
progress.value = 0;
progress.setAttribute(
'aria-label',
'Article reading progress'
);
document.body.appendChild(progress);
function updateProgress() {
const rect = article.getBoundingClientRect();
const articleHeight = article.offsetHeight;
const viewportHeight = window.innerHeight;
const travelDistance = Math.max(
1,
articleHeight - viewportHeight
);
const distanceRead = -rect.top;
const percentage = Math.min(
100,
Math.max(
0,
(distanceRead / travelDistance) * 100
)
);
progress.value = percentage;
}
window.addEventListener('scroll', updateProgress, {
passive: true
});
window.addEventListener('resize', updateProgress);
updateProgress();
});
The script creates a native HTML progress element and updates its value as the visitor scrolls.
For articles shorter than the viewport, the result is less meaningful because there is little or no scrollable reading distance. Such pages generally do not need a progress indicator.
Add the following CSS:
.js-reading-progress {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 5px;
z-index: 9999;
appearance: none;
border: 0;
border-radius: 0;
}
.js-reading-progress::-webkit-progress-bar {
background: transparent;
}
.js-reading-progress::-webkit-progress-value {
background: #2563eb;
}
.js-reading-progress::-moz-progress-bar {
background: #2563eb;
}
A native progress element exposes its value through browser accessibility APIs.
The calculation assumes that the selected article container represents the principal reading area. For pages with dynamically expanding sections, sticky elements or substantial content changes after loading, progress measurements may need additional handling.
3. Generate a Table of Contents Automatically
A table of contents is useful for lengthy WordPress tutorials containing several major sections.
This example builds a list of H2 and H3 headings found inside an article and inserts links into a dedicated container.
The page requires an article wrapper with the class js-toc-content and an empty container with the class js-table-of-contents.
A Custom HTML block can supply the table-of-contents container:
<nav class="js-table-of-contents"
aria-label="Article contents"></nav>
The JavaScript is:
document.addEventListener('DOMContentLoaded', function () {
const article = document.querySelector(
'.js-toc-content'
);
const container = document.querySelector(
'.js-table-of-contents'
);
if (!article || !container) {
return;
}
const headings = article.querySelectorAll('h2, h3');
if (headings.length === 0) {
return;
}
const list = document.createElement('ul');
headings.forEach(function (heading, index) {
if (!heading.id) {
let identifier = 'article-heading-' + (index + 1);
let suffix = 2;
while (document.getElementById(identifier)) {
identifier =
'article-heading-' +
(index + 1) +
'-' +
suffix;
suffix++;
}
heading.id = identifier;
}
const item = document.createElement('li');
const link = document.createElement('a');
link.href = '#' + encodeURIComponent(heading.id);
link.textContent = heading.textContent.trim();
if (heading.tagName === 'H3') {
item.classList.add('toc-subheading');
}
item.appendChild(link);
list.appendChild(item);
});
container.replaceChildren(list);
});
The script preserves heading IDs that already exist. When a heading lacks an ID, it generates a new unique identifier.
Using generated IDs instead of heading text avoids problems caused by duplicate titles, punctuation and special characters.
The resulting links navigate to the corresponding heading using normal browser fragment navigation.
The script does not generate nested lists for H3 headings. Instead, it assigns a class that can be styled separately:
.js-table-of-contents ul {
list-style: none;
padding-left: 0;
}
.js-table-of-contents li {
margin-bottom: 8px;
}
.js-table-of-contents .toc-subheading {
padding-left: 20px;
}
.js-table-of-contents a {
text-decoration: none;
}
.js-toc-content h2,
.js-toc-content h3 {
scroll-margin-top: 90px;
}
The scroll-margin-top value helps prevent headings from being hidden beneath a fixed site header.
This implementation is intended for static article headings present when the page finishes loading. Dynamically inserted headings would require the contents list to be rebuilt.
4. Add Copy Buttons to Code Blocks
A copy-to-clipboard button is particularly useful on a website publishing PHP, CSS, JavaScript and WP-CLI examples.
The following script looks for pre elements containing code elements, then adds a button to each one.
Assign the class js-copy-code to the main article container.
document.addEventListener('DOMContentLoaded', function () {
const codeBlocks = document.querySelectorAll(
'.js-copy-code pre'
);
codeBlocks.forEach(function (block) {
const code = block.querySelector('code');
if (!code) {
return;
}
const button = document.createElement('button');
button.type = 'button';
button.className = 'js-copy-button';
button.textContent = 'Copy code';
button.addEventListener('click', async function () {
if (
!window.isSecureContext ||
!navigator.clipboard ||
!navigator.clipboard.writeText
) {
button.textContent = 'Copy unavailable';
return;
}
try {
await navigator.clipboard.writeText(
code.textContent
);
button.textContent = 'Copied';
window.setTimeout(function () {
button.textContent = 'Copy code';
}, 2000);
} catch (error) {
button.textContent = 'Copy failed';
}
});
block.classList.add('js-copy-container');
block.appendChild(button);
});
});
The Clipboard API generally requires a secure context, normally an HTTPS website, and permission or user activation according to browser behaviour.
The script checks that the API is available before attempting the operation.
The copied text comes from textContent, preserving the textual code rather than copying syntax-highlighting HTML.
Add CSS to position the button:
.js-copy-container {
position: relative;
}
.js-copy-button {
position: absolute;
top: 8px;
right: 8px;
padding: 6px 10px;
border: 1px solid #64748b;
border-radius: 4px;
background: #ffffff;
color: #111827;
cursor: pointer;
}
This implementation assumes that adding a button directly to the pre element is compatible with the active code-block styling.
Syntax-highlighting plugins sometimes generate additional wrappers or interactive controls. Those implementations may require a different insertion point to avoid duplicate copy buttons or layout conflicts.
For code blocks containing line numbers or extra text injected into the code element, inspect what textContent actually returns.
5. Create Smooth Internal Anchor Navigation
Internal anchor links are useful for navigating long WordPress articles and reference pages.
This script applies smooth scrolling to same-page fragment links while respecting reduced-motion preferences.
document.addEventListener('DOMContentLoaded', function () {
document.addEventListener('click', function (event) {
const link = event.target.closest(
'a[href*="#"]'
);
if (!link) {
return;
}
const url = new URL(link.href, window.location.href);
if (
url.origin !== window.location.origin ||
url.pathname !== window.location.pathname ||
url.search !== window.location.search ||
!url.hash ||
url.hash === '#'
) {
return;
}
let targetId;
try {
targetId = decodeURIComponent(
url.hash.substring(1)
);
} catch (error) {
return;
}
const target = document.getElementById(targetId);
if (!target) {
return;
}
event.preventDefault();
const reducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
target.scrollIntoView({
behavior: reducedMotion ? 'instant' : 'smooth',
block: 'start'
});
history.pushState(
null,
'',
url.hash
);
});
});
The script intercepts only links that refer to a fragment on the current page.
It avoids interfering with links to other pages or websites.
The scrollIntoView() method moves the requested element into view. The URL fragment is updated using the History API.
A fixed header may overlap the target element. Apply scroll-margin-top to appropriate heading elements to compensate.
This is a lightweight enhancement, not a complete replacement for native anchor navigation. Because the example overrides the browser’s normal fragment handling, it does not automatically move keyboard focus to the target. Websites requiring full accessible focus management should account for that behaviour or retain native anchor navigation with CSS scroll-behavior.
Interactive Content and Visitor Controls
6. Create a Dismissible Notice with Local Storage
A dismissible notice can display information about a temporary website change, a new resource or an important announcement.
The visitor can close the notice, and the browser remembers that preference between visits.
Add the following markup using a Custom HTML block:
<div class="js-dismissible-notice">
<p>A new collection of WordPress resources is available.</p>
<button type="button" class="js-dismiss-notice">
Dismiss
</button>
</div>
The JavaScript is:
document.addEventListener('DOMContentLoaded', function () {
const notice = document.querySelector(
'.js-dismissible-notice'
);
if (!notice) {
return;
}
const button = notice.querySelector(
'.js-dismiss-notice'
);
if (!button) {
return;
}
const storageKey = 'site-notice-v1-dismissed';
let dismissed = false;
try {
dismissed = localStorage.getItem(storageKey) === 'yes';
} catch (error) {
// Storage may be unavailable.
}
if (dismissed) {
notice.hidden = true;
return;
}
button.addEventListener('click', function () {
notice.hidden = true;
try {
localStorage.setItem(storageKey, 'yes');
} catch (error) {
// The notice still closes for this page.
}
});
});
The localStorage API stores a small piece of data in the visitor’s browser, associated with the website’s origin.
The key site-notice-v1-dismissed identifies the specific notice. Changing the key for a new announcement allows previously dismissed notices to be replaced without requiring visitors to clear their browser storage.
The try...catch blocks handle cases where browser storage is unavailable or access is restricted.
The notice still closes during the current page visit even if the preference cannot be saved.
This implementation stores only a dismissal preference. It should not be used as a consent-management mechanism without implementing the additional controls and records required for that purpose.
A short styling example is:
.js-dismissible-notice {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 16px;
border: 1px solid #93c5fd;
background: #eff6ff;
border-radius: 6px;
}
.js-dismissible-notice[hidden] {
display: none;
}
7. Add Live Search to a Resource List
A live search field can filter a collection of WordPress resources without submitting a form or reloading the page.
This is useful for cheat sheet libraries, code snippet collections, FAQs and small directories.
The example requires a search input and a group of items.
<div class="resource-filter">
<label for="resource-search">Search resources</label>
<input
type="search"
id="resource-search"
class="js-filter-input"
placeholder="Enter a keyword"
>
<p class="js-filter-count" aria-live="polite"></p>
<ul class="js-filter-list">
<li>WordPress PHP Functions</li>
<li>WordPress CSS Snippets</li>
<li>WP-CLI Commands</li>
<li>WordPress Security Checklist</li>
<li>WordPress Troubleshooting Guide</li>
</ul>
</div>
The JavaScript is:
document.addEventListener('DOMContentLoaded', function () {
const input = document.querySelector(
'.js-filter-input'
);
const list = document.querySelector(
'.js-filter-list'
);
const count = document.querySelector(
'.js-filter-count'
);
if (!input || !list) {
return;
}
const items = Array.from(
list.children
);
function filterItems() {
const query = input.value
.trim()
.toLocaleLowerCase();
let visible = 0;
items.forEach(function (item) {
const text = item.textContent
.toLocaleLowerCase();
const matches = text.includes(query);
item.hidden = !matches;
if (matches) {
visible++;
}
});
if (count) {
count.textContent =
visible + ' of ' +
items.length +
' resources shown';
}
}
input.addEventListener('input', filterItems);
filterItems();
});
The input event executes whenever the search value changes.
The script compares the entered text with each item’s text content using a case-insensitive substring search.
Items that do not match are hidden using the native hidden property.
The optional result counter reports how many entries remain visible. Its aria-live attribute allows assistive technologies to announce updates.
This is a client-side filter rather than a WordPress database search. All resources must already be present in the page markup.
For directories containing hundreds or thousands of entries, server-side filtering, pagination or a dedicated search API may be more appropriate.
The script assumes that the immediate children of .js-filter-list are the items to filter.
8. Display a Live Character Counter
A character counter is useful for forms where the visitor must enter text within a specified limit.
Possible applications include custom title fields, short descriptions, feedback forms and user-submitted resource descriptions.
Add the following markup:
<div class="character-count-field">
<label for="short-description">
Short description
</label>
<textarea
id="short-description"
class="js-character-input"
maxlength="160"
rows="4"
></textarea>
<p class="js-character-count" aria-live="polite"></p>
</div>
The JavaScript is:
document.addEventListener('DOMContentLoaded', function () {
const field = document.querySelector(
'.js-character-input'
);
const counter = document.querySelector(
'.js-character-count'
);
if (!field || !counter) {
return;
}
const maximum = Number(
field.getAttribute('maxlength')
);
function updateCounter() {
const used = field.value.length;
const remaining = maximum - used;
counter.textContent =
used + ' characters used, ' +
remaining + ' remaining';
}
field.addEventListener('input', updateCounter);
updateCounter();
});
The example uses the HTML maxlength attribute to enforce the text input limit in normal browser interaction.
The JavaScript provides immediate feedback about how much space remains.
The length property counts JavaScript UTF-16 code units, which generally matches the measurement used by HTML text-field length constraints. Some visible Unicode characters, such as certain emoji, consist of multiple code units.
The counter should not be treated as server-side validation. If the form submits data to WordPress, the server must still validate its length and other requirements.
The result can be styled independently:
.js-character-count {
margin-top: 6px;
font-size: 0.9rem;
color: #475569;
}
9. Add a Show/Hide Password Button
Password visibility controls are common in login, registration and account-management forms.
The following example toggles an ordinary password field between concealed and visible text.
<div class="password-field">
<label for="account-password">
Password
</label>
<input
type="password"
id="account-password"
class="js-password-field"
autocomplete="current-password"
>
<button
type="button"
class="js-password-toggle"
aria-controls="account-password"
aria-pressed="false"
>
Show password
</button>
</div>
JavaScript:
document.addEventListener('DOMContentLoaded', function () {
const field = document.querySelector(
'.js-password-field'
);
const button = document.querySelector(
'.js-password-toggle'
);
if (!field || !button) {
return;
}
button.addEventListener('click', function () {
const isHidden = field.type === 'password';
field.type = isHidden ? 'text' : 'password';
button.textContent = isHidden
? 'Hide password'
: 'Show password';
button.setAttribute(
'aria-pressed',
String(isHidden)
);
});
});
The script changes the input field’s type property without modifying the password value.
The aria-pressed attribute indicates the toggle’s current state.
This does not alter how the password is submitted or stored. It only controls whether the characters are visible on screen.
WordPress and some authentication plugins already provide password visibility controls. Avoid inserting a second button where equivalent functionality is present.
On a custom WordPress form, ensure that authentication, password handling, transport security and server-side validation are implemented separately.
10. Add a Copy Page Link Button
A copy-link button can help visitors share individual reference articles, cheat sheets or product pages.
The example copies the current page URL to the clipboard.
Add a button:
<button type="button" class="js-copy-page-link">
Copy page link
</button>
JavaScript:
document.addEventListener('DOMContentLoaded', function () {
const button = document.querySelector(
'.js-copy-page-link'
);
if (!button) {
return;
}
const originalText = button.textContent;
button.addEventListener('click', async function () {
if (
!window.isSecureContext ||
!navigator.clipboard ||
!navigator.clipboard.writeText
) {
button.textContent = 'Copy unavailable';
return;
}
try {
await navigator.clipboard.writeText(
window.location.href
);
button.textContent = 'Link copied';
window.setTimeout(function () {
button.textContent = originalText;
}, 2000);
} catch (error) {
button.textContent = 'Unable to copy';
}
});
});
The script uses window.location.href to obtain the current page URL.
This includes any query string and URL fragment. If the website uses temporary tracking parameters, the copied URL may contain those parameters as well.
A site that requires clean sharing URLs could instead use the page’s canonical URL, provided that value is available and trustworthy.
The Clipboard API requires an appropriate browser security context and may fail if clipboard access is restricted.
As with the code-copy example, the visitor must actively click the button before the copying operation is attempted.
Forms, Print Controls and Dynamic Interfaces
11. Add a Print Article Button
Technical articles and reference pages are sometimes easier to use as printed documents, particularly when they contain configuration procedures, checklists or troubleshooting tables.
The following snippet provides a simple print button.
<button type="button" class="js-print-article">
Print this article
</button>
JavaScript:
document.addEventListener('DOMContentLoaded', function () {
const button = document.querySelector(
'.js-print-article'
);
if (!button) {
return;
}
button.addEventListener('click', function () {
window.print();
});
});
The window.print() method requests the browser’s printing interface.
The JavaScript itself does not determine which sections of the website will be printed. That is controlled by the page’s print stylesheet and the browser’s printing behaviour.
To remove common website interface elements from the printed page, use a print-specific media query.
@media print {
.site-header,
.site-footer,
.site-sidebar,
.js-print-article,
.js-back-to-top {
display: none !important;
}
body {
color: #000000;
background: #ffffff;
}
.article-content {
width: 100%;
max-width: none;
margin: 0;
padding: 0;
}
pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
}
The selectors for the header, footer and sidebar are examples. They must match the actual classes used by the active theme.
The pre rule allows long lines of code to wrap on printed pages. This avoids horizontal clipping, although line wrapping may make copied programming examples less convenient to read.
Review the result using the browser’s print preview before applying print rules across the entire website.
12. Add Expand All and Collapse All Controls to FAQs
The native HTML details element provides expandable content without requiring JavaScript.
JavaScript becomes useful when several sections need to be opened or closed together.
This example adds two controls to a collection of FAQ entries.
<div class="js-faq-container">
<div class="faq-controls">
<button type="button" class="js-expand-all">
Expand all
</button>
<button type="button" class="js-collapse-all">
Collapse all
</button>
</div>
<details>
<summary>Where should custom PHP code be placed?</summary>
<p>
A custom functionality plugin is suitable for
site-specific PHP functionality.
</p>
</details>
<details>
<summary>Can JavaScript run inside WordPress?</summary>
<p>
JavaScript can be loaded through an enqueued file
or a suitable code management plugin.
</p>
</details>
</div>
JavaScript:
document.addEventListener('DOMContentLoaded', function () {
const container = document.querySelector(
'.js-faq-container'
);
if (!container) {
return;
}
const expandButton = container.querySelector(
'.js-expand-all'
);
const collapseButton = container.querySelector(
'.js-collapse-all'
);
const sections = container.querySelectorAll(
'details'
);
if (!expandButton || !collapseButton) {
return;
}
expandButton.addEventListener('click', function () {
sections.forEach(function (section) {
section.open = true;
});
});
collapseButton.addEventListener('click', function () {
sections.forEach(function (section) {
section.open = false;
});
});
});
The open property controls whether a native details element is expanded.
The script changes every details element inside the selected container, but leaves other expandable sections on the page unchanged.
Individual FAQ entries remain usable without JavaScript because their native expand-and-collapse behaviour is supplied by the browser.
This is particularly useful for troubleshooting references, installation instructions and product documentation containing many expandable answers.
For larger FAQ collections, combine the approach with a search filter or divide the content into separate subject areas.
13. Highlight the Current Table of Contents Section
A table of contents becomes more useful when it indicates which section the visitor is currently reading.
The IntersectionObserver API can monitor heading positions without running a full element-position calculation on every scroll event.
This example is designed to work with the table of contents created in snippet 3.
document.addEventListener('DOMContentLoaded', function () {
const article = document.querySelector(
'.js-toc-content'
);
const toc = document.querySelector(
'.js-table-of-contents'
);
if (!article || !toc) {
return;
}
const headings = Array.from(
article.querySelectorAll('h2[id], h3[id]')
);
function setActiveHeading(heading) {
const links = toc.querySelectorAll('a[href^="#"]');
links.forEach(function (link) {
link.classList.remove('is-active');
link.removeAttribute('aria-current');
});
if (!heading) {
return;
}
links.forEach(function (link) {
let targetId;
try {
targetId = decodeURIComponent(
link.hash.substring(1)
);
} catch (error) {
return;
}
if (targetId === heading.id) {
link.classList.add('is-active');
link.setAttribute('aria-current', 'location');
}
});
}
function updateActiveHeading() {
const threshold = 140;
let active = headings[0] || null;
headings.forEach(function (heading) {
if (
heading.getBoundingClientRect().top <= threshold
) {
active = heading;
}
});
setActiveHeading(active);
}
const observer = new IntersectionObserver(
updateActiveHeading,
{
rootMargin: '-100px 0px -70% 0px'
}
);
headings.forEach(function (heading) {
observer.observe(heading);
});
window.addEventListener('scroll', updateActiveHeading, {
passive: true
});
window.addEventListener('resize', updateActiveHeading);
updateActiveHeading();
});
The script determines which heading is most recently positioned above a threshold near the top of the viewport.
It applies the is-active class to the corresponding table-of-contents link and adds aria-current="location".
The observer triggers updates as headings enter or leave its configured observation region. The scroll listener also handles positions between those transitions.
For a small number of headings, the calculation is inexpensive. Pages with hundreds of headings may benefit from a more optimised implementation that avoids examining every heading on each scroll event.
The example assumes the table of contents has already been populated. When combining this code with snippet 3, place it after the table-of-contents generation logic or merge both features into one initialisation function.
CSS can distinguish the active link:
.js-table-of-contents a.is-active {
color: #1d4ed8;
font-weight: 700;
text-decoration: underline;
}
The active-section behaviour depends on heading positions and the selected threshold. Fixed headers, dynamically loaded content and unusually tall sections may require adjustments.
14. Create a Show More / Show Less Content Control
Long resource descriptions or explanatory sections can be collapsed until the visitor chooses to read more.
This example uses the native hidden attribute rather than calculating content height or applying CSS transitions.
Add the following markup:
<div class="js-expandable-content">
<p>
This introduction remains visible.
</p>
<div class="js-extra-content" hidden>
<p>
This additional information is initially hidden.
</p>
<p>
It becomes visible when the button is selected.
</p>
</div>
<button
type="button"
class="js-show-more"
aria-expanded="false"
>
Show more
</button>
</div>
JavaScript:
document.addEventListener('DOMContentLoaded', function () {
const containers = document.querySelectorAll(
'.js-expandable-content'
);
containers.forEach(function (container) {
const extraContent = container.querySelector(
'.js-extra-content'
);
const button = container.querySelector(
'.js-show-more'
);
if (!extraContent || !button) {
return;
}
if (!extraContent.id) {
extraContent.id =
'expandable-' +
Math.random().toString(36).slice(2);
}
button.setAttribute(
'aria-controls',
extraContent.id
);
button.addEventListener('click', function () {
const opening = extraContent.hidden;
extraContent.hidden = !opening;
button.textContent = opening
? 'Show less'
: 'Show more';
button.setAttribute(
'aria-expanded',
String(opening)
);
});
});
});
The script supports multiple expandable sections on the same page.
Each button controls only the additional content within its own container.
The aria-expanded attribute reports whether the controlled content is expanded. The aria-controls attribute identifies the associated content element.
The generated ID is used only to connect the button and its content. For permanent template components, assigning stable IDs in the markup is generally preferable.
This technique is suitable for optional explanations, extended product information and supporting material.
Avoid using it to conceal essential instructions, warnings or accessibility information.
Where content must remain available without JavaScript, consider using native details and summary elements instead, as demonstrated in snippet 12.
15. Warn Before Leaving a Form with Unsaved Changes
Custom WordPress forms can contain information that takes time to enter. Accidentally closing the page or navigating away before submitting the form may cause that information to be lost.
The beforeunload event allows a browser to display a generic confirmation when the visitor attempts to leave a page with unsaved changes.
This example targets a custom form rather than modifying WordPress’s built-in editors.
<form class="js-unsaved-form" method="post">
<label for="resource-title">
Resource title
</label>
<input
type="text"
id="resource-title"
name="resource_title"
>
<label for="resource-description">
Description
</label>
<textarea
id="resource-description"
name="resource_description"
></textarea>
<button type="submit">
Save resource
</button>
</form>
JavaScript:
document.addEventListener('DOMContentLoaded', function () {
const form = document.querySelector(
'.js-unsaved-form'
);
if (!form) {
return;
}
let isDirty = false;
let isSubmitting = false;
function markChanged() {
isDirty = true;
}
form.addEventListener('input', markChanged);
form.addEventListener('change', markChanged);
form.addEventListener('submit', function () {
isSubmitting = true;
});
window.addEventListener('beforeunload', function (event) {
if (!isDirty || isSubmitting) {
return;
}
event.preventDefault();
event.returnValue = true;
});
});
The input and change events mark the form as modified.
The beforeunload handler requests a browser confirmation only when the form has changed and submission has not started.
Modern browsers control the wording of the confirmation message. Custom warning text generally cannot be supplied.
Some browsers require prior user interaction before showing a warning, and support varies across browsers and mobile environments. The event is not a reliable mechanism for automatically saving data.
This example assumes that ordinary form submission navigates away from the page. A form using asynchronous submission would need to clear the dirty state only after the server confirms that the data was saved successfully.
WordPress’s block editor and many form plugins already provide unsaved-change handling. Do not install an additional handler without checking whether it would produce duplicate warnings.
Security note: The example provides only the browser-side unsaved-change warning. It does not implement WordPress form processing, nonce verification, capability checks or server-side input validation.
Choosing the Appropriate JavaScript Enhancement
Not every website needs all 15 features. Some functions may already exist in the theme, page builder or installed plugins.
| Snippet | Primary purpose | Main consideration |
|---|---|---|
| 1. Back-to-top button | Long-page navigation | Mobile positioning |
| 2. Reading progress | Article navigation feedback | Article container dimensions |
| 3. Table of contents | Heading navigation | Accurate article selectors |
| 4. Copy code buttons | Technical code examples | Clipboard API and code markup |
| 5. Smooth anchors | Internal navigation | Focus and motion preferences |
| 6. Dismissible notice | Temporary announcements | Browser storage availability |
| 7. Live resource filter | Resource discovery | Client-side list size |
| 8. Character counter | Form feedback | Server-side validation |
| 9. Password visibility | Form usability | Existing authentication controls |
| 10. Copy page link | Sharing articles | Clipboard permissions |
| 11. Print button | Printable references | Print stylesheet |
| 12. Expand/collapse FAQ | Content organisation | Native details behaviour |
| 13. Active contents link | Article navigation | Heading positions |
| 14. Show more/less | Optional content | Accessibility and hidden content |
| 15. Unsaved form warning | Prevent accidental data loss | Browser restrictions |
For a WordPress reference website containing long technical articles, the most immediately useful features are likely to be a table of contents, copy-code buttons, reading progress, responsive content controls and resource filtering.
These features directly improve the usability of reference material rather than adding purely decorative effects.
Common JavaScript Problems in WordPress
JavaScript errors often arise because a script makes assumptions about the surrounding HTML or the point at which it executes.
A standalone JavaScript example may work in a simple HTML page but fail when inserted into a WordPress website using a page builder, delayed script execution or dynamically generated content.
| Symptom | Possible cause | Specific check |
|---|---|---|
| Script does nothing | Target selector not found | Inspect rendered HTML and class names |
| JavaScript error on page load | Missing element or incorrect execution order | Check browser console |
| Button does not respond | Event handler not attached | Inspect element and event listeners |
| Feature works on one page only | Script loaded conditionally or markup differs | Check enqueue rules and page structure |
| Clipboard operation fails | Insecure context or browser restriction | Confirm HTTPS and Clipboard API availability |
| Table of contents is empty | Headings not present when script runs | Inspect heading markup and script timing |
| CSS does not affect generated elements | Selector mismatch or cascade conflict | Inspect computed styles |
| Feature appears twice | Theme or plugin already provides it | Check existing scripts and generated markup |
| Script fails after optimisation | Deferred or delayed execution changes | Inspect the final script loading order |
| Filtering hides wrong elements | Selector targets the wrong container | Check direct children and list structure |
| JavaScript conflicts with a plugin | Shared selectors or duplicate controls | Isolate the conflicting functionality |
| Form warning does not appear | Browser restrictions or no user interaction | Test supported browsers and event conditions |
| Dynamic content is ignored | Elements added after initialisation | Reinitialise or use event delegation |
| Performance deteriorates while scrolling | Excessive event processing | Reduce work or throttle updates |
| Mobile behaviour differs | Viewport or browser API differences | Test on actual mobile devices |
Using the Browser Console
The browser developer console is the first place to inspect when a JavaScript feature does not operate correctly.
A typical error such as:
Uncaught TypeError: Cannot read properties of null
often indicates that the script attempted to access a property or method on an element that was not found.
For example:
const button = document.querySelector('.missing-button');
button.addEventListener('click', function () {
console.log('Clicked');
});
If no element matches .missing-button, querySelector() returns null. Calling addEventListener() on that value produces an error.
The safer pattern is:
const button = document.querySelector('.missing-button');
if (button) {
button.addEventListener('click', function () {
console.log('Clicked');
});
}
Most snippets in this collection use this approach to avoid errors on pages where the relevant component is absent.
A script that is loaded throughout the website can therefore remain inactive on pages that do not contain its target elements.
JavaScript Loading and Performance
Loading a small JavaScript file through WordPress’s enqueueing system is generally preferable to duplicating the same script inside every article.
A shared file can be cached by the browser and updated through a versioned URL when necessary.
When several scripts are used, consider whether they should be loaded globally or only on pages that need them.
For example, code-copy functionality may be useful across a technical blog, while a specialised resource-filter script may be needed only on a resource directory page.
Avoid adding several libraries to implement features already available through native browser APIs.
Event Listeners
Some JavaScript events occur far more frequently than others.
Scroll and resize events can execute repeatedly while the visitor interacts with the page.
A lightweight handler that checks the current scroll position is unlikely to cause significant problems on an ordinary page. A handler that repeatedly performs expensive DOM queries, layout measurements or extensive calculations can create visible delays.
The passive: true option indicates that an event handler will not cancel the default behaviour of the relevant event. It is appropriate for scroll handlers that only observe the current position.
For more demanding interfaces, consider requestAnimationFrame(), throttling or IntersectionObserver to reduce unnecessary work.
Cached and Delayed Scripts
Caching and optimisation plugins may combine, defer or delay JavaScript.
These changes can affect the order in which scripts execute, particularly when a feature depends on another library or on content generated by a page builder.
A dependency should be declared when a script is enqueued rather than relying on an assumed loading order.
For the independent examples in this article, no third-party JavaScript dependencies are required.
When testing an implementation, check both the normal website and the version served with optimisation enabled.
Accessibility
JavaScript enhancements should preserve keyboard operation, visible focus indicators and appropriate semantic HTML.
Native HTML controls are preferable to generic clickable containers where possible.
A button should generally be implemented with a button element rather than attaching a click handler to an ordinary div.
For expandable content, use aria-expanded where appropriate. For dynamic status messages, consider whether an aria-live region is useful. Do not add accessibility attributes without understanding their expected relationship to the controlled elements.
Native HTML functionality often eliminates the need for additional JavaScript. The details element, for example, can expand and collapse independently, while JavaScript can add optional controls for operating several sections together.
Maintaining Custom JavaScript
Custom JavaScript should be treated as part of the website’s maintained codebase.
Store reusable functionality in clearly named files rather than scattering large scripts throughout individual WordPress posts.
Use distinctive CSS classes for JavaScript targets and avoid relying on theme-generated class names that may change after updates.
When several snippets operate on the same element, check whether they modify compatible properties. Two scripts that both replace an element’s contents or manage its visibility may interfere with one another.
For a growing WordPress resource website, it is sensible to keep common features together and specialised features separate. Navigation enhancements, code-copy controls and resource filtering can remain independent, making them easier to update or disable.
Before removing a script, check whether any published articles, shortcodes, templates or resource pages still rely on its classes or behaviour.
Test customisations after major WordPress, theme and page-builder updates, particularly when those updates change the generated HTML or script-loading process.
