Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

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.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

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.

+ add another resource

Packages

Add Packages

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.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <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. &ndash; 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>
              
            
!

CSS

              
                /* 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;
}

              
            
!

JS

              
                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);
}

              
            
!
999px

Console