Tooltips

Demo

Text about tooltips.

<button class="notifications" aria-labelledby="tooltip-label">  
  <!-- Your SVG, IMG, icon here! -->
</button>  
<div class="arrow_box" role="tooltip" id="tooltip-label">Hi, I'm tooltip text! Hopefully, something useful and brief.</div>
 
[role="tooltip"] {
  display: none;
  border: 2px solid black;
  padding: 10px;
  border-radius: 5px;
  width: 40%;
}
.arrow_box {
    position: relative;
    background: #fff;
    border: 2px solid #000;
    margin-top: 15px;
}
.arrow_box:after, .arrow_box:before {
    bottom: 100%;
    left: 11%;
    border: solid transparent;
    content: "";
    height: 0;
    width: 0;
    position: absolute;
    pointer-events: none;
}

.arrow_box:after {
    border-color: rgba(255, 255, 255, 0);
    border-bottom-color: #fff;
    border-width: 20px;
    margin-left: -20px;
}
.arrow_box:before {
    border-color: rgba(0, 0, 0, 0);
    border-bottom-color: #000;
    border-width: 23px;
    margin-left: -23px;
}

button:hover + [role="tooltip"],  
button:focus + [role="tooltip"] {  
  display: block;
}
button {
    font-size: 1.25rem;
    border-radius: 0.33em;
    font-family: inherit;
    width: 120px;
    height: 120px;
    color: #fefefe;
    padding: 0.75rem;
    border: 0;
    background: #fff;
}
svg {
    width: 100%;
} 
				
					
// use an anonymous funciton to encapsulate
(() => {
    // max width in pixels for tooltip
    const maxWidth = 500;

    main();

    function main() {
        let revealNameTips = document.querySelectorAll('[data-revealed-name]');

        // add event listeners
        for (const revealNameElement of revealNameTips) {
            prepareHoverableContentState(revealNameElement);
        }

        // remove content on escape or control
        document.body.addEventListener('keydown', toggleHoverContent);

        // handle viewport changes
        window.addEventListener('resize', moveTooltip);
    }

    /**
     * Adds event listeners that set the state of the component
     * @param {HTMLElement} element the element to add event listeners to
     */
    function prepareHoverableContentState(element) {
        // hover
        element.addEventListener('mouseenter', (e) => setState(element, true));
        element.addEventListener('mouseleave', (e) => setState(element, false));
        // focus
        element.addEventListener('focusin', (e) => setState(element, true));
        element.addEventListener('blur', (e) => setState(element, false));
    }

    /**
     * Sets the state and any state related items/attributes.
     * @param {HTMLElement} element the element that owns the state
     * @param {Boolean} isVisible true if visible
     */
    function setState(element, isVisible) {
        // typically, when content appears due to user interaction
        // we want to use ARIA-EXPANDED to convey the expanded or collapsed
        // state of this component. However, in this case, there is no new
        // information being presented that AT would not already know (the 
        // text being shown is the accessible name, or the accessible description).
        // As such, we forgo adding the ARIA-EXPANDED attribute as it would
        // likely only confuse screen reader users, as they might expect the
        // component to reveal new information on activation, rather than
        // perform an action.
        if (isVisible) {
            element.classList.remove('hidden');
        }
        else {
            element.classList.add('hidden');
        }
        repositionTooltip(element.querySelector('[data-visual-name]'));
    }

    /**
     * Finds all elements that are currently showing their tooltip,
     * and then hides those tooltips.
     * @param {KeyboardEvent} e a keyboard event
     */
    function toggleHoverContent(e) {
        // we return if the key pressed isn't Escape or Control
        if (e.key !== 'Escape' && e.key !== 'Control') return;
        // we get all currently visible content. We use querySelectorAll
        // in case the user has both focused an element and hovered a
        // different element.
        let hoveredElements = document.querySelectorAll('[data-revealed-name]:not(.hidden)');
        for (const hoveredElement of hoveredElements) {
            hoveredElement.classList.add('hidden');
        }
    }

    /**
     * Finds all currently visible tooltip text elements, and then
     * repositions them to ensure that they are not offscreen.
     */
    function moveTooltip() {
        // get all visible tooltips
        let visibleTips = document.querySelectorAll(
            '[data-revealed-name]:not(.hidden) [data-visual-name]'
        );
        for (const visibleTip of visibleTips) {
            repositionTooltip(visibleTip);
        }
    }

    /**
     * Checks the position of the tooltip text and repositions and resizes
     * the tooltipText element such that it remains within the viewport.
     * Important for ensuring that there is no loss of content when the viewport
     * is zoomed in.
     * @param {HTMLElement} tooltipText The tooltip text element that is shown on hover/focus
     */
    function repositionTooltip(tooltipText) {
        // reset the styling
        tooltipText.style.removeProperty('width');
        tooltipText.style.removeProperty('left');
        tooltipText.style.removeProperty('white-space');
        // get tooltipText dimensions
        let tooltipRect = tooltipText.getBoundingClientRect();
        // set max-width
        if (tooltipRect.width > maxWidth) {
            tooltipText.style.width = maxWidth + 'px';
            tooltipText.style.whiteSpace = 'normal';
        }
        tooltipRect = tooltipText.getBoundingClientRect();
        // get parentElement dimensions (should be a [data-revealed-name] element)
        let parentRect = tooltipText.parentElement.getBoundingClientRect();
        // viewport width minus body margin
        let vw = document.body.clientWidth + 2 * getPropertyAsNumber(document.body, 'margin');
        // get the padding of the tooltip
        let paddingLeft = getPropertyAsNumber(tooltipText, 'padding-left');
        // math to move the tooltip to the middle
        let parentMiddlePoint = parentRect.x + (parentRect.width / 2);
        let tooltipStartPoint = parentMiddlePoint - (tooltipRect.width / 2);
        let tooltipLeftOffset = tooltipStartPoint - parentRect.x;
        tooltipText.style.left = tooltipLeftOffset + 'px';
        
        // get new tooltip dimensions
        tooltipRect = tooltipText.getBoundingClientRect();
        // if tooltipRect width is larger than viewport, set to viewport;
        if (tooltipRect.width > vw) {
            let widthOffset = (vw - (2 * paddingLeft));
            let leftOffset = (-1 * parentRect.x) +  paddingLeft;
            tooltipText.style.width = widthOffset + 'px';
            tooltipText.style.left = leftOffset + 'px';
            // in the style sheet we've set the "white-space" CSS property
            // to nowrap, but when the text is longer than the width of the
            // viewport, we need to wrap the text
            tooltipText.style.whiteSpace = 'normal';
        }
        // if it starts off screen to the left
        else if (tooltipRect.x < 0) {
            let offset = (-1 * parentRect.x) + paddingLeft;
            tooltipText.style.left = offset + 'px';
        }
        // if it is off screen to the right
        else if ((tooltipRect.x + tooltipRect.width) > vw) {
            let offset = (-1 * (parentRect.x + tooltipRect.width - vw + paddingLeft));
            tooltipText.style.left = offset + 'px';
        }
    }

    /**
     * Gets the value of a CSS property for an element as a number. 
     * @param {HTMLElement} element gets the CSS property of this element
     * @param {String} property the CSS property name
     * @returns the CSS property's value as a Number (note CSS values are typically in pixels)
     */
    function getPropertyAsNumber(element, property) {
        return Number(
            window
                .getComputedStyle(element)
                .getPropertyValue(property)
                .match(/\d+/gi)
        );
    }
})();
				
			

Why use a tooltip component?

Tooltip components in web development are used to provide additional information or context when users interact with specific elements on a webpage. These components typically display a small pop-up box containing text or multimedia content when users hover over or focus on an element, such as a button or link. From a digital accessibility perspective, tooltips play a crucial role in enhancing user experience for individuals with disabilities by offering supplementary information that may not be immediately apparent from the element’s visible content. This feature can benefit users who rely on screen readers or keyboard navigation, as tooltips provide concise explanations or instructions for interactive elements, improving comprehension and navigation on the webpage.

What are some accessibility concerns?

  1. Lack of Keyboard Accessibility
    • Concern: Without proper keyboard accessibility, users who rely on keyboard navigation cannot interact with tooltips, making the content inaccessible to them.
    • Mitigation: Ensure tooltips are keyboard accessible by making them focusable with the tabindex attribute and allowing users to activate them using the Enter or Space key. Implement ARIA roles and attributes such as role=”tooltip” and aria-describedby to associate tooltips with their triggering elements and announce them to screen reader users.
  2. Insufficient Contrast
    • Concern: Low contrast between the tooltip text and background color can make it difficult for users with low vision or color blindness to read the content.
    • Mitigation: Ensure sufficient color contrast between the tooltip text and background according to WCAG guidelines (minimum contrast ratio of 4.5:1 for normal text or 3:1 for large text). Use high-contrast color schemes or provide an option for users to customize tooltip colors to meet their visibility needs.
  3. Inconsistent or Unpredictable Behavior
    • Concern: Inconsistent behavior of tooltips, such as appearing and disappearing unpredictably, can confuse users and disrupt their workflow.
    • Mitigation: Ensure tooltips have consistent behavior across different elements and interactions throughout the website. Implement a reasonable delay before displaying tooltips to prevent them from appearing too quickly and causing distraction, and ensure they remain visible for an adequate duration before disappearing.
  4. Lack of Screen Reader Compatibility
    • Concern: Inaccessible tooltips that are not announced or properly described by screen readers can exclude users with visual impairments from accessing the additional information.
    • Mitigation: Associate tooltips with their triggering elements using ARIA attributes to ensure they are announced by screen readers. Provide descriptive tooltip content that conveys the purpose or function of the element without relying solely on visual cues.
  5. Overlapping or Obscuring Content
    • Concern: Tooltips that overlap with or obscure other content on the page can create barriers for users with cognitive or motor disabilities.
    • Mitigation: Ensure tooltips are positioned dynamically to avoid overlapping with nearby content, especially when triggered near the edges of the viewport. Implement proper spacing and layout adjustments to prevent tooltips from obscuring important page elements, especially on smaller screens or in responsive designs.
Tested using
Chrome
with NVDA
Firefox
with NVDA
  • W3C SVG
    1.3.1
    Info and Relationships ( level A ) :
    Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text.
  • W3C SVG
    2.1.1
    Keyboard ( level A ) :
    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.
  • W3C SVG
    2.4.3
    Focus Order ( level A ) :
    If a Web page can be navigated sequentially and the navigation sequences affect meaning or operation, focusable components receive focus in an order that preserves meaning and operability.
  • W3C SVG
    4.1.2
    Name, Role, Value ( level A ) :
    For all user interface components (including but not limited to: form elements, links and components generated by scripts), the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to user agents, including assistive technologies.

Lightbox

Demo

				
					
<div class="image-gallery">
  <!-- Enlarged photo div -->
  <div class="enlarged-photo">
    <img decoding="async" src="https://placehold.co/600x600?text=Image+1" alt="Thumbnail 1">
    <!-- Empty div to be populated with hovered image -->
  </div>

  <!-- Row for smaller images -->
  <div class="thumbnails">
    <!-- Replace the image URLs with your actual image URLs -->
    <div class="thumbnail-button" role="button" tabindex="0" aria-label="expand image into lightbox view">
      <img decoding="async" src="https://placehold.co/600x600?text=Image+1" alt="Thumbnail 1">
    </div>
    <div class="thumbnail-button" role="button" tabindex="0" aria-label="expand image into lightbox view">
      <img decoding="async" src="https://placehold.co/600x600?text=Image+2" alt="Thumbnail 2">
    </div>
    <div class="thumbnail-button" role="button" tabindex="0" aria-label="expand image into lightbox view">
      <img decoding="async" src="https://placehold.co/600x600?text=Image+3" alt="Thumbnail 3">
    </div>
    <div class="thumbnail-button" role="button" tabindex="0" aria-label="expand image into lightbox view">
      <img decoding="async" src="https://placehold.co/600x600?text=Image+4" alt="Thumbnail 4">
    </div>
  </div>
</div>
<!-- Lightbox view with initial hidden state -->
<div class="lightbox-view hidden">
  <button class="close-btn">&times;</button>
  <div class="lightbox-content">
    <img decoding="async" src="" alt="Lightbox image">
    <div class="navigation">
      <button class="prev-btn">&lt;</button>
      <button class="next-btn">&gt;</button>
    </div>
  </div>
</div>
				
			
				
					
/* Basic layout styles */
.image-gallery {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.lightbox {
  display: none;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background-color: rgba(255, 255, 255, 0.9);
  padding: 20px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
}

.navigation {
  margin-top: 15px;
}

/* Styling for next and previous buttons in lightbox */
.navigation button {
  background: rgba(0, 0, 0, 0.5); /* Semi-transparent background */
  color: white;
  border: none;
  padding: 10px 15px;
  font-size: 16px;
  cursor: pointer;
  border-radius: 5px;
  transition: background-color 0.3s ease-in-out;
}

.navigation button:hover {
  background: rgba(0, 0, 0, 0.7); /* Darker background on hover */
}

/* Styles for animation and interaction */
.thumbnails img:hover {
  transform: scale(1.1);
}

/* Style for close button */
.close-btn {
  position: absolute;
  top: 10px;
  right: 10px;
  font-size: 24px;
  cursor: pointer;
}

/* Enlarged photo */
.enlarged-photo {
  text-align: center;
  margin-bottom: 20px;
}

/* Additional styles for smaller images */
.thumbnails {
  display: flex;
  justify-content: space-between;
  gap: 5px;
}

.thumbnails img {
  width: calc(25% - 3px);
  height: auto;
  cursor: pointer;
  transition: transform 0.3s ease-in-out;
}

/* Styles for thumbnail buttons */
.thumbnail-button {
  display: inline-block; /* Ensure they respect their content size */
  overflow: hidden; /* Hide overflowing content */
}

.thumbnail-button img {
  width: 100%; /* Occupy full width of the button */
  height: auto; /* Maintain aspect ratio */
}

/* Adjustments for smaller images on hover */
.thumbnails img:hover {
  transform: scale(0.9);
}

/* Hidden state for the lightbox view */
.lightbox-view {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.7);
  z-index: 9999;
  overflow: auto;
}

.lightbox-view.hidden {
  visibility: hidden;
}

/* Centered lightbox content */
.lightbox-content {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
  color: white;
}

/* Close button in lightbox */
.close-btn {
  position: absolute;
  top: 10px;
  right: 10px;
  font-size: 24px;
  cursor: pointer;
  color: white; /* Change color for better visibility */
  background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent background */
  padding: 8px 12px;
  border-radius: 50%;
  z-index: 100; /* Ensures it's above the image */
}
				
			
				
					
// JavaScript for handling interactions and functionality
const thumbnails = document.querySelectorAll(".thumbnails img");
const thumbnailBtns = document.querySelectorAll(".thumbnail-button");
const enlargedPhoto = document.querySelector(".enlarged-photo");
const lightboxView = document.querySelector(".lightbox-view");
const lightboxImages = document.querySelectorAll(".lightbox-view img");
const lightboxImage = document.querySelector(
  ".lightbox-view .lightbox-content img"
);
const closeBtn = document.querySelector(".lightbox-view .close-btn");
const nextBtn = document.querySelector(".lightbox-view .next-btn");
const prevBtn = document.querySelector(".lightbox-view .prev-btn");
let currentImageIndex = 0;
let lastFocusedThumbnail = null;

// Function to update the enlarged photo on mouseover or focus
function updateEnlargedPhoto(event) {
  if (event.type === "mouseover") {
    enlargedPhoto.innerHTML = `<img decoding="async" src="${event.target.src}" alt="Enlarged image">`;
  } else if (event.type === "focus") {
    enlargedPhoto.innerHTML = `<img decoding="async" src="${
      event.target.querySelector("img").src
    }" alt="Enlarged image">`;
  }
}

// Function to manage focus trap within the lightbox view
function manageFocusTrap(event) {
  const focusableElements = [closeBtn, prevBtn, nextBtn];

  if (event.shiftKey && event.key === "Tab") {
    // If Shift + Tab is pressed, focus the last focusable element
    if (document.activeElement === closeBtn) {
      event.preventDefault();
      nextBtn.focus();
    } else if (document.activeElement === prevBtn) {
      event.preventDefault();
      closeBtn.focus();
    }
  } else if (event.key === "Tab") {
    // If Tab is pressed, focus the first focusable element
    if (document.activeElement === nextBtn) {
      event.preventDefault();
      closeBtn.focus();
    } else if (document.activeElement === closeBtn) {
      event.preventDefault();
      prevBtn.focus();
    }
  }
}

// Function to open the lightbox view with clicked image
function openLightbox(event) {
  let clickedImageSrc;
  if (event.target.tagName === "DIV") {
    clickedImageSrc = event.target.querySelector("img").src;
  } else {
    clickedImageSrc = event.target.src;
  }
  lightboxImages.forEach((image) => {
    image.src = clickedImageSrc;
  });
  lightboxView.classList.remove("hidden"); // Remove the hidden class
  lightboxView.style.display = "flex"; // Ensure the lightbox is displayed
  document.body.style.overflow = "hidden"; // Prevent scrolling on the main page
  lastFocusedThumbnail = event.currentTarget;

  // Attach focus trap event listener when the lightbox is opened
  document.addEventListener("keydown", manageFocusTrap);

  // Delay the focus on the close button after a short timeout
  setTimeout(() => {
    closeBtn.focus(); // Move focus to the close button
  }, 100); // Adjust the delay timing if needed
}

// Functionality to close the lightbox view
function closeLightbox() {
  lightboxView.classList.add("hidden"); // Hide the lightbox view
  document.body.style.overflow = "auto"; // Restore scrolling on the main page
  // Remove focus trap event listener when the lightbox is closed
  document.removeEventListener("keydown", manageFocusTrap);
  if (lastFocusedThumbnail) {
    lastFocusedThumbnail.focus(); // Return focus to the last focused thumbnail
  }
}

// Attach event listeners to thumbnails for hover and lightbox opening
thumbnails.forEach((thumbnail) => {
  thumbnail.addEventListener("mouseover", updateEnlargedPhoto);
  thumbnail.addEventListener("click", openLightbox);
});

thumbnailBtns.forEach((thumbnail) => {
  thumbnail.addEventListener("focus", updateEnlargedPhoto);
  thumbnail.addEventListener("click", openLightbox);
  thumbnail.addEventListener("keydown", (event) => {
    if (event.key === "Enter") {
      openLightbox(event);
    }
  });
});

// Functionality to close the lightbox view
closeBtn.addEventListener("click", closeLightbox);

// Function to update the lightbox with the current image
function updateLightboxImage() {
  thumbnails.forEach((thumbnail, index) => {
    if (index === currentImageIndex) {
      lightboxImage.src = thumbnail.src;
    }
  });
}

// Function to navigate to the next image
function goToNextImage() {
  currentImageIndex = (currentImageIndex + 1) % thumbnails.length;
  updateLightboxImage();
}

// Function to navigate to the previous image
function goToPrevImage() {
  currentImageIndex =
    (currentImageIndex - 1 + thumbnails.length) % thumbnails.length;
  updateLightboxImage();
}

// Functionality to navigate images using buttons and update lightbox image
nextBtn.addEventListener("click", goToNextImage);
prevBtn.addEventListener("click", goToPrevImage);

// Show the first image initially in the lightbox
updateLightboxImage();
				
			

Why use a lightbox component?

A lightbox component serves as a valuable tool in web development by offering a focused and interactive way to display images, videos, or content without navigating away from the main page. It enhances the user experience by presenting visual media in a larger, more prominent view, allowing users to engage with the content while maintaining context within the website. From an accessibility standpoint, a well-implemented lightbox ensures that users with disabilities can navigate through the media content by providing keyboard accessibility, descriptive labels, and compatibility with screen readers. It also prevents the disruption of user flow by overlaying content in a visually appealing manner, making it easier for all users to view and interact with multimedia content without compromising the overall accessibility of the website.

What are some accessibility concerns?

  1. Keyboard Accessibility
    • Concern: Users may have difficulty navigating and interacting with the lightbox using only a keyboard, especially when the focus isn’t appropriately managed.
    • Mitigation: Ensure keyboard accessibility by allowing users to open, navigate, and close the lightbox using keyboard controls such as Tab, Enter/Return, and Esc. Manage focus within the lightbox to prevent users from being trapped and ensure a logical tab order.
  2. Screen Reader Compatibility
    • Concern: Screen reader users may struggle to perceive or access content within the lightbox without proper labeling or context.
    • Mitigation: Provide appropriate alternative text (alt text) for images and descriptive text for other media types within the lightbox. Ensure the content is announced correctly and the screen reader can access it by managing ARIA attributes and roles.
  3. Color Contrast and Visibility
    • Concern: Insufficient color contrast or poor visibility within the lightbox can impact users with visual impairments.
    • Mitigation: Ensure sufficient color contrast between text and background elements within the lightbox. Avoid relying solely on color to convey information, and use other visual cues or text labels to enhance comprehension.
  4. Focus Management
    • Concern: Users might experience focus issues when entering or exiting the lightbox, leading to disorientation or loss of context.
    • Mitigation: Manage focus properly by returning focus to the appropriate element when closing the lightbox. Consider using ARIA attributes like aria-modal or aria-hidden to manage focus and ensure proper interaction with assistive technologies.
  5. Resize and Zoom Compatibility
    • Concern: Users might encounter difficulties resizing or zooming in on content within the lightbox, impacting users with low vision.
    • Mitigation: Ensure that the lightbox content is responsive and supports resizing without causing loss of information or functionality. Avoid fixed sizes that limit zoom capabilities and provide adequate controls for users to adjust content size if needed.
  6. Use of Animation or Motion
    • Concern: Excessive or distracting animations within the lightbox might be disorienting or cause difficulties for users with motion sensitivities.
    • Mitigation: Provide options to disable or limit animations within the lightbox. Ensure that any animations used are subtle, non-distracting, and conform to WCAG guidelines for motion sensitivity.
Tested using
Chrome
with NVDA
Firefox
with NVDA
  • W3C SVG
    1.1.1
    Non-text Content ( level A ) :
    All non-text content that is presented to the user has a text alternative that serves the equivalent purpose.
  • W3C SVG
    1.3.1
    Info and Relationships ( level A ) :
    Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text.
  • W3C SVG
    2.1.2
    No Keyboard Trap ( level A ) :
    If keyboard focus can be moved to a component of the page using a keyboard interface, then focus can be moved away from that component using only a keyboard interface, and, if it requires more than unmodified arrow or tab keys or other standard exit methods, the user is advised of the method for moving focus away.
  • W3C SVG
    2.4.3
    Focus Order ( level A ) :
    If a Web page can be navigated sequentially and the navigation sequences affect meaning or operation, focusable components receive focus in an order that preserves meaning and operability.
  • W3C SVG
    4.1.2
    Name, Role, Value ( level A ) :
    For all user interface components (including but not limited to: form elements, links and components generated by scripts), the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to user agents, including assistive technologies.

Image Gallery

Demo

				
					
<div class="body">
    <div class="image-gallery">
      <div class="image-item">
        <a
          href="https://example.com/link1"
          role="link"
          tabindex="0"
          aria-label="Link Image 1 - Placeholder 300x200"
        >
          <img decoding="async" src="https://placehold.co/300x200" alt="Description 1" />
        </a>
      </div>
      <div class="image-item">
        <a
          href="https://example.com/link2"
          role="link"
          tabindex="0"
          aria-label="Link Image 2 - Placeholder 600x400"
        >
          <img decoding="async" src="https://placehold.co/600x400" alt="Description 2" />
        </a>
      </div>
      <div class="image-item">
        <img decoding="async"
          src="https://placehold.co/1000x300"
          alt="Description"
          aria-label="Plain Image 3 - Placeholder 1000x300"
        />
      </div>
      <div class="image-item">
        <button type="button" aria-label="Button Image 4 - Placeholder 300x400">
          <img decoding="async" src="https://placehold.co/300x400" alt="Description 3" />
        </button>
      </div>
      <div class="image-item">
        <a
          href="https://example.com/link5"
          role="link"
          tabindex="0"
          aria-label="Link Image 5 - Placeholder 300x400"
        >
          <img decoding="async" src="https://placehold.co/300x400" alt="Description 4" />
        </a>
      </div>
      <div class="image-item">
        <img decoding="async"
          src="https://placehold.co/240x380"
          alt="Description"
          aria-label="Plain Image 6 - Placeholder 240x380"
        />
      </div>
      <div class="image-item">
        <button type="button" aria-label="Button Image 7 - Placeholder 500x175">
          <img decoding="async" src="https://placehold.co/500x175" alt="Description 5" />
        </button>
      </div>
      <div class="image-item">
        <a
          href="https://example.com/link8"
          role="link"
          tabindex="0"
          aria-label="Link Image 8 - Placeholder 900x430"
        >
          <img decoding="async" src="https://placehold.co/900x430" alt="Description 6" />
        </a>
      </div>
      <div class="image-item">
        <img decoding="async"
          src="https://placehold.co/875x600"
          alt="Description"
          aria-label="Plain Image 9 - Placeholder 875x600"
        />
      </div>
      <div class="image-item">
        <button
          type="button"
          aria-label="Button Image 10 - Placeholder 300x400"
        >
          <img decoding="async" src="https://placehold.co/300x400" alt="Description 7" />
        </button>
      </div>
      <div class="image-item">
        <a
          href="https://example.com/link11"
          role="link"
          tabindex="0"
          aria-label="Link Image 11 - Placeholder 300x400"
        >
          <img decoding="async" src="https://placehold.co/300x400" alt="Description 8  " />
        </a>
      </div>
      <div class="image-item">
        <img decoding="async"
          src="https://placehold.co/240x380"
          alt="Description"
          aria-label="Plain Image 12 - Placeholder 240x380"
        />
      </div>
      <!-- Add more images as needed -->
    </div>
  </div>
				
			
				
					
/* CSS styles */
.body {
  margin: 0;
  padding: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}

.image-gallery {
  width: 50%; /* Adjust container width here */
  column-gap: 8px; /* Adjust the gap between columns */
  margin: 0;
  padding: 0;
  columns: auto 4; /* Adjust the number of columns based on the layout */
}

.image-item {
  display: inline-block;
  margin-bottom: 8px; /* Adjust the vertical spacing between images */
  break-inside: avoid; /* Prevent breaking within images */
  transition: transform 0.3s ease-in-out; /* Added transition */
}

.image-item img {
  width: 100%; /* Ensure images take the full width of their container */
  height: auto;
  display: block;
}

.image-item button {
  border: none;
}
				
			
				
					
// Wait for the DOM content to load before executing the script
document.addEventListener("DOMContentLoaded", function () {
  // Select the image gallery container
  const gallery = document.querySelector(".image-gallery");

  // Select all individual image items within the gallery
  const items = gallery.querySelectorAll(".image-item");

  // Iterate over each image item
  items.forEach((item) => {
    // Listen for the "mouseover" event on each image item
    item.addEventListener("mouseover", function (event) {
      // Iterate over all image items again to handle scaling logic
      items.forEach((otherItem) => {
        // Check if the current item being iterated is not the hovered item
        if (otherItem !== item) {
          // Calculate the distance between the hovered item and other items
          const dx = Math.abs(otherItem.offsetLeft - item.offsetLeft);
          const dy = Math.abs(otherItem.offsetTop - item.offsetTop);

          // Check if the other item is within a specific distance from the hovered item
          if (dx <= 350 && dy <= 350) {
            // Apply a scaling effect to nearby items
            otherItem.style.transform = "scale(0.95)";
          } else {
            // Reset the scaling for items outside the specified distance
            otherItem.style.transform = "";
          }
        } else {
          // Apply a scaling effect to the hovered item itself
          item.style.transform = "scale(1.05)";
        }
      });
    });

    // Listen for the "mouseout" event on each image item
    item.addEventListener("mouseout", function () {
      // Reset the scaling for all items when the cursor moves away
      items.forEach((otherItem) => {
        otherItem.style.transform = "";
      });
    });
  });
});
				
			

Why use an image gallery?

An image gallery component in web development serves as a powerful tool for organizing and displaying a collection of visual content, offering users a seamless and engaging browsing experience. It helps in presenting a series of images or multimedia content in a structured and visually appealing manner, fostering better engagement and comprehension. From product showcases to portfolios and storytelling, image galleries enhance the aesthetic appeal of a website while facilitating efficient content consumption. Accessibility-wise, an image gallery, when built with attention to detail, supports various accessibility features like alternative text (alt tags), keyboard navigation, and semantic HTML, ensuring inclusivity by providing access to visual content for users of all abilities and assistive technologies. Thus, employing an image gallery component not only elevates the visual appeal of a website but also contributes significantly to its accessibility and user-friendliness.

What are some accessibility concerns?

  1. Alternative Text (Alt Tags):
    • Concern: Ensure each image has descriptive and concise alt text that conveys its purpose or content.
    • Mitigation: Add descriptive alt text for images: <img src=”example.jpg” alt=”Brief description”>.
  2. Keyboard Navigation:
    • Concern: Enable keyboard focus on interactive elements like buttons or links within the gallery to ensure seamless navigation for users who rely on keyboards or screen readers.
    • Mitigation: Implement tabindex and ARIA roles for keyboard accessibility: tabindex=”0″ and role=”button/link”.
  3. Focus Styles:
    • Concern: Ensure clear and visible focus styles for focused elements, aiding users in understanding which element they’re interacting with.
    • Mitigation: Style focus states for interactive elements: :focus.
  4. Semantic HTML:
    • Concern: Use semantic HTML elements and proper labeling to enhance the structure and clarity of the gallery for screen readers and other assistive technologies.
    • Mitigation: Use semantic HTML tags: <figure>, <figcaption>, <a>, <button>, etc., appropriately.
  5. Color Contrast:
    • Concern: Ensure sufficient color contrast between text, background, and interactive elements to improve readability and usability for users with visual impairments.
    • Mitigation: Check color contrast using tools like WebAIM’s Contrast Checker or browser extensions.
  6. Animations:
    • Concern: Control and provide options to pause or adjust any automatic or looping animations to prevent potential issues for users with vestibular disorders or cognitive sensitivities.
    • Mitigation: Offer controls to pause or adjust animations: “Pause” or “Stop” buttons for slideshows.
  7. Responsive Design:
    • Concern: Ensure responsiveness for different screen sizes and devices, allowing users to access and navigate the gallery comfortably on various devices.
    • Mitigation: Employ responsive design techniques: Use media queries and flexible layouts to adapt to different screens.
Tested using
Chrome
with NVDA
Firefox
with NVDA
  • W3C SVG
    1.1.1
    Non-text Content ( level A ) :
    All non-text content that is presented to the user has a text alternative that serves the equivalent purpose.
  • W3C SVG
    2.4.3
    Focus Order ( level A ) :
    If a Web page can be navigated sequentially and the navigation sequences affect meaning or operation, focusable components receive focus in an order that preserves meaning and operability.
  • W3C SVG
    4.1.2
    Name, Role, Value ( level A ) :
    For all user interface components (including but not limited to: form elements, links and components generated by scripts), the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to user agents, including assistive technologies.

Drawers

Demo





HTML Code

				
					
<!-- Dark overlay for expanded drawers -->
<!-- Buttons to trigger drawer expansion -->
<button aria-label="expand top drawer" data-direction="top">
  Top Drawer
</button>
<button aria-label="expand left drawer" data-direction="left">
  Left Drawer
</button>
<button aria-label="expand right drawer" data-direction="right">
  Right Drawer
</button>
<button aria-label="expand bottom drawer" data-direction="bottom">
  Bottom Drawer
</button>
<!-- Top Drawer -->
<button>Close</button>
<!-- Navigation links for the top drawer -->
<nav>
  <a href="#">Link 1</a>
  <a href="#">Link 2</a>
  <a href="#">Link 3</a>
  <a href="#">Link 4</a>
  <a href="#">Link 5</a>
  <a href="#">Link 6</a>
</nav>
<!-- Left Drawer -->
<button>Close</button>
<!-- Navigation links for the left drawer -->
<nav>
  <a href="#">Link 1</a>
  <a href="#">Link 2</a>
  <a href="#">Link 3</a>
  <a href="#">Link 4</a>
  <a href="#">Link 5</a>
  <a href="#">Link 6</a>
</nav>
<!-- Right Drawer -->
<button>Close</button>
<!-- Navigation links for the right drawer -->
<nav>
  <a href="#">Link 1</a>
  <a href="#">Link 2</a>
  <a href="#">Link 3</a>
  <a href="#">Link 4</a>
  <a href="#">Link 5</a>
  <a href="#">Link 6</a>
</nav>
<!-- Bottom Drawer -->
<button>Close</button>
<!-- Navigation links for the bottom drawer -->
<nav>
  <a href="#">Link 1</a>
  <a href="#">Link 2</a>
  <a href="#">Link 3</a>
  <a href="#">Link 4</a>
  <a href="#">Link 5</a>
  <a href="#">Link 6</a>
</nav>

				
			

CSS Code

				
					
@import url("https://fonts.googleapis.com/css2?family=Barlow&family=Oswald&display=swap");
body {
  font-family: "Barlow", sans-serif;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  background-color: #f4f4f4;
  transition: background-color 0.5s; /* Transition for background color change */
}
.select-container {
  display: flex;
  gap: 10px;
}
.drawer {
  position: fixed;
  z-index: 1000;
  opacity: 0;
  transition: transform 0.5s, opacity 0.5s;
  background-color: #fff; /* Set the background color to white */
}
/* Adjust the positioning of the close button */
.drawer-content {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: 20px;
  position: relative; /* Ensure positioning context for the absolute button */
}
.close-button {
  cursor: pointer;
  background: none;
  border: none;
  font-size: 16px;
  color: #005e86;
  position: relative;
  left: 20px;
  top: 10px;
}
.top,
.bottom,
.left,
.right {
  border: 2px solid transparent; /* Adding a transparent border initially */
}
.top {
  width: 100%;
  max-height: 60vh;
  overflow-y: auto;
  transform-origin: top;
  top: 0;
  left: 0;
  pointer-events: none; /* Disable pointer events */
  user-select: none; /* Disable user selection */
}
.bottom {
  transform-origin: bottom;
  transform: translateY(-100%);
  bottom: 0;
  left: 0;
  width: 100%;
  max-height: 60vh;
  overflow-y: auto;
  pointer-events: none; /* Disable pointer events */
  user-select: none; /* Disable user selection */
}
.left {
  height: 100%;
  max-width: 140vw;
  overflow-x: auto;
  transform-origin: left;
  transform: translateX(-100%);
  top: 0;
  left: 0;
  pointer-events: none; /* Disable pointer events */
  user-select: none; /* Disable user selection */
}
.right {
  height: 100%;
  max-width: 140vw;
  overflow-x: auto;
  transform-origin: right;
  transform: translateX(100%);
  top: 0;
  right: 0;
  pointer-events: none; /* Disable pointer events */
  user-select: none; /* Disable user selection */
}
.top .drawer-content,
.bottom .drawer-content {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
.left .drawer-content,
.right .drawer-content {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-start;
}
.drawer-content a {
  text-decoration: none;
  color: #005e86;
  transition: color 0.3s;
  font-size: 18px; /* Increased font size for better readability */
  width: 90%;
  padding: 10px;
}
.drawer-content a:hover,
.drawer-content a:focus {
  color: #211224;
  background-color: rgba(211, 211, 211, 0.774);
}
.drawer-button {
  padding: 10px 20px;
  background-color: #005e86;
  color: #fff;
  border: none;
  border-radius: 5px;
  font-size: 16px;
  cursor: pointer;
  transition: background-color 0.3s;
}
.drawer-button:hover {
  background-color: #99b4c0;
}
.close-button:hover {
  color: #99b4c0;
}
/* Adjust drawer appearance when it's open */
.drawer-open .drawer {
  border-color: #005e86; /* Change to your desired border color */
}
/* Adjust drawer appearance when it's open */
.drawer-open .top,
.drawer-open .bottom,
.drawer-open .left,
.drawer-open .right {
  border-color: #005e86; /* Change to your desired border color */
}
/* Dark overlay for the main page when drawer is opened */
.drawer-open .overlay {
  position: fixed;
  z-index: 1;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(
    0,
    0,
    0,
    0.3
  ); /* Adjust the alpha value to change darkness */
  pointer-events: none; /* Allows interaction with elements behind the overlay */
}
/* Adjust drawer appearance when it's open */
.drawer-open .drawer {
  z-index: 1001; /* Ensures drawers are on top of the overlay */
}
				
			

JavaScript Code

				
					
document.addEventListener("DOMContentLoaded", function () {
  // Set initial tabindex to -1 for all drawer links and close buttons
  const drawerLinks = document.querySelectorAll(".drawer-content a");
  drawerLinks.forEach((link) => {
    link.tabIndex = -1;
  });
  const closeButtons = document.querySelectorAll(".close-button");
  closeButtons.forEach((button) => {
    button.tabIndex = -1;
  });
  // Logic for opening/closing drawers
  const buttons = document.querySelectorAll(".drawer-button");
  const closeButton = document.querySelectorAll(".close-button");
  // Function to close all drawers
  function closeAllDrawers() {
    const allDrawers = document.querySelectorAll(".drawer");
    allDrawers.forEach((drawer) => {
      drawer.style.transform = "scale(0)";
      drawer.style.opacity = "0";
      drawer.setAttribute("aria-hidden", "true");
      drawer.style.pointerEvents = "none";
      drawer.style.userSelect = "none";
    });
  }
  // Function to close a specific drawer
  function closeDrawer(drawer) {
    drawer.style.transform = "scale(0)";
    drawer.style.opacity = "0";
    drawer.setAttribute("aria-hidden", "true");
    drawer.style.pointerEvents = "none";
    drawer.style.userSelect = "none";
  }
  // Event listeners for opening drawers
  buttons.forEach((button) => {
    button.addEventListener("click", function () {
      // Close all drawers before opening a new one
      closeAllDrawers();
      const direction = button.getAttribute("data-direction");
      const selectedDrawer = document.querySelector(`.drawer.${direction}`);
      // Open the selected drawer
      selectedDrawer.style.transform = "scale(1)";
      selectedDrawer.style.opacity = "1";
      selectedDrawer.setAttribute("aria-hidden", "false");
      // Move focus into the opened drawer
      selectedDrawer.querySelector("a").focus();
      // Enable links in the opened drawer for focus
      const drawerLinks = selectedDrawer.querySelectorAll(".drawer-content a");
      drawerLinks.forEach((link) => {
        link.tabIndex = 0;
      });
      // Enable close button for focus
      const closeBtn = selectedDrawer.querySelector(".close-button");
      closeBtn.tabIndex = 0;
      // Enable pointer events and user selection for the opened drawer
      selectedDrawer.style.pointerEvents = "auto";
      selectedDrawer.style.userSelect = "auto";
      // Add class to body to enable overlay and borders
      document.body.classList.add("drawer-open");
      // Trap focus inside the opened drawer
      selectedDrawer.addEventListener("keydown", trapFocus);
    });
  });
  // Event listeners for closing drawers
  closeButton.forEach((button) => {
    button.addEventListener("click", function () {
      const drawer = button.closest(".drawer");
      // Find the button that triggered the opening of this drawer
      const associatedButton = document.querySelector(
        `.drawer-button[data-direction="${drawer.classList[1]}"]`
      );
      // Close the drawer
      closeDrawer(drawer);
      // Disable links in the closed drawer for focus
      const drawerLinks = drawer.querySelectorAll(".drawer-content a");
      drawerLinks.forEach((link) => {
        link.tabIndex = -1;
      });
      // Disable close button for focus
      button.tabIndex = -1;
      // Move focus back to the associated button that triggered opening this drawer
      associatedButton.focus();
      // Remove class from body to disable overlay and borders
      document.body.classList.remove("drawer-open");
      // Remove focus trap when drawer is closed
      drawer.removeEventListener("keydown", trapFocus);
    });
  });
  // Log clicked element for debugging
  document.addEventListener("click", function (event) {
    console.log("Clicked Element:", event.target);
  });
  // Function to trap focus inside the opened drawer
  function trapFocus(event) {
    const focusableElements = event.currentTarget.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    const firstFocusable = focusableElements[0];
    const lastFocusable =
      focusableElements[focusableElements.length - 1];
    if (event.key === "Tab") {
      if (
        event.shiftKey &&
        document.activeElement === firstFocusable
      ) {
        lastFocusable.focus();
        event.preventDefault();
      } else if (
        !event.shiftKey &&
        document.activeElement === lastFocusable
      ) {
        firstFocusable.focus();
        event.preventDefault();
      }
    }
  }
});

				
			

Why use drawer components?

Drawer components in web development offer an effective way to present additional content, options, or navigation without occupying constant screen space. These components typically slide in or out from the edge of the screen, providing users with quick access to supplementary information or functionalities. From an accessibility standpoint, well-implemented drawer components can enhance user experience by allowing users to focus on the main content while accessing secondary or contextual information when needed. They ensure that the content within the drawer is easily navigable and perceivable by all users, including those relying on assistive technologies. Additionally, drawer components contribute to a more organized and clutter-free interface, maintaining a balance between accessibility and functionality by presenting content in an unobtrusive yet accessible manner. They serve as a versatile tool for optimizing screen real estate and accommodating diverse user needs without compromising accessibility.

What are some accessibility concerns?

  1. Keyboard Accessibility:
    • Concern: Keyboard-only users might face challenges navigating and interacting with drawer content.
    • Mitigation: Ensure keyboard accessibility by allowing users to open, close, and navigate within the drawer using keyboard controls (such as Tab, Arrow keys, and Esc).
  2. Screen Reader Compatibility:
    • Concern: Screen reader users may encounter difficulties perceiving and accessing the drawer’s content.
    • Mitigation: Use proper semantic markup (e.g., <aside> or <nav> for drawers) and provide descriptive labels or headings to assist screen reader users in understanding the purpose and content of the drawer.
  3. Visual and Cognitive Accessibility:
    • Concern: Users with visual impairments or cognitive disabilities might have difficulty identifying the drawer’s presence or understanding its content.
    • Mitigation: Ensure there are clear visual indicators (e.g., icons, labels, or buttons) indicating the presence and functionality of the drawer. Maintain sufficient color contrast and provide text alternatives where necessary.
  4. Focus Management and Trap:
    • Concern: Users might get “trapped” within the drawer, unable to navigate back to the main content.
    • Mitigation: Implement proper focus management to ensure that keyboard focus moves appropriately between the main content and the drawer. Avoid trapping users within the drawer by allowing easy navigation in and out of the drawer.
  5. Mobile and Touch Accessibility:
    • Concern: Drawer interactions might pose challenges for touch device users or smaller screens.
    • Mitigation: Optimize drawer interactions for touch devices, providing large enough touch targets and intuitive gestures for opening and closing the drawer.

Tested using
Chrome
with NVDA
Firefox
with NVDA


  • W3C SVG
    1.3.1

    Info and Relationships
    ( level A )
    :
    Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text.

  • W3C SVG
    2.1.1

    Keyboard
    ( level A )
    :
    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.

  • W3C SVG
    2.1.2

    No Keyboard Trap
    ( level A )
    :
    If keyboard focus can be moved to a component of the page using a keyboard interface, then focus can be moved away from that component using only a keyboard interface, and, if it requires more than unmodified arrow or tab keys or other standard exit methods, the user is advised of the method for moving focus away.

  • W3C SVG
    2.4.3

    Focus Order
    ( level A )
    :
    If a Web page can be navigated sequentially and the navigation sequences affect meaning or operation, focusable components receive focus in an order that preserves meaning and operability.

Select Components

Standard Select Demo

				
					
<!-- Container for the Single Select -->
<div class="select-container">
    <!-- Label for the Single Select -->
    <label for="singleSelect">Single Select</label>
    <!-- Single Select element -->
    <select id="singleSelect" class="single-select" aria-label="Single Select" role="listbox">
        <!-- Options for single select -->
        <option value="option1">Option 1</option>
        <option value="option2">Option 2</option>
        <!-- Add more options here -->
    </select>
</div>

				
			
				
					
/* Set the brand colors */
:root {
  --main-color: #005e86;
  --secondary-color: #99b4c0;
  --accent-color: #211224;
}

/* Basic styles */
body {
  font-family: 'Barlow', sans-serif;
}

/* Select rows and containers */
.select-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  margin-bottom: 20px;
}

.select-container {
  width: calc(40% - 10px); /* Adjust as needed */
  margin-bottom: 10px;
  position: relative;
  box-sizing: border-box;
}

/* Select styling */
select,
input[type="text"] {
  width: calc(100% - 2px);
  padding: 8px;
  border: 1px solid var(--main-color);
  border-radius: 5px;
  font-family: inherit;
  font-size: 14px;
}
				
			

Multi-Select Demo

				
					
<!-- Container for the Multi-Select -->
<div class="select-container">
    <!-- Label for the Multi-Select -->
    <label for="multiSelect">Multi Select</label>
    <!-- Multi-Select element -->
    <select id="multiSelect" class="multi-select" aria-label="Multi Select" multiple="multiple" role="listbox">
        <!-- Options for multi-select -->
        <option value="option1">Option 1</option>
        <option value="option2">Option 2</option>
        <option value="option3">Option 3</option>
        <option value="option4">Option 4</option>
        <option value="option5">Option 5</option>
        <!-- Add more options here -->
    </select>
</div>
				
			
				
					
/* Set the brand colors */
:root {
  --main-color: #005e86;
  --secondary-color: #99b4c0;
  --accent-color: #211224;
}

/* Basic styles */
body {
  font-family: 'Barlow', sans-serif;
}

/* Select rows and containers */
.select-row {
  display: flex;
  justify-content: space-between;
  margin-bottom: 20px;
}

.select-container {
  width: 45%;
  position: relative;
}

/* Select styling */
select, input[type="text"] {
  width: 100%;
  padding: 8px;
  border: 1px solid var(--main-color);
  border-radius: 5px;
  font-family: inherit;
  font-size: 14px;
}

/* Styling for single select */
.single-select {
  /* Add specific styles if needed */
}

/* Styling for multi-select */
.multi-select {
}

/* Styling for searchable select */
.searchable-select {
  position: relative;
}

.searchable-select ul {
  list-style: none;
  padding: 0;
  margin: 0;
  border: 1px solid var(--main-color);
  border-radius: 5px;
  position: absolute;
  top: 100%;
  left: 0;
  background-color: #fff;
  z-index: 1;
  display: none;
  width: 50%;
}

.searchable-select li {
  padding-left: 10px;
  padding-top: 5px;
  padding-bottom: 5px;
}

/* Styling for tag-based select */
.tag-select {
  display: flex;
  flex-wrap: wrap;
  border: 1px solid var(--main-color);
  border-radius: 5px;
  padding: 5px;
}

/* Animation for appearing and fading submenus */
@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes fadeOut {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}
				
			
				
					
// Select elements
const multiSelect = document.getElementById('multiSelect'); // Reference to the multi-select element
const tagSelect = document.querySelector('.tag-select'); // Reference to the tag-based select container

// Function to update the selected options in the tag-based select
function updateTagSelect(selectedOptions) {
  tagSelect.innerHTML = ''; // Clear previous tags

  // Create tag elements for each selected option and append them to the tag-based select container
  selectedOptions.forEach(option => {
    const tag = document.createElement('span');
    tag.textContent = option; // Set the text content to the selected option
    tag.classList.add('tag'); // Apply the 'tag' class to the created tag element
    tagSelect.appendChild(tag); // Add the tag element to the tag-based select container
  });
}

// Event listener for the multi-select
multiSelect.addEventListener('change', () => {
  // Get an array of values from the selected options in the multi-select
  const selectedOptions = Array.from(multiSelect.selectedOptions).map(option => option.value);
  // Update the tag-based select with the newly selected options
  updateTagSelect(selectedOptions);
});

// Event listener for the tag-based select
tagSelect.addEventListener('click', (event) => {
  // Check if the clicked element is a tag
  if (event.target.classList.contains('tag')) {
    const clickedTag = event.target.textContent; // Get the text content of the clicked tag
    const options = Array.from(multiSelect.options); // Get an array of options in the multi-select

    // Toggle selection for the corresponding option in the multi-select based on the clicked tag
    options.forEach(option => {
      if (option.value === clickedTag) {
        option.selected = !option.selected; // Toggle the selection state of the option
      }
    });

    // Update the tag-based select based on the updated selections in the multi-select
    const selectedOptions = Array.from(multiSelect.selectedOptions).map(option => option.value);
    updateTagSelect(selectedOptions);
  }
});

				
			

Searchable Select Demo

				
					
<div class="select-container">
  <!-- Label for the searchable select -->
  <label for="searchableSelect">Searchable Select</label>
  <!-- Container for the searchable select -->
  <div class="searchable-select" role="combobox" aria-expanded="false" aria-haspopup="listbox">
    <!-- Input field for user search with accessibility attributes -->
    <input
      type="text"
      id="searchableSelect"
      aria-autocomplete="list"
      aria-controls="searchableOptions"
      placeholder="Search..."
    />
    <!-- List to display options for the searchable select -->
    <ul id="searchableOptions" class="options-list" role="listbox">
      <!-- Options for the searchable select will be dynamically populated here -->
    </ul>
  </div>
</div>

				
			
				
					
/* Set the brand colors */
:root {
  --main-color: #005e86;
  --secondary-color: #99b4c0;
  --accent-color: #211224;
}

/* Basic styles */
body {
  font-family: 'Barlow', sans-serif;
}

/* Select rows and containers */
.select-row {
  display: flex;
  justify-content: space-between;
  margin-bottom: 20px;
}

.select-container {
  width: 45%;
  position: relative;
}

/* Select styling */
select, input[type="text"] {
  width: 50%;
  padding: 8px;
  border: 1px solid var(--main-color);
  border-radius: 5px;
  font-family: inherit;
  font-size: 14px;
}

/* Styling for single select */
.single-select {
  /* Add specific styles if needed */
}

/* Styling for multi-select */
.multi-select {
  /* Add specific styles if needed */
}

/* Styling for searchable select */
.searchable-select {
  position: relative;
}

.searchable-select ul {
  list-style: none;
  padding: 0;
  margin: 0;
  border: 1px solid var(--main-color);
  border-radius: 5px;
  position: absolute;
  top: 100%;
  left: 0;
  background-color: #fff;
  z-index: 1;
  display: none;
  width: 50%;
}

.searchable-select li {
  padding-left: 10px;
  padding-top: 5px;
  padding-bottom: 5px;
}

/* Styling for tag-based select */
.tag-select {
  display: flex;
  flex-wrap: wrap;
  border: 1px solid var(--main-color);
  border-radius: 5px;
  padding: 5px;
}

/* Animation for appearing and fading submenus */
@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes fadeOut {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}
				
			
				
					
// Sample array of options for the searchable select
const allOptions = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry', 'Fig', 'Grape'];

// Function to populate options in the searchable select based on user input
function populateOptions(searchText) {
  const filteredOptions = allOptions.filter(option => option.toLowerCase().includes(searchText.toLowerCase()));
  const optionsList = document.getElementById('searchableOptions');
  optionsList.innerHTML = ''; // Clear previous options

  if (filteredOptions.length > 0) {
    filteredOptions.forEach(option => {
      const li = document.createElement('li');
      li.textContent = option;
      li.setAttribute('role', 'option');
      li.setAttribute('tabindex', '0'); // Make options focusable
      li.addEventListener('click', () => {
        searchableInput.value = option; // Set input value on click
        optionsList.style.display = 'none'; // Hide options after selection
      });
      optionsList.appendChild(li);
    });
    optionsList.style.display = 'block'; // Show the options
  } else {
    optionsList.style.display = 'none'; // Hide the options if no matches
  }
}

// Select elements
const searchableInput = document.getElementById('searchableSelect');
const optionsList = document.getElementById('searchableOptions');

// Event listeners
searchableInput.addEventListener('input', (event) => {
  const searchText = event.target.value;
  populateOptions(searchText);
});

// Show options dropdown on focus or click
searchableInput.addEventListener('focus', () => {
  const searchText = searchableInput.value;
  populateOptions(searchText);
});

searchableInput.addEventListener('click', () => {
  const searchText = searchableInput.value;
  populateOptions(searchText);
});

// Handle option selection on keyboard navigation
optionsList.addEventListener('keydown', (event) => {
  if (event.key === 'Enter' || event.key === ' ') {
    event.preventDefault();
    const selectedOption = event.target.textContent;
    searchableInput.value = selectedOption; // Set input value on selection
    optionsList.style.display = 'none'; // Hide options after selection
  }
});

// Close the options list if user clicks outside
document.addEventListener('click', (event) => {
  if (!event.target.closest('.searchable-select')) {
    optionsList.style.display = 'none';
  }
}); 
				
			

Tag/Chip Select

				
					
<div class="select-container">
  <!-- Label for the tag-based select -->
  <label for="tagSelect">Tag-based Select</label>
  <!-- Input field for displaying selected tags -->
  <input type="text" id="tagInput" readonly>
  <!-- Container for the tag-based select -->
  <div class="tag-select" role="listbox">
    <!-- Container to display selected tags as 'chips' -->
    <div class="tag-container" id="tagContainer"></div>
  </div>
</div>

				
			
				
					
/* Set the brand colors */
:root {
  --main-color: #005e86;
  --secondary-color: #99b4c0;
  --accent-color: #211224;
}

/* Basic styles */
body {
  font-family: 'Barlow', sans-serif;
}

/* Select container */
.select-container {
  width: 200px; /* Adjust as needed */
}

/* Label style */
.select-container label {
  display: block;
  margin-bottom: 5px;
}

/* Tag-select styles */
.tag-select {
  position: relative;
}

#tagInput {
  width: calc(100% - 2px);
  padding: 8px;
  border: 1px solid var(--main-color);
  border-radius: 5px;
  font-family: inherit;
  font-size: 14px;
  margin-bottom: 5px;
}

/* Ensure the list items display vertically */
.tag-select .tag {
  display: block; /* Make tags display vertically */
  padding: 8px;
  cursor: pointer;
}

.tag-select .tag:hover {
  background-color: #f0f0f0;
}
				
			
				
					
document.addEventListener('DOMContentLoaded', () => {
  const allOptionsForTags = ['Tag1', 'Tag2', 'Tag3', 'Tag4', 'Tag5', 'Tag6', 'Tag7'];
  const tagInput = document.getElementById('tagInput');
  const tagContainer = document.getElementById('tagContainer');

  const selectedTags = new Set(); // To store selected tags

  // Function to populate options in the tag-based select
  function populateOptionsForTags() {
    tagContainer.innerHTML = ''; // Clear previous tags
    allOptionsForTags.forEach(option => {
      const tag = document.createElement('div');
      tag.textContent = option;
      tag.classList.add('tag');
      tag.setAttribute('tabindex', '0'); // Make tags focusable
      tagContainer.appendChild(tag);
    });
  }

  // Event listener to display the tag dropdown when input is focused/clicked
  tagInput.addEventListener('focus', () => {
    populateOptionsForTags();
    tagContainer.style.display = 'block';
  });

  // Event listener to hide the tag dropdown when input is blurred
  tagInput.addEventListener('blur', (event) => {
    // Delay hiding the dropdown to allow click events to be detected on tags
    setTimeout(() => {
      if (!tagContainer.contains(event.relatedTarget)) {
        tagContainer.style.display = 'none';
      }
    }, 100); // Adjust the delay as needed
  });

  // Event listener for selecting a tag from the dropdown using click or Enter key
  tagContainer.addEventListener('click', (event) => {
    if (event.target.classList.contains('tag')) {
      const clickedTag = event.target.textContent;
      if (!selectedTags.has(clickedTag)) {
        selectedTags.add(clickedTag);
        tagInput.value += `${clickedTag} `;
      } else {
        selectedTags.delete(clickedTag);
        tagInput.value = tagInput.value.replace(clickedTag + ' ', '');
      }
      tagInput.focus(); // Return focus to the input field
    }
  });

  // Event listener for keyboard navigation within the tag dropdown
  tagContainer.addEventListener('keydown', (event) => {
    const focusedTag = document.activeElement;

    if (event.key === 'Enter' && focusedTag.classList.contains('tag')) {
      const clickedTag = focusedTag.textContent;
      if (!selectedTags.has(clickedTag)) {
        selectedTags.add(clickedTag);
        tagInput.value += `${clickedTag} `;
        tagInput.focus(); // Return focus to the input field
      } else {
        selectedTags.delete(clickedTag);
        tagInput.value = tagInput.value.replace(clickedTag + ' ', '');
      }
    }
  });

  // Event listener for adding tags via Enter key
  tagInput.addEventListener('keydown', function (event) {
    if (event.key === 'Enter' || event.keyCode === 13) {
      event.preventDefault();
      const value = this.value.trim();

      if (value !== '') {
        selectedTags.add(value);
        tagInput.value += `${value} `;
        this.value = ''; // Clear input after adding the tag
        tagInput.focus(); // Return focus to the input field
      }
    }
  });
});
				
			

Why use a select element?

Select elements are fundamental components in web development that offer users a structured and intuitive way to make choices or selections from a list of options. These elements provide a dropdown menu displaying multiple choices, allowing users to pick one or more options based on their needs. From an accessibility perspective, select elements offer inherent benefits by enabling keyboard navigation, ensuring compatibility with screen readers, and supporting various assistive technologies. They contribute to a more inclusive user experience by allowing users with motor impairments or visual disabilities to interact with the interface effectively. Select elements also ensure consistency in user interaction patterns, facilitating a familiar and predictable method for users to navigate and make selections across different web applications and devices. Overall, the proper implementation of select elements aligns with accessibility standards, enhancing usability and accessibility for all users across diverse browsing environments.

What are some accessibility concerns?

  1. Keyboard Accessibility:
    • Concern: Keyboard-only users might encounter difficulties navigating and selecting options within the dropdown.
    • Mitigation: Ensure the select element and its options are accessible via keyboard navigation (Tab/Arrow keys) without relying solely on mouse interaction.
  2. Screen Reader Compatibility:
    • Concern: Screen reader users may struggle to perceive or interact with the select element’s options.
    • Mitigation: Use proper semantic markup (e.g., <select>, <option>) and associated labels (using <label> or aria-labelledby) for screen readers to interpret and convey the select’s purpose and options accurately.
  3. Visual and Cognitive Accessibility:
    • Concern: Users with visual impairments or cognitive disabilities might find it challenging to perceive or comprehend the options.
    • Mitigation: Provide sufficient contrast between the select element and its options to ensure visibility. Utilize clear labels or additional visual cues (like borders or icons) to aid comprehension.
  4. Mobile and Touch Accessibility:
    • Concern: Select elements might be challenging to interact with on touch devices or smaller screens.
    • Mitigation: Optimize the select element’s design for touch devices, ensuring sufficient touch target sizes and intuitive interactions for users with limited dexterity.
  5. ARIA Attributes:
    • Concern: Proper usage of ARIA attributes to enhance functionality and convey the select element’s state to assistive technologies.
    • Mitigation: Implement appropriate ARIA attributes (aria-expanded, aria-selected, etc.) to enhance the select element’s functionality and convey its state to assistive technologies.
  6. Error Handling and Validation:
    • Concern: Clear error messages and validation cues for users encountering issues with selecting options or during submission.
    • Mitigation: Ensure clear error messages and validation cues for users if there are issues with selecting options or if errors occur during submission.
Tested using
Chrome
with NVDA
Firefox
with NVDA
  • W3C SVG
    1.3.1
    Info and Relationships ( level A ) :
    Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text.
  • W3C SVG
    2.1.1
    Keyboard ( level A ) :
    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.
  • W3C SVG
    2.1.2
    No Keyboard Trap ( level A ) :
    If keyboard focus can be moved to a component of the page using a keyboard interface, then focus can be moved away from that component using only a keyboard interface, and, if it requires more than unmodified arrow or tab keys or other standard exit methods, the user is advised of the method for moving focus away.
  • W3C SVG
    2.4.3
    Focus Order ( level A ) :
    If a Web page can be navigated sequentially and the navigation sequences affect meaning or operation, focusable components receive focus in an order that preserves meaning and operability.
  • W3C SVG
    4.1.2
    Name, Role, Value ( level A ) :
    For all user interface components (including but not limited to: form elements, links and components generated by scripts), the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to user agents, including assistive technologies.

Toast Messages

Demo

				
					
<!-- Container for displaying toast messages with accessibility attributes -->
<div class="toast-container" role="status" aria-live="polite" aria-atomic="true"></div>

<!-- Controls section for buttons and select element -->
<div class="controls">
  <!-- Container for the select element -->
  <div class="select-container">
    <!-- Label for the select element -->
    <label for="durationSelect" class="select-label">Select Toast Duration</label>
    <!-- Select element for choosing toast duration -->
    <select id="durationSelect" aria-label="Select toast duration">
      <!-- Options for different toast durations -->
      <option value="10000">10 Seconds</option>
      <option value="100000">100 Seconds</option>
      <option value="0">Toasts do not fade</option>
    </select>
  </div>

  <!-- Button to trigger an error toast message -->
  <button id="errorButton" class="error-button">Trigger Error Toast</button>
  <!-- Button to trigger a notification toast message -->
  <button id="notificationButton" class="notification-button">Trigger Notification Toast</button>
  <!-- Button to trigger a system update toast message -->
  <button id="updateButton" class="update-button">Trigger System Update Toast</button>
</div>
				
			
				
					
/* Importing fonts */
@import url("https://fonts.googleapis.com/css2?family=Barlow&family=Oswald&display=swap");

/* Container for the select element */
.select-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding-right: 20px;
}

/* Label above the select element */
.select-label {
  font-family: "Barlow", sans-serif;
  font-size: 16px;
  color: #333;
  margin-bottom: 10px;
}

/* Main container for toast messages */
.toast-container {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 1000;
  font-family: "Barlow", sans-serif;
}

/* Buttons container */
.controls {
  display: flex;
  justify-content: center;
  align-items: center;
  margin-top: 20px;
  font-family: "Barlow", sans-serif;
}

/* Styling for different buttons */
.error-button,
.notification-button,
.update-button {
  /* Clear initial background and border */
  background: none;
  border: 2px solid transparent;
  border-radius: 8px;
  padding: 12px 20px; /* Increased padding for larger buttons */
  margin: 5px;
  cursor: pointer;
  transition: all 0.3s ease; /* Smooth transition on hover */
  font-size: 16px; /* Increased font size */
}

/* Hover styles for buttons */
.error-button:hover,
.notification-button:hover,
.update-button:hover {
  opacity: 0.8; /* Reduced opacity on hover */
}

/* Error button styles */
.error-button {
  color: #8c0000; /* Darker text */
  border-color: #ff4d4d; /* Red border */
  background-color: #ffe6e6; /* Lighter red background */
}

/* Notification button styles */
.notification-button {
  color: #004d99; /* Darker text */
  border-color: #007acc; /* Blue border */
  background-color: #cce0ff; /* Lighter blue background */
}

/* Update button styles */
.update-button {
  color: #804d00; /* Darker text */
  border-color: #ff9933; /* Orange border */
  background-color: #ffd699; /* Lighter orange background */
}

/* Individual toast message styling */
.toast {
  position: relative;
  max-width: 200px;
  width: 80%; /* Allow the toast to take full width within the max-width */
  padding: 20px;
  border-radius: 8px;
  margin-bottom: 20px;
  word-wrap: break-word; /* Allow long words to break and wrap */
  word-break: break-word; /* Break words when necessary */
  opacity: 0;
  animation: slideFadeIn 0.5s ease forwards; /* Animation for toast appearance */
}

/* Close button style within the toast */
.toast button.close-button {
  /* Positioning */
  position: absolute;
  top: 5px;
  right: 5px;
  /* Button styling */
  background: none;
  border: none;
  cursor: pointer;
  font-size: 14px; /* Adjusted font size for smaller close button */
  color: #; /* Missing color value */
  width: 20px; /* Reduced width */
  height: 20px; /* Reduced height */
  text-align: center;
}

/* Hover styles for close button */
.toast button.close-button:hover {
  color: #000; /* Change color on hover for better contrast */
}

/* Styling for error toast */
.error-toast {
  border: 1px solid #ff4d4d; /* Red outline */
  background-color: #ffe6e6; /* Lighter red interior */
  color: #8c0000; /* Darker text for contrast */
}

/* Styling for notification toast */
.notification-toast {
  border: 1px solid #007acc; /* Blue outline */
  background-color: #cce0ff; /* Lighter blue interior */
  color: #004d99; /* Darker text for contrast */
}

/* Styling for update toast */
.update-toast {
  border: 1px solid #ff9933; /* Orange outline */
  background-color: #ffd699; /* Lighter orange interior */
  color: #804d00; /* Darker text for contrast */
}

/* Select element styles */
#durationSelect {
  font-family: "Barlow", sans-serif;
  font-size: 16px;
  padding: 12px 20px;
  border: 2px solid transparent;
  border-radius: 8px;
  margin-right: 5px;
  cursor: pointer;
  transition: all 0.3s ease;
}

/* Hover styles for select element */
#durationSelect:hover {
  opacity: 0.8;
}

/* Keyframe animation for toast appearance */
@keyframes slideFadeIn {
  0% {
    opacity: 0;
    transform: translateY(-50px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}
				
			
				
					
// Wait for the DOM content to be fully loaded
document.addEventListener("DOMContentLoaded", function () {
  // Get necessary elements from the DOM
  const durationSelect = document.getElementById("durationSelect");
  const toastContainer = document.querySelector(".toast-container");

  // Function to create toast messages
  function createToast(message, durationClass) {
    console.log(
      `Creating toast with message: "${message}" and duration class: "${durationClass}"`
    );

    // Get all existing toast messages
    const existingToasts = document.querySelectorAll(".toast");

    // Create a new toast message element
    const newToast = document.createElement("div");
    newToast.classList.add("toast", durationClass);

    // Create close button for the toast message
    const closeButton = document.createElement("button");
    closeButton.classList.add("close-button");
    closeButton.innerHTML = "X";
    closeButton.setAttribute("aria-label", "Close toast message");
    closeButton.addEventListener("click", function () {
      newToast.remove();
    });

    // Set the message content and append the close button
    newToast.innerText = message;
    newToast.appendChild(closeButton);

    // Add the new toast message at the beginning of the container
    toastContainer.prepend(newToast);

    // Apply animation for displaying new toast messages
    existingToasts.forEach((toast) => {
      toast.style.animation = "slideDown 0.5s ease forwards";
      toast.style.transform = "translateY(" + (toast.offsetHeight + 20) + "px)";
    });

    // Remove animation after a set time
    setTimeout(() => {
      existingToasts.forEach((toast) => {
        toast.style.animation = "";
        toast.style.transform = "";
      });
    }, 500);

    // Handle the duration for toast messages
    const selectedDuration = parseInt(durationSelect.value);
    if (selectedDuration === 0) {
      console.log("Infinite duration set for toast");
      return; // For infinite duration, don't set a timeout for removal
    }

    console.log(`Toast duration set for ${selectedDuration} milliseconds`);

    // Set a timeout to remove the toast message after the specified duration
    setTimeout(() => {
      newToast.remove();
    }, selectedDuration);
  }

  // Event listener for changes in the duration select element
  durationSelect.addEventListener("change", function () {
    const toasts = document.querySelectorAll(".toast");
    const durationClass = this.value === "0" ? "no-fade" : "";

    // Apply the selected duration class to existing toast messages
    toasts.forEach((toast) => {
      if (!toast.classList.contains("no-fade")) {
        toast.classList.remove("default", "10-seconds");
        toast.classList.add(durationClass);
      }
    });
  });

  // Event listeners for creating different types of toast messages
  document.getElementById("errorButton").addEventListener("click", function () {
    createToast(
      "This is an example toast message for a software or website process error.",
      "error-toast"
    );
  });

  document
    .getElementById("notificationButton")
    .addEventListener("click", function () {
      createToast(
        "This is an example toast message for a user notification.",
        "notification-toast"
      );
    });

  document
    .getElementById("updateButton")
    .addEventListener("click", function () {
      createToast(
        "This is an example toast message for a system update.",
        "update-toast"
      );
    });
});
				
			

Why use toast messages?

In web development, toast messages serve as unobtrusive, temporary notifications that deliver essential information to users without disrupting their workflow. These messages are particularly valuable as they provide immediate feedback on actions, updates, or alerts, enhancing the user experience by keeping users informed about critical events or changes. From an accessibility standpoint, well-designed toast messages offer benefits by providing clear and concise information that is easily perceivable by all users, including those utilizing assistive technologies. When properly implemented with appropriate ARIA attributes and keyboard accessibility, toast messages ensure that users with disabilities receive timely and accessible notifications, contributing to a more inclusive and user-friendly web experience.

What are some accessibility concerns?

  1. Visibility and Readability:
    • Concern: Toast messages might appear abruptly or be difficult to notice, especially for users with visual impairments.
    • Mitigation: Ensure adequate contrast between the toast message and its background for readability. Use visually distinct colors, text size, and animations to draw attention without being overwhelming. Provide sufficient time for users to read and comprehend the message.
  2. Keyboard Accessibility:
    • Concern: Keyboard-only users might have difficulties interacting with or dismissing toast messages.
    • Mitigation: Implement keyboard accessibility by allowing users to navigate to and dismiss toast messages using keyboard controls (e.g., Tab, Enter/Space, and Esc keys). Provide focus management to ensure that users can interact with the message content and dismiss it easily.
  3. Screen Reader Compatibility:
    • Concern: Screen reader users might not receive or understand the content of the toast messages.
    • Mitigation: Ensure toast messages are programmatically available to assistive technologies by using appropriate ARIA roles and attributes. Announce the appearance of new messages and their content using live regions or ARIA alerts.
  4. Timing and Dismissal:
    • Concern: Toast messages might disappear too quickly, making it challenging for users to read or act upon them.
    • Mitigation: Allow sufficient time for users to perceive and comprehend the message. Provide options for users to dismiss or interact with the message, allowing them to control the duration of its display.
  5. Focus and Interruptions:
    • Concern: Toast messages might disrupt users’ focus or interfere with screen reader announcements.
    • Mitigation: Avoid abrupt changes or automatic updates that might interrupt users. Ensure toast messages do not steal focus and provide options to pause or dismiss animations for users with attention-related disabilities.
Tested using
Chrome
with NVDA
Firefox
with NVDA
  • W3C SVG
    1.3.1
    Info and Relationships ( level A ) :
    Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text.
  • W3C SVG
    2.1.1
    Keyboard ( level A ) :
    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.
  • W3C SVG
    2.2.2
    Pause, Stop, Hide ( level A ) :
    For moving, blinking, scrolling, or auto-updating information, all of the following are true:
    Moving, blinking, scrolling: For any moving, blinking or scrolling information that (1) starts automatically, (2) lasts more than five seconds, and (3) is presented in parallel with other content, there is a mechanism for the user to pause, stop, or hide it unless the movement, blinking, or scrolling is part of an activity where it is essential, and
    Auto-updating: For any auto-updating information that (1) starts automatically and (2) is presented in parallel with other content, there is a mechanism for the user to pause, stop, or hide it or to control the frequency of the update unless the auto-updating is part of an activity where it is essential.
  • W3C SVG
    4.1.3
    Status Messages ( level AA ) :
    In content implemented using markup languages, status messages can be programmatically determined through role or properties such that they can be presented to the user by assistive technologies without receiving focus.

Navigation Dropdown

Demo

				
					
<nav class="navigation" aria-label="main navigation menu">
  <ul class="nav-list">
    <li class="current"><a href="#" aria-current="page">Home</a></li>
    <li class="has-submenu submenu-1">
      <a href="#">Shop</a>
      <ul class="submenu">
        <li><a href="#">Submenu Link 1</a></li>
        <li><a href="#">Submenu Link 2</a></li>
        <li><a href="#">Submenu Link 3</a></li>
      </ul>
    </li>
    <li class="has-submenu submenu-2">
      <a href="#">Blog</a>
      <ul class="submenu">
        <li><a href="#">Submenu Link 4</a></li>
        <li><a href="#">Submenu Link 5</a></li>
        <li><a href="#">Submenu Link 6</a></li>
      </ul>
    </li>
    <li><a href="#">About</a></li>
  </ul>
</nav>
				
			
				
					
@import url('https://fonts.googleapis.com/css2?family=Barlow&family=Oswald&display=swap');

body {
  font-family: 'Barlow', sans-serif;
  background-color: #fff;
  color: #005E86;
}

.navigation {
  width: 75%;
  margin: auto;
  font-size: 1.2em;
}

.nav-list {
  list-style: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 0;
}

.nav-list a {
  text-decoration: none;
  color: #005E86;
  border-bottom: 3px solid transparent;
  transition: border-color 0.3s ease-in-out;
}

.nav-list a:hover,
.nav-list a:focus {
  border-color: #211224;
}

.has-submenu {
  position: relative;
}

.submenu {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  background-color: #fff;
  border: 1px solid #99b4c0;
  padding: 10px;
  width: 120px;
  font-size: .9em;
}

.submenu li {
  list-style: none;
  margin-bottom: 5px;
}

.submenu a {
  color: #211224;
  text-decoration: none;
}

/* Show submenus when the 'visible' class is added */
.submenu.visible {
  display: block;
}

.current {
  border-bottom: 3px solid #211224;
}
				
			
				
					
// Execute when the DOM content is loaded
window.addEventListener("DOMContentLoaded", () => {
  // Select all elements with class 'has-submenu' that are direct children of an <a> tag
  const subMenus = document.querySelectorAll(".has-submenu > a");

  // Iterate through each 'has-submenu' element
  subMenus.forEach((menuItem) => {
    // Get the parent <li> and its submenu
    const parentLI = menuItem.parentElement;
    const submenu = parentLI.querySelector(".submenu");
    const submenuLinks = submenu.querySelectorAll("a");
    let isSubmenuHovered = false; // Flag to track submenu hover state

    // Function to show the submenu
    function showSubmenu() {
      submenu.classList.add("visible");
      parentLI.setAttribute("aria-expanded", "true");
    }

    // Function to hide the submenu
    function hideSubmenu() {
      if (!isSubmenuHovered) {
        submenu.classList.remove("visible");
        parentLI.setAttribute("aria-expanded", "false");
      }
    }

    // Function to handle backward navigation
    function handleBackwardNavigation(e) {
      const isBackwardTab = e.key === "Tab" && e.shiftKey;
      if (isBackwardTab && submenu.classList.contains("visible")) {
        hideSubmenu();
      }
    }
    
    document.addEventListener('click', (e) => {
      const isClickInsideMenu = menuItem.contains(e.target); // Check if click is inside the menu

      if (!isClickInsideMenu) {
        hideSubmenu();
      }
    });

    // Event listeners for showing and hiding submenu on mouse events
    parentLI.addEventListener("mouseenter", showSubmenu);
    parentLI.addEventListener("mouseleave", hideSubmenu);
    submenu.addEventListener("mouseenter", () => {
      isSubmenuHovered = true;
    });
    submenu.addEventListener("mouseleave", () => {
      isSubmenuHovered = false;
    });

    // Event listener to show submenu on focus
    parentLI.addEventListener("focusin", showSubmenu);

    // Event listener to hide submenu on focus out from the last submenu link
    submenuLinks[submenuLinks.length - 1].addEventListener(
      "focusout",
      hideSubmenu
    );

    // Event listener to hide submenu on 'Escape' key press
    parentLI.addEventListener("keydown", (e) => {
      if (e.key === "Escape") {
        hideSubmenu();
      }
    });

    // Event listener for handling backward navigation on 'Shift + Tab' key press
    document.addEventListener("keydown", handleBackwardNavigation);

    // Initialize aria-expanded attribute to false
    parentLI.setAttribute("aria-expanded", "false");
  });

  // Event listener to handle submenu hover when leaving the submenu area
  document.addEventListener("mouseover", (event) => {
    if (event.target.tagName === "BODY") {
      isSubmenuHovered = false;
    }
  });
});
				
			

Why use dropdown menus?

Navigation menus with dropdowns are employed in web development to organize and present site content in a structured and accessible manner. These menus enhance user experience by categorizing information hierarchically, allowing visitors to navigate through a website’s sections or pages efficiently. Dropdown menus provide a compact way to display extensive navigation options without cluttering the interface, promoting a clean and organized design. From an accessibility standpoint, well-designed dropdown menus can improve site accessibility by enabling keyboard navigation, ensuring proper focus management, and offering clear labels and instructions that assist screen reader users. When implemented with accessibility in mind, these menus contribute to a more inclusive browsing experience, accommodating users of various abilities and interaction preferences.

What are some accessibility concerns?

  1. Keyboard Accessibility:
    • Concern: Dropdown menus might be inaccessible or challenging to navigate using a keyboard.
    • Mitigation: Ensure the dropdown menus are keyboard accessible by enabling navigation using the Tab key and ensuring proper focus management. Use ARIA roles and attributes (role=”menu”, role=”menuitem”, etc.) to convey the menu’s structure and allow users to navigate through dropdown options with arrow keys and Enter/Space for selection.
  2. Screen Reader Compatibility:
    • Concern: Screen reader users might have difficulty understanding or accessing the dropdown content.
    • Mitigation: Utilize proper ARIA roles and labels to convey the dropdown’s purpose and relationships between menu items. Ensure that screen readers announce the dropdown’s visibility, state changes, and associated actions accurately.
  3. Visibility and Consistency:
    • Concern: Dropdown menus might not be visually noticeable or consistent across different devices or screen sizes.
    • Mitigation: Ensure dropdown menus are visually distinct and clearly visible, especially for users with low vision. Implement responsive design principles to adapt the dropdown layout for different screen sizes while maintaining consistency and usability.
  4. Focus Management and Interaction:
    • Concern: Users might lose context or encounter issues when navigating through dropdowns.
    • Mitigation: Implement clear and consistent focus styles to indicate the focused item within the dropdown. Use appropriate keyboard interactions to manage dropdown visibility, ensuring users can easily access and navigate within the dropdown without abrupt changes or loss of context.
  5. Touch and Mobile Usability:
    • Concern: Dropdown menus might be challenging to interact with on touch devices or smaller screens.
    • Mitigation: Optimize dropdown interaction for touch devices by providing larger tap areas and considering touch gestures. Ensure dropdowns expand and collapse smoothly without covering crucial content on smaller screens.
Tested using
Chrome
with NVDA
Firefox
with NVDA
  • W3C SVG
    1.3.1
    Info and Relationships ( level A ) :
    Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text.
  • W3C SVG
    2.1.1
    Keyboard ( level A ) :
    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.
  • W3C SVG
    2.4.3
    Focus Order ( level A ) :
    If a Web page can be navigated sequentially and the navigation sequences affect meaning or operation, focusable components receive focus in an order that preserves meaning and operability.
  • W3C SVG
    4.1.2
    Name, Role, Value ( level A ) :
    For all user interface components (including but not limited to: form elements, links and components generated by scripts), the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to user agents, including assistive technologies.

Pagination Links

Demo

				
					
<div class="pagination-container">
  <div class="results">
    <!-- Results will be dynamically generated here -->
  </div>
  <div class="pagination">
    <ul role="tablist" aria-label="Pagination">
      <li role="presentation"><a aria-label="navigate to results page 1" href="#" role="tab" aria-selected="true">1</a></li>
      <li role="presentation"><a aria-label="navigate to results page 2" href="#" role="tab">2</a></li>
      <li role="presentation"><a aria-label="navigate to results page 3" href="#" role="tab">3</a></li>
      <li role="presentation"><a aria-label="navigate to results page 4" href="#" role="tab">4</a></li>
    </ul>
  </div>
</div>
				
			
				
					
@import url("https://fonts.googleapis.com/css2?family=Barlow&family=Oswald&display=swap");

.pagination-container {
  width: 75%;
  font-family: 'Barlow', sans-serif;
}

.pagination ul {
  list-style: none;
  display: flex;
  justify-content: center;
  padding: 0;
}

.pagination li {
  margin: 0 5px;
}

.pagination a {
  font-family: 'Oswald', sans-serif;
  text-decoration: none;
  color: #005e86;
  padding: 8px 12px;
  border-radius: 4px;
  transition: background-color 0.3s ease;
}

.pagination a:hover {
  background-color: #99b4c0;
}

.pagination a[aria-selected="true"] {
  background-color: #211224;
  color: white;
}

.results {
  width: 75%;
  margin: auto;
  border: 2px #211224 solid;
  border-radius: 10px;
}

.results h3, .results a, .results p {
  margin-left: 15px;
}

.pagination {
  margin-top: 40px;
}
				
			
				
					
const resultsContainer = document.querySelector('.results');
const paginationLinks = document.querySelectorAll('.pagination a');

// Function to generate example results
function generateResults(pageNumber) {
  // Simulating results for the page
  const results = [];
  for (let i = 1; i <= 5; i++) {
    results.push({
      title: `Example Result ${i + (pageNumber - 1) * 5}`,
      text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
      link: '#'
    });
  }
  return results;
}

// Function to display results
function displayResults(pageNumber) {
  resultsContainer.innerHTML = '';
  const results = generateResults(pageNumber);
  results.forEach(result => {
    const resultElement = document.createElement('div');
    resultElement.innerHTML = `
      <h3><a href="${result.link}">${result.title}</a></h3>
      <p>${result.text}</p>
    `;
    resultsContainer.appendChild(resultElement);
  });
}

// Event listeners for pagination links
paginationLinks.forEach((link, index) => {
  link.addEventListener('click', event => {
    event.preventDefault();
    // Update aria-selected attribute for all links
    paginationLinks.forEach((link) => link.setAttribute('aria-selected', 'false'));
    // Set aria-selected attribute to 'true' for the clicked link
    link.setAttribute('aria-selected', 'true');
    // Display results for the selected page
    displayResults(index + 1);
  });
});

// Set aria-selected attribute on the first pagination link initially
paginationLinks[0].setAttribute('aria-selected', 'true');

// Initial display of results for the first page
displayResults(1);
				
			

Why use pagination links?

Pagination links in web development serve as navigational aids, breaking down content into manageable segments and allowing users to navigate through various pages of information, search results, or listings. They provide a structured and organized way to access content, preventing information overload by displaying a limited set of items per page. However, considerations for accessibility are pivotal when implementing pagination links. Users relying on assistive technologies or those with motor impairments might face challenges when interacting with pagination. Accessibility concerns revolve around ensuring keyboard operability, providing clear and descriptive link text, and conveying the pagination’s structure and current location to screen reader users. Incorporating appropriate HTML markup, utilizing ARIA attributes for conveying roles and relationships, and designing for keyboard navigation aid in creating accessible pagination links.

What are some accessibility concerns?

  1. Keyboard Accessibility:
    • Concern: Users relying on keyboards may face difficulties navigating pagination links.
    • Mitigation: Ensure pagination links are keyboard accessible by allowing users to navigate and activate them using keyboard controls (e.g., Tab and Enter). Use proper HTML semantics (<a> tags) and ensure focus visibility on active links.
  2. Focus Indication:
    • Concern: Inadequate focus indication may confuse users with visual impairments regarding the active link.
    • Mitigation: Ensure clear and visible focus styles on active pagination links, providing a distinct visual cue for users navigating with keyboards or screen readers.
  3. Consistent Identification:
    • Concern: Users might struggle to understand the pagination’s purpose and sequence.
    • Mitigation: Provide clear and consistent labeling for pagination links. Use ARIA attributes (role=”navigation”, aria-label, or aria-labelledby) to convey the pagination’s purpose and sequence to assistive technologies.
  4. Parsing and Comprehension:
    • Concern: Screen reader users might not comprehend the relationship between pagination links and the content structure.
    • Mitigation: Use semantic HTML markup (<nav>, <ul>, <li>) to structure pagination links. Ensure proper labeling of the links and convey their association with the content using ARIA attributes or landmarks.
  5. Responsive Design and Reflow:
    • Concern: Pagination links might not adapt well to different screen sizes or orientations.
    • Mitigation: Implement responsive design principles, ensuring that pagination links are accessible and usable across various devices and screen sizes without loss of functionality or information.
Tested using
Chrome
with NVDA
Firefox
with NVDA
  • W3C SVG
    1.3.1
    Info and Relationships ( level A ) :
    Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text.
  • W3C SVG
    2.1.1
    Keyboard ( level A ) :
    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.
  • W3C SVG
    2.4.7
    Focus Visible ( level AA ) :
    Any keyboard operable user interface has a mode of operation where the keyboard focus indicator is visible.
  • W3C SVG
    4.1.2
    Name, Role, Value ( level A ) :
    For all user interface components (including but not limited to: form elements, links and components generated by scripts), the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to user agents, including assistive technologies.

Tablist

Demo

Tab 1 Content

Example text for Tab 1...

				
					
<div class="tablist">
  <div role="tablist" aria-label="Sample Tablist" class="tab-headers">
    <button id="tab1" role="tab" aria-controls="panel1" aria-selected="true" tabindex="0">Tab 1</button>
    <button id="tab2" role="tab" aria-controls="panel2" tabindex="0">Tab 2</button>
    <button id="tab3" role="tab" aria-controls="panel3" tabindex="0">Tab 3</button>
    <button id="tab4" role="tab" aria-controls="panel4" tabindex="0">Tab 4</button>
  </div>
  <div class="tab-panels">
    <div id="panel1" role="tabpanel" aria-labelledby="tab1">
      <!-- Content for Tab 1 -->
      <h2>Tab 1 Content</h2>
      <p>Example text for Tab 1...</p>
      <button>Button 1</button>
      <!-- Additional content for Tab 1 -->
    </div>
    <div aria-hidden="true" id="panel2" role="tabpanel" aria-labelledby="tab2" hidden>
      <!-- Content for Tab 2 -->
      <h2>Tab 2 Content</h2>
      <p>Example text for Tab 2...</p>
      <button>Button 2</button>
      <!-- Additional content for Tab 2 -->
    </div>
    <div aria-hidden="true" id="panel3" role="tabpanel" aria-labelledby="tab3" hidden>
      <!-- Content for Tab 3 -->
      <h2>Tab 3 Content</h2>
      <p>Example text for Tab 3...</p>
      <button>Button 3</button>
      <!-- Additional content for Tab 3 -->
    </div>
    <div aria-hidden="true" id="panel4" role="tabpanel" aria-labelledby="tab4" hidden>
      <!-- Content for Tab 4 -->
      <h2>Tab 4 Content</h2>
      <p>Example text for Tab 4...</p>
      <button>Button 4</button>
      <!-- Additional content for Tab 4 -->
    </div>
  </div>
</div>
				
			
				
					
@import url("https://fonts.googleapis.com/css2?family=Barlow&family=Oswald&display=swap");

/* Tablist container */
.tablist {
  display: flex;
  flex-direction: column;
  width: 90%; /* Adjust as needed */
  max-width: 1200px; /* Optional: Set a maximum width for larger screens */
  margin: 0 auto;
}

/* Tab headers styling */
.tab-headers {
  display: flex;
  justify-content: space-between;
  width: 100%; /* Ensure the tab headers span the full width */
}

button[role="tab"] {
  font-family: "Oswald", sans-serif;
  font-size: 1.2em;
  flex: 1;
  background-color: #005e86; /* Your main brand color */
  color: #fff;
  border: none;
  padding: 15px 20px;
  cursor: pointer;
  box-sizing: border-box;
  margin-left: 3px;
  position: relative;
  transition: background-color 0.3s ease-in-out;
}

/* Differentiate the selected tab */
button[aria-selected="true"] {
  background-color: #99b4c0; /* Your secondary brand color */
  color: #211224;
}

/* Tab panels styling */
[role="tabpanel"] {
  width: 100%;
  padding: 20px; /* Add padding as needed */
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-family: "Barlow", sans-serif;
  border: 2px #211224 solid;
  margin-top: 5px;
}

/* Hide the hidden tab panels */
[role="tabpanel"][aria-hidden="true"] {
  display: none;
  visibility: hidden;
  height: 0;
  overflow: hidden;
}
/* Style buttons inside tab panels */
[role="tabpanel"] button {
  font-family: "Oswald", sans-serif;
  background-color: #005e86; /* Primary color */
  color: #fff;
  border: none;
  padding: 10px 15px;
  cursor: pointer;
  border-radius: 5px;
  transition: background-color 0.3s ease-in-out, color 0.3s ease-in-out;
  margin: 5px;
}

/* Hover effect for buttons inside tab panels */
[role="tabpanel"] button:hover {
  background-color: #cce1e7; /* Secondary color on hover */
  color: #005e86; /* Primary color text on hover */
}
				
			
				
					
// Wait for the DOM content to be fully loaded before executing the code
document.addEventListener("DOMContentLoaded", function () {
  // Select all elements with role="tab" and role="tabpanel"
  const tabs = document.querySelectorAll('[role="tab"]');
  const panels = document.querySelectorAll('[role="tabpanel"]');

  // Show the first tab and panel by default
  tabs[0].setAttribute("aria-selected", "true"); // Activate the first tab
  tabs[0].setAttribute("tabindex", "0"); // Set tabindex for focus
  panels[0].removeAttribute("hidden"); // Show the first panel

  // Add event listeners to each tab for click and keyboard interactions
  tabs.forEach((tab) => {
    tab.addEventListener("click", () => {
      activateTab(tab); // Call the activateTab function on click
    });

    tab.addEventListener("keydown", (e) => {
      if (e.key === "Enter" || e.key === " ") {
        e.preventDefault();
        activateTab(tab); // Call the activateTab function on Enter or Space key press
      }
    });
  });

  // Function to activate a tab and associated panel
  function activateTab(currentTab) {
    const tabID = currentTab.getAttribute("id");

    // Deactivate all tabs and hide all panels
    tabs.forEach((t) => {
      t.setAttribute("aria-selected", "false");
    });

    panels.forEach((panel) => {
      panel.setAttribute("aria-hidden", "true");
    });

    // Activate the selected tab and show its associated panel
    currentTab.setAttribute("aria-selected", "true");
    currentTab.setAttribute("tabindex", "0");

    const panel = document.getElementById(
      currentTab.getAttribute("aria-controls")
    );
    panel.setAttribute("aria-hidden", "false");

    currentTab.focus(); // Set focus to the activated tab
  }
});

// Another event listener for when the DOM content is loaded
document.addEventListener("DOMContentLoaded", function () {
  const tabs = document.querySelectorAll('[role="tab"]');

  // Add click event listeners to each tab for animation (not recommended)
  tabs.forEach((tab) => {
    tab.addEventListener("onclick", () => {
      const currentTab = document.querySelector('[aria-selected="true"]');
      const newTab = tab;

      // Create a div for the animation
      const animation = document.createElement("div");
      animation.classList.add("tab-animation");
      animation.style.backgroundColor = getComputedStyle(
        currentTab
      ).backgroundColor;

      currentTab.appendChild(animation); // Append animation to the current tab

      const rect = newTab.getBoundingClientRect();
      const offset = rect.left - currentTab.getBoundingClientRect().left;

      animation.style.transform = `translateX(${offset}px)`; // Apply animation

      // Perform animation transitions and tab switching
      setTimeout(() => {
        currentTab.removeChild(animation);
        newTab.appendChild(animation);
        animation.style.transform = `translateX(0)`;

        setTimeout(() => {
          newTab.removeChild(animation);
          newTab.setAttribute("aria-selected", "true"); // Activate new tab
          newTab.focus(); // Set focus to the new tab
          currentTab.removeAttribute("aria-selected"); // Deactivate current tab
        }, 300);
      }, 300);
    });
  });
});
				
			

Why use a tablist component?

Tablist components in web development serve as efficient navigational elements that organize content into discrete sections, allowing users to switch between different panels or sections within a single space. They offer a structured layout that presents information in a clear and organized manner, enhancing user experience by reducing clutter and enabling users to focus on specific content sections. Accessibility considerations play a crucial role in the use of tablist components. For users reliant on assistive technologies or keyboard navigation, ensuring the proper implementation of tablist components is vital. Accessibility concerns include keyboard operability, providing clear focus indicators, and ensuring that users using screen readers understand the relationship between tabs and associated content. By following accessibility best practices, such as utilizing semantic markup, implementing ARIA attributes to convey structure, and maintaining keyboard navigability, tablist components can provide an inclusive and intuitive way for users of all abilities to navigate and access content effectively within a web interface.

What are some accessibility concerns?

  1. Keyboard Accessibility:
    • Concern: Users relying on keyboards might have difficulties navigating and selecting tabs within the tablist.
    • Mitigation: Ensure keyboard operability by allowing users to navigate and select tabs using keyboard controls (e.g., arrow keys, Enter key). Apply proper focus management and use ARIA attributes (role=”tab”, aria-selected) to indicate the selected tab and manage focus transitions.
  2. Screen Reader Compatibility and Tab Association:
    • Concern: Screen reader users may struggle to understand the relationship between tabs and their associated content.
    • Mitigation: Use proper ARIA attributes (aria-controls, aria-labelledby) to associate tabs with their respective content panels, allowing screen readers to convey the connection. Announce changes in tab states and content updates using ARIA live regions to provide clear notifications to screen reader users.
  3. Focus Indication and Visual Clarity:
    • Concern: Insufficient focus indicators or unclear visual cues may confuse users with visual impairments or low vision.
    • Mitigation: Ensure clear and visible focus indicators, such as distinct outlines or changes in color, to indicate the focused tab. Maintain sufficient contrast between the selected tab and its background to ensure visibility.
  4. Semantic Markup and ARIA Roles:
    • Concern: Improper use of markup or ARIA roles may lead to inconsistencies or confusion for assistive technologies.
    • Mitigation: Use semantic HTML elements (<ul>, <li>) to structure the tablist and associated content. Apply ARIA roles and attributes as necessary (role=”tablist”, role=”tab”, role=”tabpanel”) to convey the component’s structure and relationships to assistive technologies.
  5. Responsive Design and Interaction on Mobile Devices:
    • Concern: Tablist components may not function optimally on smaller screens or touch devices.
    • Mitigation: Ensure responsive design principles are applied, allowing the tablist to adapt and remain usable across various devices and screen sizes. Optimize tab interaction for touch-based devices, providing larger touch areas and suitable touch interactions.
Tested using
Chrome
with NVDA
Firefox
with NVDA
  • W3C SVG
    1.3.1
    Info and Relationships ( level A ) :
    Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text.
  • W3C SVG
    2.1.1
    Keyboard ( level A ) :
    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.
  • W3C SVG
    2.4.3
    Focus Order ( level A ) :
    If a Web page can be navigated sequentially and the navigation sequences affect meaning or operation, focusable components receive focus in an order that preserves meaning and operability.
  • W3C SVG
    2.4.7
    Focus Visible ( level AA ) :
    Any keyboard operable user interface has a mode of operation where the keyboard focus indicator is visible.
  • W3C SVG
    4.1.1
    Parsing ( level A ) :
    In content implemented using markup languages, elements have complete start and end tags, elements are nested according to their specifications, elements do not contain duplicate attributes, and any IDs are unique, except where the specifications allow these features.

Custom Select

Demo

				
					
<!-- Custom select container -->
<div id="container">
<div class="custom-select" role="combobox" aria-haspopup="listbox" aria-expanded="false">
  <!-- Button to trigger select dropdown -->
  <button class="select-trigger" aria-haspopup="listbox" aria-expanded="false" aria-labelledby="selectedOption">
    <span id="selectedOption">Select an option</span>
    <span aria-hidden="true">▼</span>
  </button>
  <!-- Dropdown options list -->
  <ul class="options-list" role="listbox" aria-labelledby="selectedOption" aria-hidden="true">
    <li role="option" aria-selected="false" tabindex="-1">Option 1</li>
    <li role="option" aria-selected="false" tabindex="-1">Option 2</li>
    <li role="option" aria-selected="false" tabindex="-1">Option 3</li>
    <!-- Add more options here -->
  </ul>
</div>
</div>
				
			
				
					
/* Styles for custom select component */
#container {
  margin: auto;
  width: 50%;
  text-align: center;
  margin-top: 50px;
}

.custom-select {
  position: relative;
  display: inline-block;
}

.select-trigger {
  display: flex;
  align-items: center;
  padding: 8px 16px;
  border: 1px solid #ccc;
  border-radius: 4px;
  background-color: #fff;
  cursor: pointer;
  align-text: center;
  font-size: 1.5em;
}

.options-list {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 1;
  display: none;
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid #ccc;
  border-top: none;
  border-radius: 0 0 4px 4px;
  background-color: #fff;
}

.options-list li {
  padding: 8px 16px;
  cursor: pointer;
}

.options-list li:hover {
  background-color: #f0f0f0;
}
				
			
				
					
const customSelect = document.querySelector('.custom-select');
const selectTrigger = document.querySelector('.select-trigger');
const optionsList = document.querySelector('.options-list');
const options = document.querySelectorAll('.options-list li');

let isOpen = false;

function toggleOptionsList() {
  isOpen = !isOpen;

  if (isOpen) {
    optionsList.style.display = 'block';
    selectTrigger.setAttribute('aria-expanded', 'true');
    optionsList.setAttribute('aria-hidden', 'false');

    // Set tabindex for options and focus the options list
    optionsList.querySelectorAll('li').forEach((option) => {
      option.setAttribute('tabindex', '0');
    });

    optionsList.focus();
    optionsList.addEventListener('keydown', collapseOnEscape);
    optionsList.addEventListener('keydown', navigateOptions);
    customSelect.addEventListener('focusout', collapseDropdown);
  } else {
    optionsList.style.display = 'none';
    selectTrigger.setAttribute('aria-expanded', 'false');
    optionsList.setAttribute('aria-hidden', 'true');

    // Reset tabindex for options and focus selectTrigger
    optionsList.querySelectorAll('li').forEach((option) => {
      option.setAttribute('tabindex', '-1');
    });

    selectTrigger.focus();
  }
}

function collapseOnEscape(event) {
  if (event.key === 'Escape') {
    isOpen = false;
    optionsList.style.display = 'none';
    selectTrigger.setAttribute('aria-expanded', 'false');
    selectTrigger.focus();
  }
}

function navigateOptions(event) {
  const focusedIndex = Array.from(options).indexOf(document.activeElement);
  if (event.key === 'ArrowDown') {
    event.preventDefault();
    const previousIndex = (focusedIndex - 1 + options.length) % options.length;
    options[previousIndex].focus();
  } else if (event.key === 'ArrowUp') {
    event.preventDefault();
    const nextIndex = (focusedIndex + 1) % options.length;
    options[nextIndex].focus();
  }
}

function collapseDropdown(event) {
  if (!event.relatedTarget || !optionsList.contains(event.relatedTarget)) {
    isOpen = false;
    optionsList.style.display = 'none';
    selectTrigger.setAttribute('aria-expanded', 'false');
    selectTrigger.focus();
  }
}

selectTrigger.addEventListener('click', toggleOptionsList);

optionsList.addEventListener('keydown', trapFocus);

options.forEach((option) => {
  option.addEventListener('click', () => {
    selectOption(option);
  });
  option.addEventListener('keydown', (event) => {
    if (event.key === 'Enter' || event.key === ' ') {
      selectOption(option);
    } else if (event.key === 'ArrowDown') {
      navigateOptions(event);
    } else if (event.key === 'ArrowUp') {
      navigateOptions(event);
    } else if (event.key === 'Escape') {
      toggleOptionsList();
    }
  });
});

function selectOption(option) {
  selectTrigger.querySelector('#selectedOption').innerText = option.innerText;
  options.forEach((opt) => {
    opt.setAttribute('aria-selected', 'false');
  });
  option.setAttribute('aria-selected', 'true');
  setTimeout(() => {
    toggleOptionsList();
  }, 100);
}

function trapFocus(event) {
  const focusableElements = optionsList.querySelectorAll('li');
  const firstFocusable = focusableElements[0];
  const lastFocusable = focusableElements[focusableElements.length - 1];
  const isTabPressed = event.key === 'Tab' || event.keyCode === 9;

  if (!isTabPressed) {
    return;
  }

  if (event.shiftKey) {
    if (document.activeElement === firstFocusable) {
      event.preventDefault();
      lastFocusable.focus();
    }
  } else {
    if (document.activeElement === lastFocusable) {
      event.preventDefault();
      firstFocusable.focus();
    }
  }
}
				
			

Why use a select element?

Select components are fundamental in web development as they provide users with an intuitive interface for making choices or selections from a predefined set of options. These components, commonly known as dropdowns or select menus, offer a compact and organized way to present multiple choices, making them ideal for forms, settings, or navigation elements. For users who rely on screen readers or keyboard navigation due to disabilities or limitations, properly designed select components ensure that the options are programmatically accessible and navigable. Accessibility concerns often revolve around ensuring keyboard operability, clear labeling, and semantic markup, allowing all users, including those with disabilities, to efficiently interact with and select options within these components. By prioritizing accessible design in select components, web developers contribute to a more inclusive digital landscape, fostering usability and equal access for everyone.

What are some accessibility concerns?

  1. Keyboard Accessibility:
    • Concern: Users who rely on keyboards for navigation may encounter difficulties when accessing select components or navigating through options.
    • Mitigation: Ensure full keyboard operability by allowing users to navigate and select options within the dropdown using keyboard controls, such as arrow keys, Enter, and Esc. Ensure proper focus management and use ARIA roles to convey the select element’s purpose.
  2. Screen Reader Compatibility and Labels:
    • Concern: Screen readers may not announce select components clearly or might miss label associations, impacting users’ understanding of the selectable options.
    • Mitigation: Use proper labeling techniques, associating labels explicitly with the select element using <label> tags or ARIA aria-label and aria-labelledby attributes. Ensure screen readers announce the label and provide context for the select component.
  3. Visual and Semantic Markup:
    • Concern: Select components might lack clear visual cues or semantic markup, making it challenging for users with disabilities to perceive and understand them.
    • Mitigation: Ensure consistent styling and visual cues indicating the dropdown nature of the select component. Use semantic HTML elements and ARIA roles (like role=”combobox”, role=”listbox”, and role=”option”) to convey the structure and purpose to assistive technologies.
  4. Complexity and Cognitive Load:
    • Concern: Dropdowns with an extensive list of options might overwhelm users, causing difficulties in finding or selecting the desired choice.
    • Mitigation: Implement features like filtering, autocomplete, or categorization to manage large sets of options. Provide clear instructions or context to aid users in making selections within the dropdown.
  5. Responsive Design and Accessibility:
    • Concern: Dropdowns may not scale or function well on smaller devices, impacting usability for users accessing the site on mobile or touch devices.
    • Mitigation: Ensure responsive design, allowing select components to adapt and remain usable across various screen sizes and devices. Use touch-friendly interfaces, larger tap areas, and well-designed dropdown behavior for touch-based interactions.
Tested using
Chrome
with NVDA
Firefox
with NVDA
  • W3C SVG
    1.4.10
    Reflow ( level AA ) :
    Content can be presented without loss of information or functionality, and without requiring scrolling in two dimensions for:
    User Interface Components: 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;
    Graphical Objects: Parts of graphics required to understand the content, except when a particular presentation of graphics is essential to the information being conveyed.
  • W3C SVG
    2.1.1
    Keyboard ( level A ) :
    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.
  • W3C SVG
    2.4.7
    Focus Visible ( level AA ) :
    Any keyboard operable user interface has a mode of operation where the keyboard focus indicator is visible.
  • W3C SVG
    3.3.2
    Labels or Instructions ( level A ) :
    Labels or instructions are provided when content requires user input.
  • W3C SVG
    4.1.2
    Name, Role, Value ( level A ) :
    For all user interface components (including but not limited to: form elements, links and components generated by scripts), the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to user agents, including assistive technologies.