HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
Any URLs added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by Skypack, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ES6 import
usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<html data-lightMode=auto>
<body>
<main>
<!-- Adapted from https://www.a11yproject.com/checklist/ -->
<header>
<div>
<h1>Giving users advanced warning when opening a new window</h1>
<p>The objective of this method is to provide an accessible tooltip to give the user fair warning before opening a new window or tab.</p>
</div>
</header>
<div>
<section>
<!-- <h2>Explanation</h2> -->
<blockquote>
<p>Opening new windows automatically when a link is activated can be disorienting for people who have difficulty perceiving visual content, and for some people with cognitive disabilities, if they are not warned in advance. Providing a warning allows the user to decide it they want to leave the current window, and the warning will help them find their way back, if they do decide they would like to go to the new window. It will help them understand that the "back" button will not work and that they have to return to the last window they had open, in order to find their previous location.</p>
<cite><external-link><a href="https://www.w3.org/WAI/WCAG21/Techniques/general/G201">Giving users advanced warning when opening a new window</a></external-link></cite>
</blockquote>
<!-- <h2>Test cases for external links</h2> -->
<details>
<summary>External link test cases</summary>
<div class=tests>
<p>For testing purposes only, the links don't go anywhere, they just open this content in a new window.</p>
<h3>Smallest text link, left aligned</h3>
<p><external-link><a target=_blank href="">1</a></external-link></p>
<h3>Smallest text link, right aligned - test viewport overflow</h3>
<p style="text-align: right"><external-link><a target=_blank href="">1</a></external-link></p>
<h3>Multi-line text link</h3>
<p><external-link><a target=_blank href="">A fake anchor link which is in an extremely long sentence to see what exactly happens when the anchor is spread over multiple lines, especially when the text is zoomed to 200% or more</a></external-link></p>
<h3>List item links</h3>
<ul>
<li><external-link><a target=_blank href="">Fake link</a></external-link></li>
<li>Text before a <external-link><a target=_blank href="">Fake link</a></external-link></li>
<li><external-link><a target=_blank href="">A fake anchor link which is in an extremely long sentence to see what exactly happens when the anchor is spread over multiple lines, especially when the text is zoomed to 200% or more</a></external-link></li>
<li>Text before a <external-link><a target=_blank href="">A fake anchor link which is in an extremely long sentence to see what exactly happens when the anchor is spread over multiple lines, especially when the text is zoomed to 200% or more</a></external-link></li>
</ul>
<p>Text links in a sentence may be observed in content that follows.</p>
</div>
</details>
<h2>How</h2>
<p>Achieved using a mix of CSS and JS to provide a tooltip warning that is both visible and available to screen-readers. Meeting WCAG 2.2 AA to the fullest extent.</p>
<h2>WCAG guidelines followed</h2>
<p>As the link launches a new window, or tab:</p>
<details>
<summary><span>3.2.5 Change on Request</span></summary>
<blockquote>
<p>Success Criterion <external-link><a target=_blank href="https://www.w3.org/TR/WCAG21/#change-on-request">3.2.5 Change on Request</a></external-link> (Level AAA): Changes of context are initiated only by user request or a mechanism is available to turn off such changes.</p>
<cite><external-link><a target=_blank href="https://www.w3.org/WAI/WCAG21/Understanding/change-on-request.html">Understanding Success Criterion 3.2.5: Change on Request</a></external-link></cite>
</blockquote>
<p>If a link launches a new window or tab: <external-link><a target=_blank href="https://www.w3.org/WAI/WCAG21/Techniques/html/H83.html">H83: Using the target attribute to open a new window on user request and indicating this in link text </a></external-link></p>
</details>
<p>The user is informed that the link "Opens new window" by a tooltip, which must be available on both hover and focus:</p>
<details>
<summary><span>2.1.1 Keyboard</span></summary>
<blockquote>
<p>Success Criterion <external-link><a target=_blank href="https://www.w3.org/TR/WCAG21/#keyboard">2.1.1 Keyboard</a></external-link> (Level AA): All functionality of the content is operable through a keyboard interface without requiring specific timings for individual keystrokes, except where the underlying function requires input that depends on the path of the user's movement and not just the endpoints.</p>
<cite><external-link><a target=_blank href="https://www.w3.org/WAI/WCAG21/Understanding/keyboard.html">Understanding Success Criterion 2.1.1: Keyboard</a></external-link></cite>
</blockquote>
</details>
<p>As the tooltip message "Opens new window" only appears when hovering or upon focus it must meet:</p>
<details>
<summary><span>1.4.13 Content on Hover or Focus</span></summary>
<blockquote>
<p>Success Criterion <external-link><a target=_blank href="https://www.w3.org/TR/WCAG21/#content-on-hover-or-focus">1.4.13 Content on Hover or Focus</a></external-link> (Level AA): Where receiving and then removing pointer hover or keyboard focus triggers additional content to become visible and then hidden, the following are true:</p>
<dl>
<dt>Dismissible</dt>
<dd>A method is available to dismiss the additional content without moving pointer hover or keyboard focus, unless the additional content communicates an input error, or does not obscure or replace other content;</dd>
<dt>Hoverable</dt>
<dd>If pointer hover can trigger the additional content, then the pointer can be moved over the additional content without the additional content disappearing;</dd>
<dt>Persistent</dt>
<dd>The additional content remains visible until the hover or focus trigger is removed, the user dismisses it, or its information is no longer valid.</dd>
</dl>
<cite><external-link><a target=_blank href="https://www.w3.org/WAI/WCAG21/Understanding/content-on-hover-or-focus.html">Understanding Success Criterion 1.4.13: Content on Hover or Focus</a></external-link></cite>
</blockquote>
</details>
<p>The tooltip is dismissable via <kbd>Escape</kbd>, it is hoverable, and remains displayed until mouse hover, or the focus, moves away. – One issue, see to do towards the end of article.</p>
<p>As both the link and tooltip are textural:</p>
<details>
<summary><span>1.4.4 Resize Text</span></summary>
<blockquote>
<p>Success Criterion <external-link><a target=_blank href="https://www.w3.org/TR/WCAG21/#resize-text">1.4.4 Resize Text</a></external-link> (Level AA): Except for captions and images of text, text can be resized without assistive technology up to 200 percent without loss of content or functionality.</p>
<cite><external-link><a target=_blank href="https://www.w3.org/WAI/WCAG21/Understanding/resize-text.html">Understanding Success Criterion 1.4.4: Resize Text</a></external-link></cite>
</blockquote>
</details>
<details>
<summary><span>1.4.10 Reflow</span></summary>
<blockquote>
<p>Success Criterion <external-link><a target=_blank href="https://www.w3.org/TR/WCAG21/#reflow">1.4.10 Reflow</a></external-link> (Level AA): Content can be presented without loss of information or functionality, and without requiring scrolling in two dimensions for:</p>
<ul>
<li>Vertical scrolling content at a width equivalent to 320 CSS pixels;</li>
<li>Horizontal scrolling content at a height equivalent to 256 CSS pixels.</li>
</ul>
<cite><external-link><a target=_blank href="https://www.w3.org/WAI/WCAG21/Understanding/reflow.html">Understanding Success Criterion 1.4.10: Reflow</a></external-link></cite>
</blockquote>
</details>
<details>
<summary><span>1.4.12 Text Spacing</span></summary>
<blockquote>
<p>Success Criterion <external-link><a target=_blank href="https://www.w3.org/TR/WCAG21/#text-spacing">1.4.12 Text Spacing</a></external-link> (Level AA): In content implemented using markup languages that support the following text style properties, no loss of content or functionality occurs by setting all of the following and by changing no other style property:</p>
<ul>
<li>Line height (line spacing) to at least 1.5 times the font size;</li>
<li>Spacing following paragraphs to at least 2 times the font size;</li>
<li>Letter spacing (tracking) to at least 0.12 times the font size;</li>
<li>Word spacing to at least 0.16 times the font size.</li>
</ul>
<cite><external-link><a target=_blank href="https://www.w3.org/WAI/WCAG21/Understanding/text-spacing.html">Understanding Success Criterion 1.4.12: Text Spacing</a></external-link></cite>
</blockquote>
</details>
<p>Which the method easily meets, as the content may be resized to at least 500% on a 320px viewport.</p>
<p>The "Opens new window" icon:
<svg class=svg-newWindow width='40' height='40' style="display:inline;vertical-align:bottom">
<title>New window icon</title>
<path d='M28,4 39,4 39,15 M39,4 23,20 M28,9 7,9 7,34 35,34 35,15' fill='none' stroke='#808080' stroke-width='3' />
</svg>
must meet contrast guidelines:
</p>
<details>
<summary><span>1.4.11 Non-text Contrast</span></summary>
<blockquote>
<p>Success Criterion <external-link><a target=_blank href="https://www.w3.org/TR/WCAG21/#non-text-contrast">1.4.11 Non-text Contrast</a></external-link> (Level AA): The visual presentation of the following have a contrast ratio of at least 3:1 against adjacent color(s):</p>
<dl>
<dt>User Interface Components</dt>
<dd>Visual information required to identify user interface components and states, except for inactive components or where the appearance of the component is determined
by the user agent and not modified by the author;</dd>
<dt>Graphical Objects</dt>
<dd>Parts of graphics required to understand the content, except when a particular presentation
of graphics is essential to the information being conveyed.</dd>
</dl>
<cite><external-link><a target=_blank href="https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast.html">Understanding Success Criterion 1.4.11: Non-text Contrast</a></external-link></cite>
</blockquote>
</details>
<p>The icon stroke colour is a middle grey #808080 which provides a 3:1 contrast against a light background, color range: #e4e4e4 to white, while it also provides a 3:1 contrast against a dark background, color range: black to #353535. It's more subtle than the text, and there's no need, with this example, to adjust between light and dark modes, but that would require testing wherever used.</p>
<p>The contrast of the link text to backgound:</p>
<details>
<summary><span>1.4.3 Contrast (Minimum)</span></summary>
<blockquote>
<p>Success Criterion <external-link><a target=_blank href="https://www.w3.org/TR/WCAG21/#contrast-minimum">1.4.3 Contrast (Minimum)</a></external-link> (Level AA): Provide enough contrast between text and its background so that it can be read by people with moderately low vision.</p>
<p>The visual presentation of text and images of text has a contrast ratio of at least 4.5:1</p>
<cite><external-link><a target=_blank href="https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html">Understanding Success Criterion 1.4.3: Contrast (Minimum)</a></external-link></cite>
</blockquote>
</details>
<details>
<summary><span>1.4.1 Use of Color</span></summary>
<blockquote>
<p>Success Criterion <external-link><a target=_blank href="https://www.w3.org/TR/WCAG21/#contrast-minimum">1.4.1 Use of Color</a></external-link> (Level AA): Color is not used as the only visual means of conveying information, indicating an action, prompting a response, or distinguishing a visual element.</p>
<cite><external-link><a target=_blank href="https://www.w3.org/WAI/WCAG21/Understanding/use-of-color.html">Understanding Success Criterion 1.4.1: Use of Color</a></external-link></cite>
</blockquote>
<p>Ensure the link is recognisable by more than just colour. Underlines are best in paragraphs, bold text may work sometimes. Link position can be acceptable, for example in navigation blocks.</p>
</details>
<details>
<summary><span>2.4.4 Link Purpose (In Context)</span></summary>
<blockquote>
<p>Success Criterion <external-link><a target=_blank href="https://www.w3.org/TR/WCAG21/#link-purpose-in-context">2.4.4 Link Purpose (In Context)</a></external-link> (Level A): The purpose of each link can be determined from the link text alone or from the link text together with its programmatically determined link context, except where the purpose of the link would be ambiguous to users in general.</p>
<cite><external-link><a target=_blank href="https://www.w3.org/WAI/WCAG21/Understanding/link-purpose-in-context.html">Understanding Success Criterion 2.4.4: Link Purpose (In Context)</a></external-link></cite>
</blockquote>
<p>Ensure the link text adequately describes the destination. For example don't use "Click here", "More info" and the like. Ask yourself would the link text make sense if it were separated from the content?</p>
</details>
<p>Passes in this example, but again requires testing wherever used.</p>
<h2>To do</h2>
<ul>
<li>Test parent container for overflow:hidden and set edge detection to that element instead of the global window.</li>
<li>Top and bottom viewport overflow detection.</li>
<li>Reading direction right to left, left to right</li>
<li>Cross-pollinate learnings gained here with Adam Argyle's <external-link><a target=_blank href="https://github.com/argyleink/gui-challenges">Tooltip component</a></external-link></li>
<li>Fully test across device / platform / browser / AT and peer review.</li>
</ul>
<h2>Further reading</h2>
<ul>
<li>Sarah Higley: <external-link><a target=_blank href="https://sarahmhigley.com/writing/tooltips-in-wcag-21/">Tooltips in WCAG 2.1</a></external-link></li>
<li>Sarah Higley: <external-link><a target=_blank href="https://codepen.io/smhigley/pen/KjoerX">Tooltips codepen demo</a></external-link></li>
<li>Heydon Pickering: <external-link><a target=_blank href="https://inclusive-components.design/tooltips-toggletips/">Inclusive tooltips and toggletips</a></external-link></li>
<li>Adam Argyle: <external-link><a target=_blank href="https://web.dev/building-a-tooltip-component/">Building a tooltip component</a></external-link></li>
<li>Adam Argyle: <external-link><a target=_blank href="https://github.com/argyleink/gui-challenges">Tooltip component demo</a></external-link></li>
<li>Hidde de Vries: <external-link><a href="https://hidde.blog/dialog-modal-popover-differences/" target=_blank>Dialogs, modality and popovers seem similar. How are they different?</a></external-link></li>
</ul>
</section>
</div>
</main>
</body>
</html>
/* html {
font-family: var(--sans-font, sans-serif);
line-height: var(--line-height, 1.5);
word-break: break-word;
overflow-wrap: break-word;
hyphens: auto;
}
*,
*::before,
*::after {
box-sizing: border-box;
} */
.tests {
padding: .5rem 1rem;
}
.tests h3 {
font-size: calc(var(--base-fontsize, 1.25rem));
font-weight: 500;
margin-top: 1rem;
}
/* <external-links> Opens in new window */
/* Disable default Simplerest icon */
external-link > a[target="_blank"]::after {
display:none;
}
/* Custom element, but no web components were harmed... */
tool-tip {
display: inline-block;
position: relative;
}
/* Add a new window icon (replaces default Simplerest) */
tool-tip::after {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='40' height='40'><path d='M28,4 39,4 39,15 M39,4 23,20 M28,9 7,9 7,34 35,34 35,15' fill='none' stroke='%23808080' stroke-width='3'/></svg>");
background-size: 1em 1em;
background-repeat: no-repeat;
content: "";
display: inline-block;
height: 1em;
/* margin: .125em 0 -.125em .125em; */
margin: 0 0 0 .125em;
width: 1em;
}
tool-tip > span {
/* The right side of the tooltip is aligned with the right side of the link */
--tipLeft: auto;
--tipRight: 0;
/* The arrow is positioned towards the right */
--arrowLeft: auto;
--arrowRight: 0;
left: -200em;
opacity: 0;
overflow: hidden;
position: absolute;
transition: opacity .5s ease-out .5s;
width: 0;
}
@media (prefers-reduced-motion: no-preference) {
tool-tip > span {
transform: translatey(.75rem);
transition:
opacity .5s ease-out .5s,
transform .3s ease-out .25s;
will-change: opacity, transform;
}
}
[target="_blank"]:is(:hover, :focus, :active) tool-tip > span {
border-top: 8px solid transparent;
left: var(--tipLeft, auto);
opacity: 1;
overflow: initial;
/* Default: aligns right side of the tooltip to the right side of the icon */
right: var(--tipRight, 0);
/* transform: translatey(1rem); */
width: max-content;
z-index: 1;
}
tool-tip > span > span {
/* Tooltip colours are the inverse of the page colours */
background-color: CanvasText;
/* Opinion: looks better to me with: */
border-radius: 2px;
box-shadow: 0 2px 4px #000;
/* Tooltip colours are the inverse of the page colours */
color: Canvas;
display: inline-block;
/* rem - So it doesn't inherit a smaller font size from the cascade */
font-size: var(--fs-100, .75rem);
font-style: normal;
line-height: var(--lh-200, calc(2ex + 4px));
max-width: 95vw;
/* px - To prevent increased padding with font-scaling */
padding: 4px 8px 4px;
position: relative;
text-align: center;
}
/* Arrow */
tool-tip > span > span::after {
content: "";
position: absolute;
top: -.3rem;
/* Default: arrow towards the right side of tooltip */
left: var(--arrowLeft, auto);
right: var(--arrowRight, 0);
/* CSS border creates the arrow */
border-bottom: .7em solid CanvasText;
border-left: .7em solid transparent;
border-right: .7em solid transparent;
}
console.clear();
let externalLinkEventsAdded = false;
class externalLinkHTML extends HTMLElement {
/**
* Get and render external HTML
* @param {String} path The path to the external HTML
*/
async #getHTML (link) {
const tooltipElement = 'tool-tip';
const tooltipHoveredClass = '-js-' + tooltipElement + '-hovered';
const linkRel = 'external noopener';
const minMargin = 8;
const tooltipText = document.querySelector('#externalLinkDescription')?.textContent || 'Opens in new window';
const createTooltip = link => {
const tip = document.createElement(tooltipElement);
const span2 = document.createElement('span');
const span3 = document.createElement('span');
span3.textContent = tooltipText;
span2.appendChild(span3);
tip.setAttribute('aria-hidden', true);
tip.appendChild(span2);
return tip;
};
// Need to extend to accomodate overflow:hidden on a container
const isClippedLeft = elem =>
elem && elem.getBoundingClientRect().left < 0;
// Need to extend to accomodate overflow:hidden on a container
const isClippedRight = elem =>
elem && elem.getBoundingClientRect().right > document.documentElement.clientWidth;
const adjustToolTipPosition = tip => {
const span = tip.querySelector('span');
if (!span) return;
// Reset and retest upon each hover
span.removeAttribute('style');
const isClippedL = isClippedLeft(span);
const isClippedR = isClippedRight(span);
// Not clipped, then do nothing
if (!isClippedL && !isClippedR) return;
const viewportWidth = document.documentElement.clientWidth;
const boxTip = tip.getBoundingClientRect();
const boxText = span.getBoundingClientRect();
const remainingSpace = viewportWidth - boxText.width;
if (remainingSpace <= (minMargin * 2)) {
// doesn't fit in viewport - fix the width
span.style.width = `calc(${viewportWidth} - ${minMargin * 2}px)`;
}
if (isClippedL) {
span.style.setProperty('--tipLeft', minMargin - boxTip.left + 'px');
span.style.setProperty('--arrowLeft', boxTip.left - (minMargin / 2) + 'px');
span.style.setProperty('--arrowRight','auto');
}
};
const tooltipStateChange = event => {
const target = event.target;
const externalLink = target.closest('external-link');
if (!externalLink) return;
const tip = externalLink.querySelector(tooltipElement);
if (!tip) return;
const type = event.type;
const isEnter = (type === 'mouseover' || type === 'focusin');
const isLeave = (type === 'mouseout' || type === 'focusout');
isLeave && tip.classList.remove(tooltipHoveredClass);
if (isLeave) return;
isEnter && tip.classList.add(tooltipHoveredClass);
isEnter && adjustToolTipPosition(tip);
};
const isDismissed = event => {
const target = event.target;
if (event.key !== 'Escape') return;
// One element may have focus, while another could have hover
// Solution - remove both
const tips = document.querySelectorAll(`.${tooltipHoveredClass}`);
if (tips) {
// If several listeners are attached to the same element for the same event type, they are called in the order in which they were added.
// If stopImmediatePropagation() is invoked during one such call, no remaining listeners will be called.
// Hopefully preventing the closure of a modal for example.
// To be tested...
event.stopImmediatePropagation();
}
for (const tip of tips) {
const span = tip.querySelector('span');
tip.removeChild(span);
}
};
!link.hasAttribute('rel') && link.setAttribute('rel', linkRel);
if (link.querySelector('tool-tip')) return;
if (link.getAttribute('target') !== '_blank') {
link.target = '_blank';
}
const tip = createTooltip(link);
link.appendChild(tip);
if (externalLinkEventsAdded) return;
// Esc key to dismiss tooltip
document.addEventListener('keydown', isDismissed);
document.addEventListener('mouseover', tooltipStateChange);
document.addEventListener('focusin', tooltipStateChange);
document.addEventListener('mouseout', tooltipStateChange);
document.addEventListener('focusout', tooltipStateChange);
externalLinkEventsAdded = true;
}
constructor () {
// Always call super first in constructor
super();
const link = this.querySelector('a[href]')
if (!link) return;
const externalLinkDescription = document.querySelector('#externalLinkDescription');
if (!externalLinkDescription) {
const description = document.createElement('div');
description.textContent = 'Opens in new window';
description.id = 'externalLinkDescription';
description.hidden = true;
document.body.appendChild(description);
}
link.setAttribute('aria-describedby', 'externalLinkDescription');
this.#getHTML(link);
}
/**
* Runs each time the element is appended to or moved in the DOM
*/
connectedCallback () {
}
}
// Define the new web component
if ('customElements' in window) {
customElements.define('external-link', externalLinkHTML);
}
Also see: Tab Triggers