Skip to main content

Disclosure v2.5.0

Reaveal or hide additional content when triggered, helping to manage the display of supplementary information.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam congue, urna eu efficitur accumsan, massa sem rhoncus elit, eu mattis erat metus gravida dolor. Maecenas imperdiet sodales dolor, sit amet malesuada lorem tincidunt in. Nunc a risus augue. Sed pellentesque pharetra iaculis. Donec gravida risus ut placerat venenatis. Integer sed erat eget risus rutrum condimentum. Curabitur tincidunt nisl ornare, viverra est eget, condimentum lectus. Integer pretium justo ac odio vestibulum ornare. Nullam mi risus, faucibus lacinia justo et, auctor vehicula lectus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam congue, urna eu efficitur accumsan, massa sem rhoncus elit, eu mattis erat metus gravida dolor. Maecenas imperdiet sodales dolor, sit amet malesuada lorem tincidunt in. Nunc a risus augue. Sed pellentesque pharetra iaculis. Donec gravida risus ut placerat venenatis. Integer sed erat eget risus rutrum condimentum. Curabitur tincidunt nisl ornare, viverra est eget, condimentum lectus. Integer pretium justo ac odio vestibulum ornare. Nullam mi risus, faucibus lacinia justo et, auctor vehicula lectus.

Reminder: A disclosure is intended for (mostly) single-purpose use. If you need to group multiple disclosures, such as in a FAQ or similar layout, please use the Accordion component.

<button class="disclosure--btn">Learn More</button>

<div class="disclosure--content">

  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam congue, urna eu efficitur accumsan, massa sem rhoncus elit, eu mattis erat metus gravida dolor. Maecenas imperdiet sodales dolor, sit amet malesuada lorem tincidunt in. Nunc a risus augue. Sed pellentesque pharetra iaculis. Donec gravida risus ut placerat venenatis. Integer sed erat eget risus rutrum condimentum. Curabitur tincidunt nisl ornare, viverra est eget, condimentum lectus. Integer pretium justo ac odio vestibulum ornare. Nullam mi risus, faucibus lacinia justo et, auctor vehicula lectus.</p>

  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam congue, urna eu efficitur accumsan, massa sem rhoncus elit, eu mattis erat metus gravida dolor. Maecenas imperdiet sodales dolor, sit amet malesuada lorem tincidunt in. Nunc a risus augue. Sed pellentesque pharetra iaculis. Donec gravida risus ut placerat venenatis. Integer sed erat eget risus rutrum condimentum. Curabitur tincidunt nisl ornare, viverra est eget, condimentum lectus. Integer pretium justo ac odio vestibulum ornare. Nullam mi risus, faucibus lacinia justo et, auctor vehicula lectus.</p>

</div>
/*!

  Radancy Component Library: Disclosure

  Contributor(s):
  Michael "Spell" Spellacy

  Dependencies: Sass

*/

// Sass Variables

$disclosure-breakpoint: 60em;

:root {

  --disclosure-arrow-size: 0.8em; // Toggle Arrow Size
  --disclosure-arrow-border: calc(2rem / 16); // Toggle Arrow Border
  --disclosure-base-color: #6F00EF; // Primary Color
  --disclosure-color-active: #a9a9a9; // Active State
  --disclosure-color-default: #d3d3d3; // Default States
  --disclosure-color-interactive: #666; // Interactive State 
  --disclosure-color-selected: var(--disclosure-base-color); // Selected States

}

// Disclosure Button

.disclosure--btn {
background-color: var(--disclosure-color-default) ;
border: 0;
color: inherit;
cursor: pointer;
display: grid;
font-size: inherit;
grid-auto-flow: column;
line-height: normal;
padding: 1em;
position: relative;
text-align: start;
width: 100%;

  // Accessibility: Focus and hover states must meet a 3:1 color ratio between background color and foreground text.
  // The project designer should be providing this state. Also, never remove focus outlines! Either redesign them or 
  // design an alternative focus state. Learn: https://webaim.org/articles/contrast/

  &:focus-visible, &:hover {
  background-color: var(--disclosure-color-active);
  }
    
  &:focus-visible {
  outline-offset: -5px;
  }

  &[aria-expanded="true"] {
  background-color: var(--disclosure-color-selected);
  color: #fff;

    // Disclosure Arrow: State

    .disclosure--arrow {
    border-block-end-color: #fff;
    border-inline-start-color: #fff;
    transform: rotate(135deg);
    translate: 0 calc(8rem/16);
    }

  }

}

// Disclosure Heading

.disclosure--heading {
all: unset;
display: contents;
}

// Disclosure Arrow

.disclosure--arrow {
block-size: var(--disclosure-arrow-size);
border-width: var(--disclosure-arrow-border);
border-block-color: transparent var(--disclosure-color-selected);
border-block-start-width: 0;
border-inline-color: var(--disclosure-color-selected) transparent;
border-inline-end-width: 0;
border-style: solid;
direction: ltr;
display: block;
inline-size: var(--disclosure-arrow-size);
justify-self: end;
transform: rotate(-45deg);
}

// Disclosure Content

.disclosure--content {
background-color: var(--disclosure-color-default);
display: none;
overflow: hidden;
padding: 1em;

  &[data-animation] {
  
    block-size: 0;
    interpolate-size: allow-keywords; // Allows animating to/from "auto". Unsupported browsers simply skip the height animation.
    opacity: 0;
    padding-block: 0;

      @media (prefers-reduced-motion: no-preference) {

        transition: display 500ms allow-discrete, block-size 500ms ease-in-out, opacity 500ms ease-in-out;

      }
  
  }

}

// Persistent State 

.disclosure--btn, .disclosure--heading {

  &[data-persistent] {

    @media(min-width: $disclosure-breakpoint) {

      display: none;

      + .disclosure--content {
      display: block;

        &[data-animation] {
        block-size: auto;
        opacity: 1;
        }

      }

    }

  }

}

// Viewport Context 

.disclosure--btn, .disclosure--heading {

  &[data-context], &[data-disclosure-viewport] {

    @media(min-width: $disclosure-breakpoint) {

      padding: 0;
      pointer-events: none;
      visibility: hidden;

        .disclosure--btn {
        padding: inherit;
        pointer-events: inherit;
        visibility: inherit;
        }

        span:not([aria-hidden]) {
        color: var(--disclosure-base-color);
        font-weight: 700;
        visibility: visible;
        }

        // If icon present

        span[aria-hidden] {
        display: none;
        }

        + .disclosure--content {
          background-color: transparent;
          display: block;
          padding: 0;
          }

    }

  }

}

// Disclosure Event

.disclosure--btn[aria-expanded="true"] + .disclosure--content, .disclosure--heading:has(.disclosure--btn[aria-expanded="true"]) + .disclosure--content {
display: block;

  &[data-animation] {
  
    block-size: auto;
    opacity: 1;

    @starting-style {

      block-size: 0;
      opacity: 0;
    
    }

  }

}
/*!

  Radancy Component Library: Disclosure

  Contributor(s):
  Michael "Spell" Spellacy

*/

(() => {

  "use strict";

  // Display which Disclosure is in use via console:

  console.log("%cDisclosure%cv2.5.0", "background: #2d2d2d; color: #fff; padding: 6px 10px; border-radius: 16px 0 0 16px; font-weight: 600;" , "background: #6e00ee; color: #fff; padding: 6px 10px; border-radius: 0 16px 16px 0; font-weight: 600;");

  // Commonly used Classes, Data Attributes, States, Strings, etc.

  const disclosureButtonClass = ".disclosure--btn";
  const disclosureButtons = document.querySelectorAll(disclosureButtonClass);
  const disclosureArrowClassName = "disclosure--arrow";
  const disclosureContentClass = ".disclosure--content";
  const disclosureHeadingClass = ".disclosure--heading";
  const disclosureDataDefaultOpen = "data-open";
  const disclosureDataEnableAnchor = "data-enable-anchor";
  const disclosureDataLightDismiss = "data-light-dismiss";
  const disclosureDataRemoveArrow = "data-remove-arrow";

  // Light Dismiss: Disclosures that close on Escape, or when the user clicks or tabs to something that is not part of any disclosure.

  const allDisclosures = [];
  const lightDismissDisclosures = [];

  // Focus moves on pointerdown. Closing then would collapse content and shift the layout before the click lands, so pointer interactions are left to the click listener.

  let isPointerInteraction = false;

  const isInsideDisclosure = (disclosure, node) => disclosure.trigger.contains(node) || disclosure.content.contains(node);

  // Persistent and context disclosures hide the button on larger viewports, where content is always shown. They then behave as regular page content.

  const isDisclosureInteractive = (disclosure) => {

    const button = disclosure.button;

    return getComputedStyle(button).visibility === "visible" && button.getClientRects().length > 0;

  };

  const isDisclosureOpen = (disclosure) => disclosure.button.getAttribute("aria-expanded") === "true" && isDisclosureInteractive(disclosure);

  // Moving between disclosures leaves them open, so they do not behave like an accordion.

  const isInsideAnyDisclosure = (node) => allDisclosures.some((disclosure) => isDisclosureInteractive(disclosure) && isInsideDisclosure(disclosure, node));

  const closeDisclosure = (disclosure) => {

    disclosure.button.setAttribute("aria-expanded", "false");

  };

  const closeOpenDisclosures = () => {

    lightDismissDisclosures.filter(isDisclosureOpen).forEach(closeDisclosure);

  };

  // Add Listener and other needed attributes to disclosure button

  disclosureButtons.forEach((button, index) => {

    const count = index + 1;

    // Trigger is the heading wrapping the button, or the button itself.

    const trigger = button.closest(disclosureHeadingClass) || button;

    // Content must directly follow the trigger. If missing, skip this disclosure.

    const content = trigger.nextElementSibling;

    if (!content || !content.matches(disclosureContentClass)) {

      console.warn(`Disclosure: No ${disclosureContentClass} found directly after button.`, button);

      return;

    }

    button.setAttribute("aria-expanded", "false");

    let thisButtonID, thisContentID;

    if (button.hasAttribute("id")) {

      // Check for custom ID on button.

      thisButtonID = button.getAttribute("id");
      thisContentID = `${thisButtonID}-content`;

    } else {

      // Else, add dynamic ID.

      thisButtonID = `disclosure-btn-${count}`;
      thisContentID = `disclosure-content-${count}`;

    }

    // Set ID and aria-controls on button.

    button.setAttribute("id", thisButtonID);
    button.setAttribute("aria-controls", thisContentID);
    content.setAttribute("id", thisContentID);

    // If custom arrow not needed.

    if (!button.hasAttribute(disclosureDataRemoveArrow)) {

      const toggleState = document.createElement("span");
      toggleState.setAttribute("aria-hidden", "true");
      toggleState.classList.add(disclosureArrowClassName);
      button.append(toggleState);

    }

    button.addEventListener("click", () => {

      const isExpanded = button.getAttribute("aria-expanded") === "true";
      
      button.setAttribute("aria-expanded", isExpanded ? "false" : "true");

      if (button.hasAttribute(disclosureDataEnableAnchor)) {

        history.pushState(null, null, `#${button.id}`);

      }

    });

    // Track all disclosures, and those with light dismiss enabled.

    const disclosure = { button, trigger, content };

    allDisclosures.push(disclosure);

    if (button.hasAttribute(disclosureDataLightDismiss)) {

      lightDismissDisclosures.push(disclosure);

    }

    // Open desired disclosure.

    if (button.hasAttribute(disclosureDataDefaultOpen)) {

      button.setAttribute("aria-expanded", "true");

    }

  });

  // Open selected disclosure via URL fragment, on load and whenever the fragment changes (e.g., in-page links).

  const openFromURLFragment = () => {

    const disclosureTarget = document.getElementById(location.hash.slice(1));

    if (disclosureTarget && disclosureTarget.matches(disclosureButtonClass)) {

      disclosureTarget.setAttribute("aria-expanded", "true");
      disclosureTarget.focus();

    }

  };

  openFromURLFragment();

  window.addEventListener("hashchange", openFromURLFragment);

  // Light Dismiss: Outside click, outside focus, and Escape key. Only added if a light dismiss disclosure is present.

  if (lightDismissDisclosures.length) {

    document.addEventListener("pointerdown", () => {

      isPointerInteraction = true;

    }, true);

    // Reset on click, or on keydown in case the pointer was dragged away and no click followed.

    ["click", "keydown"].forEach((type) => {

      document.addEventListener(type, () => {

        isPointerInteraction = false;

      }, true);

    });

    document.addEventListener("click", (event) => {

      if (!isInsideAnyDisclosure(event.target)) {

        closeOpenDisclosures();

      }

    });

    document.addEventListener("focusin", (event) => {

      if (!isPointerInteraction && !isInsideAnyDisclosure(event.target)) {

        closeOpenDisclosures();

      }

    });

    document.addEventListener("keydown", (event) => {

      if (event.key !== "Escape" || event.defaultPrevented) {

        return;

      }

      const openDisclosures = lightDismissDisclosures.filter(isDisclosureOpen);
      const focusedElement = document.activeElement;

      // Close the innermost open disclosure containing focus. Nested disclosures come later in document order, so the last match is the innermost.

      const currentDisclosure = openDisclosures.findLast((disclosure) => isInsideDisclosure(disclosure, focusedElement));

      if (currentDisclosure) {

        closeDisclosure(currentDisclosure);

        // Return focus to the button so it is not left inside hidden content.

        if (currentDisclosure.content.contains(focusedElement)) {

          currentDisclosure.button.focus();

        }

        // Prevent an enclosing dialog from also closing on the same key press.

        event.preventDefault();

      } else if (focusedElement === document.body && openDisclosures.length) {

        // Safari does not focus buttons on click, so focus may still be on the body after opening with a mouse.

        closeOpenDisclosures();

        event.preventDefault();

      }

    });

  }

})();

Properties Anchor (Properties)

Use the following properties to configure this component.

Attribute Applies To Note Description
data-animation .disclosure--content Boolean, Optional
Used to enable content to be animated when opened. Please note that the default animation leverages @starting-style at-rule.
data-context .disclosure--btn, .disclosure--heading Boolean, Optional
Used to allow the disclosure to become non-interactive on larger viewport sizes, while maintaining interactivity on mobile. The older attribute, data-disclosure-viewport has been deprecated and should no longer be used.
data-enable-anchor .disclosure--btn Boolean, Optional
Used to enable anchor functionality. When enabled, selecting a disclosure appends its configured anchor to the URL. Anchors can be useful for tracking interactions and creating links that navigate directly to specific content.
data-light-dismiss .disclosure--btn Boolean, Optional
Closes the disclosure when the user presses Escape, or clicks or moves focus to something that is not part of a disclosure. Moving between disclosures leaves them open, so they do not behave like an accordion. If focus is inside the content when Escape is pressed, focus returns to the button. Nested disclosures close one at a time, innermost first. Not intended for use with data-open, as an open disclosure would close on the first click anywhere on the page.
data-open .disclosure--btn Boolean, Optional
Specifies that content should be open on page load. If not specified, no content will be opened.
data-persistent .disclosure--btn Boolean, Optional
Specifies that the content should remain persistent and always open in larger viewports. Must not be used with .disclosure--heading, as hiding the heading removes it from the page outline. For headings, use data-context instead.
data-remove-arrow .disclosure--btn Boolean, Optional
By default, all buttons include a span element for icons. If this is not desirable, you can remove it by adding this attribute and using your own inline or CSS icon. For custom inline icons, ensure they are hidden from assistive technologies by using aria-hidden="true".

Class Applies To Note Description
.disclosure--btn button Mandatory
Required on your disclosure button.
.disclosure--content div, section Mandatory
Required on your disclosure content.
.disclosure--heading h1, h2, h3, h4, h5, h6 Optional
Include this class on any heading you might have that wraps your button. A heading should always wrap a button and not the other way around.

Release Notes Anchor (Release Notes)

Keep up to date on major changes to this component.

Feature
October 16, 2026: Added data-light-dismiss, a new attribute for .disclosure--btn, allowing a disclosure to close on Escape, outside click, or when focus leaves it. Bumped to 2.5.0.
Bug
October 16, 2026: Fixed URL fragment handling so IDs starting with a digit or containing special characters no longer throw, and in-page links now open their disclosure. A missing .disclosure--content now logs a warning instead of breaking every disclosure after it. Bumped to 2.4.3.
Behavior
October 16, 2026: The data-animation height animation now uses interpolate-size, so tall content is no longer clipped at 1500px. Browsers without support skip the height animation but still fade in.
Note
October 16, 2026: Corrected data-persistent documentation. It belongs on .disclosure--btn only, not on the content, and headings should use data-context instead.
Bug
September 28, 2026: Fixed a bug introduced in 2.4.1 with addition of new animation styles, which caused content to be hidden when data-persistent was used. Bumped to 2.4.2.
Behavior
August 19, 2026: Made CSS improvements to the animation example. Bumped to 2.4.1.
Bug
January 31, 2025: Fixed issue with script breaking when URL contains a page fragment that is not used by the disclosure. Addressed some redundancy as well. Thanks, James!
Feature
January 29, 2025: Added data-persistent, a new attribute for .disclosure--btn, allowing triggers to be hidden on desktop but visible on mobile.
Behavior
January 29, 2025: Reinstated the viewport-friendly method, allowing triggers to be non-interactive on larger viewports. The attribute is now called data-context and should be used going forward. data-disclosure-viewport is deprecated and will be removed in a future release.
Note
April 24, 2025: Upgraded component to ES6. Thanks, Donte!

Report Issues

Find a bug? Want a new feature? Report it on JIRA. You're so awesome!