- Vanilla JS installs
- Custom parameters
- Theming
- Useful JS events
- Modifying tips
- Writing custom tests
- Dealing with alerts on hidden or size-constrained content
Vanilla JS installs
To build your own implementation, download a local copy (or reference a CDN version) of a Language pack, the JS min file, and the CSS min file, and then create a new “Ed11y” instance.
For legacy (UMD) JS (local development and legacy browser systems):
// CSS
<link href="/YOUR_COPY_OR_CDN/dist/css/editoria11y.min.css" rel="stylesheet">
// JS
<script src="/YOUR_COPY_OR_CDN/dist/js/ed11y.umd.js"></script>
// Language
<script src="/YOUR_COPY_OR_CDN/dist/js/lang/en.umd.js"></script>
//
<script>
const ed11y = new Ed11y.Ed11y(
// options would go here
});
</script>
Note that Editoria11y will be looking for that CSS file link tag to copy it into the various shadow DOM components. If you aggregate CSS in your implementation, you MUST provide a URL to an un-aggregated copy, by adding the URL to the cssUrls array:
{
// In options
cssUrls: ["/PATH/TO/YOUR/COPY/editoria11y.min.css"]
}
For ESM/JS module imports, you will want to import the JS files as modules, using whatever method fits your workflow. For the Drupal module, we do a dynamic import at runtime based on the page’s content language and the module version tag.
// CSS still called from HTML header as usual, assuming current version is 3.0.123:
//<link href="/YOUR_COPY_OR_CDN/dist/css/editoria11y.min.css?v=3.0.123" rel="stylesheet">
// Load dynamically with version string.
const modulePromises = [
import(`../library/dist/js/ed11y.esm.min.js?v=${version}`),
import(`../library/dist/js/lang/${lang}.js?v=${version}`)
];
const [Ed11y, ed11yLang] = await Promise.all(modulePromises);
const ed11y = new Ed11y.Ed11y({
cssUrls: ["/THE_MODULE_PATH/dist/css/editoria11y.min.css?v=3.0.123"]
});
// Optionally make a global alias.
Drupal.Ed11y = Ed11y;
Parameters
A complete implementation will only be called for logged-in editors (you don’t want your site visitors seeing your checker!), and can override any of the default parameters.
Note that defaults are provided for all parameters; only include things you want to override.
A heavily customized implementation might look like this:
const ed11y = new Ed11y.Ed11y({
// Define content regions
checkRoot: 'main, .footer-content-zone',
// Add custom CSS files
cssUrls: [
'/css/editoria11y.min.css',
'/css/custom.css'
],
// Define Web components to include in checks
shadowComponents: 'accordion, lightbox',
// We want the box to open automatically for errors:
alertMode: 'assertive',
// Our external link icon has visually hidden text
// to delete before checking if the link has text:
linkIgnoreStrings: ['(opens in new window)'],
// Content editors cannot edit these elements:
ignoreElements: 'nav *, #social-block',
// Prevent this user from hiding alerts.
allowHide: false,
allowOK: true,
// Don't scan while the editor toolbar is open:
preventCheckingIfPresent: ".editor-toolbar",
// Turn off warnings that an element might be visually hidden:
checkVisible: false,
// Dispatch a JS event before highlighting this,
// so our JS can modify it first:
hiddenHandlers: ".accordion-panel",
// Dispatch a JS event when the library is ready
// for custom tests, then wait for 3 ed11yResume
// events from our 3 tests to continue.
customTests: 3,
checks: {
// Turn off the HEADING_FIRST test
HEADING_FIRST: false,
// Allow dismissing the HEADING_SKIPPED_LEVEL test
HEADING_SKIPPED_LEVEL: {
type: 'warning',
}
}
});
Turnkey integrations often set these variables on the fly – e.g., loading pages in “assertive” mode when they were recently edited, and switching back to “polite” after several minutes.
Theming
Several parameters allow for selecting a theme, overriding colors, and injecting CSS.
Only include the parameters you need to override to make future updates easier.
// Add custom CSS file
cssUrls: ['/css/editoria11y.min.css', '/css/custom.css'],
theme : 'darkTheme', // Select base theme
darkTheme: { // Override base colors
bg: '#0a2051',
bgHighlight: '#7b1919',
text: '#f4f7ff',
primary: '#4930a0',
primaryText: '#f4f7ff',
secondary: '#20160c',
warning: '#fad859',
alert: '#b80519',
button: '#dde8ff',
focusRing: 'cyan',
activeTab: '#0a2051',
tipHeader: '#4930a0',
},
// Override fonts
baseFontSize: 14, // px
baseFontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif',
Useful JS events
Themers can hook these events to react and modify the page as needed.
In typical order of appearing…
ed11yRunCustomTests: dispatched when the Results object is ready for custom results to be injected. Note that the customTests parameter must be set to the number of custom test functions you will be running (…probably one…) for this event to dispatch. Details in following section.ed11yResults: dispatched when all checks are finished. API integrations can now harvest data out of the Ed11y.results object.ed11yPanelOpened: dispatched if the panel opens, automatically or manually.ed11yShowHidden: provides adata-ed11y-resultnumber and aviaJumpboolean. Only dispatched if the “ed11yShowHidden” parameter is set and the a parent of the element matches a selector . Used to reveal alerts in not-yet-open containers, e.g., accordions, tabs and carousels. Usage examples in next section.ed11yPop: providesdata-ed11y-resultID, as well as references to theresultandtipobjects. Dispatched when a tooltip appears, allowing you to modify the tips at runtime.ed11yShut: providesdata-ed11y-resultID. Dispatched when a tooltip closes.ed11yDismissalUpdate: provides extended information when a user dismisses or restores an alert. Used for API integrations. Event object contains:dismissPagedismissTestdismissKeydismissAction
Code samples can be found in the following sections.
Modifying tips
If all you want to do is modify the text of a tip, items in the ed11yLang.en global (before calling the library) or Ed11y.M (after calling the library) objects can be directly overridden. e.g., in the Drupal module where we aliased the language pack to Drupal.ed11yLang:
Drupal.ed11yLang.lang.testNames.title = "Please write shorter headings."
This is true of any of the default keys in the localization file or the active language-specific translation.
The Drupal module also adds custom edit links to the tips, using the editLinks parameter. Note “Page editor” and “Layout editor:”

A simplified version of the code that sets that parameter:
const editLinks = document.createElement('div');
const editLink = document.createElement('a');
editLink.href="/node/1/edit";
editLink.textContent = "Edit page";
editLinks.appendChild(editLink);
const ed11y = new Ed11y({
editLinks: editLinks,
});
The Drupal module then uses the ed11yPop event to dynamically show and hide the edit link based on context when tips are shown.
This event provides references to the result and tip objects:
// Listen for event
document.addEventListener('ed11yPop', e => {
if (e.detail.result.element.closest(
drupalSettings.editoria11y.hide_edit_links)
) {
e.detail.tip.shadowRoot.querySelector(
'.ed11y-custom-edit-links'
)?.setAttribute('hidden', '');
}
});
Custom tests
If the customTests parameter is a number, Editoria11y will dispatch an “ed11yRunCustomTests” event while checking, and then pause for up to 500ms while listening for that number of “ed11yResume” events.
This can be leveraged to call as many scripts containing custom tests as you like, which can push their results into the results array before the tips are draws.
For example, if you wanted to create this tip, to flag links that have been pasted from emails with obfuscated URLs:

You would:
- Add a listener for the
ed11yRunCustomTestsevent - Find matching elements
- Define the tip message
- Dispatch the “resume” event to let Editoria11y draw the tip:
document.addEventListener('ed11yRunCustomTests', function() {
// 1. Write your custom test. This can be anything you want.
//
// This example searches the active root using the .getElements() function,
// which is aware of the checkRoots and ignoreElements parameters.
const safeLinks = Drupal.Ed11y.getElements('a[href*="safelinks.protection.outlook.com/?"]', 'root');
// This is just a utility function for this particular test...
const decodeSafelink = function(url) {
const params = new URL(url).searchParams;
let decoded = params.get('url');
if (!!decoded) {
decoded = Ed11y.sanitizeForHTML(decoded);
return `<p>The actual URL may be:<br><em style='word-wrap: break-word;'>${decoded}</em></p>`
}
return '';
}
// 2. Add the test name to the active language pack.
// Add a title:
Drupal.ed11yLang.lang.testNames.outlookSafeLink = 'URL is Safe Link encoded';
// 3. Push each item you want flagged to Ed11y.results.
//
// You must provide:
// element: The element itself
// test: A key for your test, matching step 2.
// type: warning or error
// content: The tip HTML.
// position: "beforebegin" for interactive elements,
// "afterbegin" for paragraphs and table cells.
// dismiss: false for errors,
// a unique string for manual checks.
safeLinks?.forEach((el) => {
Drupal.Ed11y.State.results.push({
element: el,
test: 'outlookSafeLink',
type: 'error',
content: `<div class="title">Please fix the URL in this link</div>
${decodeSafelink(el.href)}
<div class="why">
<p>The URL for this link is an Outlook "Safe Link" that was copied from an email.
It includes the original recipient's email address,
and will break if Microsoft shuts down the service.</p>
<p>The pasted URL is:<br>
<em style='word-break: break-all;font-size: 90%;line-height: 1.2;
display: block;'>${Drupal.Ed11y.sanitizeHTML(el.href)}</em></p>
</div>
`,
position: 'beforebegin',
dismiss: false,
});
});
// 4. When you are done with all your custom tests, dispatch an "ed11yResume" event:
let allDone = new CustomEvent('ed11yResume');
document.dispatchEvent(allDone);
})
Dealing with alerts on hidden or size-constrained content
Many interactive components (tabs, sliders, accordions) hide content. The Editoria11y info panel includes next/previous buttons to jump directly to issues. If Editoria11y thinks the issue’s tooltip is currently invisible, it will alert the user something is wrong, and then highlight the first visible ancestor – e.g., the div around an accordion.
Ideally, your theme will make those elements visible before Editoria11y’s visibility check runs, so everything just works like magic for your users — carousels auto-advance and accordions auto-open to display the issue.
To do this when the panel first opens (e.g., unfolding all accordion panels with issues) add a JS event listener for ed11yPanelOpened, then do a querySelectorAll for relevant ed11y-element-result elements, and then trigger whatever function your theme uses to reveal that part of the page.
Here’s a jQuery based example. When the panel opens, it disables a sticky menu (so elements are not covered), then looks for any ed11y-element-result elements inside closed accordion items and simulates a click on their toggle button:
document.addEventListener("ed11yPanelOpened", function () {
$('body').addClass('no-stick');
$('ed11y-element-result').each(function () {
if ($(this).parents('.ps-accordion-content[style*="display"]').length > 0) {
$(this).parents('.ps-accordion-content').prev().find('.ps-accordion-item-button').click();
}
})
To reveal content only when jumping to a specific tip via the panel’s “next” button (e.g., in a closed tab or the next carousel slide), you will need to set both hiddenHandlers to relevant CSS selectors and checkVisible to TRUE in your parameters. Then add an event listener for the ed11yShowHidden event. This is thrown if Editoria11y recognizes a tip is inside a container with one of the listed hiddenHandlers selectors provided in the options list. This JS event will include the ID of the tip that it is about to open. Editoria11y will then pause briefly after dispatching this event, to give your JS time to make the element visible.
Here’s an example from a Penn State implementation. It looks for the element matching the provided ID, then finds its parent interactive component container, and triggers its event to activate its toggle:
context.addEventListener('ed11yShowHidden', e => {
if (!e.detail.viaJump) {
return;
}
context.getElementById(e.detail.id)
.closest('[data-interactive-component]')
.dispatchEvent(new CustomEvent('component:activate'));
});
Last note: some themes are just not compatible with visibility checking — e.g., the <main> container has a height of 0px. Such sites should disable all visibility checking by setting checkVisible to false.