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();
}
});
}
})();What do you mean? An African or a European swallow?
I don't know that. Aaaaaaaaagh!
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.
Experimental: Please note that this example uses the new @starting-style at-rule and transition-behavior: allow-discrete; to produce animation. This technique is supported in most major browsers. If you require specific animations in non-supporting browserss, you will need to come up with a traditional implementation.
<button class="disclosure--btn">What is the airspeed velocity of an unladen swallow?</button>
<div class="disclosure--content" data-animation>
<p>What do you mean? An African or a European swallow?</p>
<p>I don't know that. Aaaaaaaaagh!</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>
<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();
}
});
}
})();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.
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" data-open>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>
<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();
}
});
}
})();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.
Note: In this example, 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.
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" data-enable-anchor>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();
}
});
}
})();Purple 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 42 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.
<h3 class="disclosure--heading"><button class="disclosure--btn">What is the answer to life, the universe, and everything?</button></h3>
<div class="disclosure--content">
<p>Purple 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 <strong>42</strong> 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();
}
});
}
})();To seek the Holy Grail. Purple 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.
Note: In this example, the disclosure closes when you press Escape, click outside of it, or tab away from it. This is not typical for a simple disclosure, so use it sparingly, such as when the disclosure acts more like a dropdown.
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" data-light-dismiss>What is your quest?</button>
<div class="disclosure--content">
<p>To seek the Holy Grail. Purple 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 <a href="#!">malesuada lorem tincidunt</a> 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();
}
});
}
})();Purple 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 42 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.
Note: Reduce your screen size to reveal the disclosure button. In some cases, we may only require a discosure to be present on mobile, but not on desktop.
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.
Warning: This approach should be used sparingly. Try to avoid removing interactive components based on screen size, as it can disrupt usability. If a user zooms in and focuses on a button, they will lose focus when zooming out because the button or button visibility is removed, which can be disorienting.
<button class="disclosure--btn" data-persistent>Open Me</button>
<div class="disclosure--content" data-animation>
<p>Purple 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 <strong>42</strong> 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();
}
});
}
})();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.
Note: In some scenarios, we may only want the disclosure to be present on smaller viewports. In such cases, it is crucial to ensure that accessibility on larger screens is not compromised by leaving buttons accessible to assistive technologies. To see this solution in action, try pressing or tabbing to the headings in larger viewport sizes. As you'll notice, the headings are not interactive and all content that would normally be collapsed is revealed, which is exactly what we want. However, when you change the viewport to a smaller size, the component then becomes a traditional accordion. This kind of behavior can often be found in the footer of applications or website, where space is more limited on smaller screens.
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.
Warning: This approach should be used sparingly. Try to avoid removing interactive components based on screen size, as it can disrupt usability. If a user zooms in and focuses on a button, they will lose focus when zooming out because the button or button visibility is removed, which can be disorienting.
<h3 class="disclosure--heading" data-context><button class="disclosure--btn"><span>Learn More</span></button></h3>
<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--contentnow logs a warning instead of breaking every disclosure after it. Bumped to 2.4.3. - Behavior
- October 16, 2026: The
data-animationheight animation now usesinterpolate-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-persistentdocumentation. It belongs on.disclosure--btnonly, not on the content, and headings should usedata-contextinstead. - 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-persistentwas 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-contextand should be used going forward.data-disclosure-viewportis 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!