Skip to content

πŸ“¦ 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.