π¦ Progressive Disclosure Components¶
Philosophy¶
Different stakeholders need different levels of detail:
- Board Directors: Want summaries, decisions, and exceptions
- Executives: Need context, trends, and strategic options
- Management: Require operational details and implementation steps
- Technical Staff: Need full specifications and configurations
Progressive disclosure ensures each user sees exactly what they need, when they need it.
Core Patterns¶
1. Expandable Summary Cards¶
<article class="gc-disclosure" data-expanded="false">
<header
class="gc-disclosure__header"
role="button"
aria-expanded="false"
aria-controls="disclosure-content-1"
tabindex="0"
>
<div class="gc-disclosure__summary">
<h3 class="gc-disclosure__title">Essential Eight Compliance</h3>
<div class="gc-disclosure__metrics">
<span class="gc-disclosure__metric gc-disclosure__metric--primary">
ML1 Current
</span>
<span class="gc-disclosure__metric gc-disclosure__metric--target">
ML2 Target
</span>
</div>
</div>
<button class="gc-disclosure__toggle" aria-label="Show details">
<svg class="gc-disclosure__icon" aria-hidden="true">
<use href="#icon-chevron-down"></use>
</svg>
</button>
</header>
<div class="gc-disclosure__content" id="disclosure-content-1" hidden>
<div class="gc-disclosure__details">
<!-- Detailed content here -->
</div>
</div>
</article>
.gc-disclosure {
border: 2px solid var(--gc-border-default);
border-radius: 12px;
background: var(--gc-bg-card);
transition: all 0.3s ease;
}
.gc-disclosure__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
cursor: pointer;
user-select: none;
}
.gc-disclosure__header:hover {
background: var(--gc-bg-hover);
}
.gc-disclosure__header:focus-visible {
outline: 3px solid var(--gc-info);
outline-offset: -3px;
border-radius: 10px;
}
.gc-disclosure__toggle {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
background: transparent;
cursor: pointer;
transition: transform 0.3s ease;
}
.gc-disclosure[data-expanded='true'] .gc-disclosure__toggle {
transform: rotate(180deg);
}
.gc-disclosure__content {
border-top: 1px solid var(--gc-border-light);
padding: 0;
max-height: 0;
overflow: hidden;
transition:
max-height 0.3s ease,
padding 0.3s ease;
}
.gc-disclosure[data-expanded='true'] .gc-disclosure__content {
max-height: 2000px; /* Sufficient for content */
padding: 20px;
}
/* Animation for smooth reveal */
@keyframes slide-down {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.gc-disclosure__content[hidden] {
display: none;
}
.gc-disclosure__content:not([hidden]) {
animation: slide-down 0.3s ease;
}
class DisclosureCard {
constructor(element) {
this.element = element
this.header = element.querySelector('.gc-disclosure__header')
this.content = element.querySelector('.gc-disclosure__content')
this.isExpanded = false
this.init()
}
init() {
this.header.addEventListener('click', () => this.toggle())
this.header.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
this.toggle()
}
})
// Remember user preference
const savedState = localStorage.getItem(`disclosure-${this.element.id}`)
if (savedState === 'expanded') {
this.expand()
}
}
toggle() {
this.isExpanded ? this.collapse() : this.expand()
}
expand() {
this.isExpanded = true
this.element.dataset.expanded = 'true'
this.header.setAttribute('aria-expanded', 'true')
this.content.hidden = false
// Save preference
localStorage.setItem(`disclosure-${this.element.id}`, 'expanded')
// Analytics
this.trackEvent('expand')
}
collapse() {
this.isExpanded = false
this.element.dataset.expanded = 'false'
this.header.setAttribute('aria-expanded', 'false')
// Delay hiding for animation
setTimeout(() => {
if (!this.isExpanded) {
this.content.hidden = true
}
}, 300)
// Save preference
localStorage.setItem(`disclosure-${this.element.id}`, 'collapsed')
// Analytics
this.trackEvent('collapse')
}
trackEvent(action) {
// Track user behavior for optimization
if (window.analytics) {
window.analytics.track('Disclosure Interaction', {
action,
cardId: this.element.id,
userRole: window.userRole,
})
}
}
}
2. Role-Based Default States¶
class RoleAwareDisclosure {
constructor(element, userRole) {
this.element = element
this.userRole = userRole
this.defaultStates = {
board: 'collapsed', // Board sees summaries
executive: 'collapsed', // Executives see summaries
management: 'expanded', // Management sees details
technical: 'expanded', // Technical sees everything
}
}
getDefaultState() {
// Check if explicitly marked for role
const roleDefault = this.element.dataset[`defaultFor${this.userRole}`]
if (roleDefault) return roleDefault
// Check user preference
const saved = localStorage.getItem(`disclosure-${this.element.id}`)
if (saved) return saved
// Use role default
return this.defaultStates[this.userRole] || 'collapsed'
}
applyDefaultState() {
const state = this.getDefaultState()
if (state === 'expanded') {
this.expand()
} else {
this.collapse()
}
}
}
3. Hierarchical Disclosure¶
<div class="gc-hierarchy">
<!-- Level 1: Executive Summary -->
<section
class="gc-hierarchy__level gc-hierarchy__level--1"
data-level="executive"
>
<h2 class="gc-hierarchy__title">
Compliance Overview
<span class="gc-hierarchy__badge">Board View</span>
</h2>
<div class="gc-hierarchy__summary">
<div class="gc-stat">
<span class="gc-stat__value">85%</span>
<span class="gc-stat__label">Overall Compliance</span>
</div>
<div class="gc-stat">
<span class="gc-stat__value">2</span>
<span class="gc-stat__label">Actions Required</span>
</div>
</div>
<button class="gc-hierarchy__expand" aria-label="Show management details">
View Details β
</button>
</section>
<!-- Level 2: Management Details -->
<section
class="gc-hierarchy__level gc-hierarchy__level--2"
data-level="management"
hidden
>
<h3 class="gc-hierarchy__title">
Framework Performance
<span class="gc-hierarchy__badge">Management View</span>
</h3>
<div class="gc-hierarchy__grid">
<div class="gc-framework">
<h4>Essential Eight</h4>
<div class="gc-framework__progress">
<div class="gc-framework__bar" style="width: 70%"></div>
</div>
<span>ML1 β ML2</span>
</div>
<!-- More frameworks -->
</div>
<button class="gc-hierarchy__expand" aria-label="Show technical details">
Technical Details β
</button>
</section>
<!-- Level 3: Technical Implementation -->
<section
class="gc-hierarchy__level gc-hierarchy__level--3"
data-level="technical"
hidden
>
<h3 class="gc-hierarchy__title">
Implementation Status
<span class="gc-hierarchy__badge">Technical View</span>
</h3>
<div class="gc-hierarchy__technical">
<table class="gc-table">
<thead>
<tr>
<th>Control</th>
<th>Status</th>
<th>Evidence</th>
<th>Next Step</th>
</tr>
</thead>
<tbody>
<!-- Detailed rows -->
</tbody>
</table>
</div>
</section>
</div>
4. Smart Summaries¶
<div class="gc-smart-summary">
<div class="gc-smart-summary__primary">
<!-- Always visible to board -->
<h3 class="gc-smart-summary__headline">
Cybersecurity posture is <strong>improving</strong>
</h3>
<p class="gc-smart-summary__key-point">
3 of 5 frameworks meet board-set targets
</p>
</div>
<div class="gc-smart-summary__secondary" data-role="executive">
<!-- Visible to executives by default -->
<ul class="gc-smart-summary__highlights">
<li>Essential Eight: ML1 β ML2 (60% complete)</li>
<li>ISO 27001: Certification on track</li>
<li>NIST: 85% controls implemented</li>
</ul>
</div>
<details class="gc-smart-summary__details">
<summary class="gc-smart-summary__toggle">View Complete Analysis</summary>
<div class="gc-smart-summary__full">
<!-- Full details for those who want them -->
</div>
</details>
</div>
.gc-smart-summary__primary {
font-size: 18px;
line-height: 1.6;
margin-bottom: 16px;
}
.gc-smart-summary__headline strong {
color: var(--gc-status-good);
font-weight: 600;
}
.gc-smart-summary__secondary {
padding: 16px;
background: var(--gc-bg-subtle);
border-radius: 8px;
margin-bottom: 16px;
}
/* Hide secondary for board by default */
[data-user-role='board'] .gc-smart-summary__secondary {
display: none;
}
/* Show when explicitly requested */
[data-user-role='board'] .gc-smart-summary__secondary[data-show='true'] {
display: block;
}
.gc-smart-summary__details {
border: 1px solid var(--gc-border-light);
border-radius: 8px;
padding: 0;
}
.gc-smart-summary__toggle {
display: block;
width: 100%;
padding: 12px 16px;
text-align: left;
cursor: pointer;
background: transparent;
border: none;
font-size: 14px;
color: var(--gc-info);
}
.gc-smart-summary__toggle:hover {
background: var(--gc-bg-hover);
}
.gc-smart-summary__toggle::marker {
content: '';
}
.gc-smart-summary__toggle::before {
content: 'βΆ';
display: inline-block;
margin-right: 8px;
transition: transform 0.3s;
}
.gc-smart-summary__details[open] .gc-smart-summary__toggle::before {
transform: rotate(90deg);
}
5. Inline Expansion¶
<p class="gc-text">
Your organization's compliance score is
<span class="gc-inline-expand">
<span class="gc-inline-expand__summary">85%</span>
<button
class="gc-inline-expand__trigger"
aria-label="Show calculation details"
aria-expanded="false"
>
<svg><use href="#icon-info"></use></svg>
</button>
<span class="gc-inline-expand__details" hidden>
(calculated from: E8 70% Γ 0.3 + ISO 90% Γ 0.3 + NIST 85% Γ 0.2 + PCI 95%
Γ 0.2)
</span>
</span>
which exceeds the industry average.
</p>
.gc-inline-expand {
position: relative;
display: inline-flex;
align-items: baseline;
gap: 4px;
}
.gc-inline-expand__summary {
font-weight: 600;
color: var(--gc-info);
}
.gc-inline-expand__trigger {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 0;
border: none;
background: var(--gc-bg-info);
border-radius: 50%;
cursor: pointer;
vertical-align: text-top;
}
.gc-inline-expand__trigger svg {
width: 12px;
height: 12px;
color: var(--gc-info);
}
.gc-inline-expand__details {
position: absolute;
top: 100%;
left: 0;
z-index: 10;
margin-top: 4px;
padding: 8px 12px;
background: var(--gc-bg-card);
border: 1px solid var(--gc-border-default);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
font-size: 14px;
white-space: nowrap;
opacity: 0;
transform: translateY(-4px);
transition: all 0.2s ease;
}
.gc-inline-expand__details:not([hidden]) {
opacity: 1;
transform: translateY(0);
}
6. Tabbed Disclosure¶
<div class="gc-tabs" role="tablist" aria-label="View options">
<button
class="gc-tabs__tab gc-tabs__tab--active"
role="tab"
aria-selected="true"
aria-controls="panel-summary"
id="tab-summary"
>
Summary
</button>
<button
class="gc-tabs__tab"
role="tab"
aria-selected="false"
aria-controls="panel-details"
id="tab-details"
>
Details
</button>
<button
class="gc-tabs__tab"
role="tab"
aria-selected="false"
aria-controls="panel-technical"
id="tab-technical"
>
Technical
</button>
</div>
<div class="gc-panels">
<div
class="gc-panel gc-panel--active"
role="tabpanel"
aria-labelledby="tab-summary"
id="panel-summary"
>
<!-- Summary content for board -->
</div>
<div
class="gc-panel"
role="tabpanel"
aria-labelledby="tab-details"
id="panel-details"
hidden
>
<!-- Detailed content for management -->
</div>
<div
class="gc-panel"
role="tabpanel"
aria-labelledby="tab-technical"
id="panel-technical"
hidden
>
<!-- Technical content for IT -->
</div>
</div>
Progressive Loading¶
class ProgressiveContent {
constructor(container) {
this.container = container
this.observer = null
this.loaded = new Set()
}
init() {
// Load visible content immediately
this.loadVisibleContent()
// Set up intersection observer for hidden content
this.setupLazyLoading()
// Preload based on user behavior
this.setupPredictiveLoading()
}
loadVisibleContent() {
const visible = this.container.querySelectorAll('[data-load="immediate"]')
visible.forEach((el) => this.loadContent(el))
}
setupLazyLoading() {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !this.loaded.has(entry.target)) {
this.loadContent(entry.target)
}
})
},
{
rootMargin: '100px', // Load 100px before visible
}
)
const lazy = this.container.querySelectorAll('[data-load="lazy"]')
lazy.forEach((el) => this.observer.observe(el))
}
setupPredictiveLoading() {
// Preload content when user hovers over trigger
this.container.addEventListener(
'mouseenter',
(e) => {
const trigger = e.target.closest('[data-preload]')
if (trigger) {
const target = document.querySelector(trigger.dataset.preload)
if (target && !this.loaded.has(target)) {
this.preloadContent(target)
}
}
},
true
)
}
async loadContent(element) {
if (this.loaded.has(element)) return
const url = element.dataset.contentUrl
if (!url) return
try {
const response = await fetch(url)
const content = await response.text()
element.innerHTML = content
this.loaded.add(element)
element.classList.add('gc-content--loaded')
} catch (error) {
console.error('Failed to load content:', error)
element.innerHTML = '<p class="gc-error">Failed to load content</p>'
}
}
preloadContent(element) {
// Preload but don't insert yet
const url = element.dataset.contentUrl
if (!url) return
fetch(url)
.then((response) => response.text())
.then((content) => {
// Cache for immediate insertion when needed
element.dataset.cachedContent = content
})
}
}
Density Controls¶
<div class="gc-density-control">
<label class="gc-density-control__label">Information Density</label>
<div class="gc-density-control__options" role="radiogroup">
<button
class="gc-density-control__option"
role="radio"
aria-checked="false"
data-density="compact"
>
<svg><use href="#icon-compact"></use></svg>
Compact
</button>
<button
class="gc-density-control__option gc-density-control__option--active"
role="radio"
aria-checked="true"
data-density="comfortable"
>
<svg><use href="#icon-comfortable"></use></svg>
Comfortable
</button>
<button
class="gc-density-control__option"
role="radio"
aria-checked="false"
data-density="spacious"
>
<svg><use href="#icon-spacious"></use></svg>
Spacious
</button>
</div>
</div>
/* Density-based styling */
[data-density='compact'] {
--spacing-unit: 8px;
--font-size-base: 14px;
--line-height: 1.4;
}
[data-density='comfortable'] {
--spacing-unit: 12px;
--font-size-base: 16px;
--line-height: 1.6;
}
[data-density='spacious'] {
--spacing-unit: 16px;
--font-size-base: 18px;
--line-height: 1.8;
}
.gc-card {
padding: calc(var(--spacing-unit) * 2);
font-size: var(--font-size-base);
line-height: var(--line-height);
}
React Implementation¶
// Progressive Disclosure Component
export const ProgressiveDisclosure: React.FC<{
summary: React.ReactNode
details: React.ReactNode
defaultExpanded?: boolean
role?: UserRole
}> = ({ summary, details, defaultExpanded = false, role }) => {
const [isExpanded, setIsExpanded] = useState(() => {
// Check saved preference
const saved = localStorage.getItem(`disclosure-${id}`)
if (saved) return saved === 'expanded'
// Use role-based default
if (role === 'board' || role === 'executive') return false
return defaultExpanded
})
const toggle = () => {
const newState = !isExpanded
setIsExpanded(newState)
localStorage.setItem(
`disclosure-${id}`,
newState ? 'expanded' : 'collapsed'
)
}
return (
<div
className={`gc-disclosure ${isExpanded ? 'gc-disclosure--expanded' : ''}`}
>
<button
className="gc-disclosure__header"
onClick={toggle}
aria-expanded={isExpanded}
>
<div className="gc-disclosure__summary">{summary}</div>
<ChevronIcon
className={`gc-disclosure__icon ${isExpanded ? 'rotate-180' : ''}`}
/>
</button>
<AnimatePresence>
{isExpanded && (
<motion.div
className="gc-disclosure__content"
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3 }}
>
{details}
</motion.div>
)}
</AnimatePresence>
</div>
)
}
Accessibility Guidelines¶
Keyboard Navigation¶
- Enter/Space to toggle expanded state
- Tab to navigate between disclosures
- Escape to collapse all (optional)
Screen Reader Support¶
- Announce expanded/collapsed state
- Provide context for what will be revealed
- Use proper ARIA attributes
Visual Indicators¶
- Clear expand/collapse icons
- Smooth animations for state changes
- Maintain focus visibility
Testing Checklist¶
- Correct default states per role
- Smooth expand/collapse animations
- State persistence across sessions
- Keyboard navigation works
- Screen reader announces correctly
- Touch targets are adequate
- Performance with many disclosures
- Progressive loading functions
- Density controls apply correctly
- Mobile experience is optimized
Conclusion¶
Progressive disclosure ensures every user sees the right amount of information for their role and current task. By defaulting to summaries for executives and details for implementers, GetCimple optimizes for both quick decision-making and thorough execution.