HTML <details> and <summary> reference Jump to this section

Common attributes Jump to this section

Basic structure Jump to this section

The <details> element creates a disclosure widget where content can be shown or hidden. The <summary> element provides the visible label/heading. When clicked, it toggles the visibility of the remaining content.

Use for FAQs, collapsible sections, progressive disclosure, or any content that benefits from being hidden by default.

Click to expand

Hidden content appears here when expanded.

<details>
    <summary>Click to expand</summary>
    <p>Hidden content appears here.</p>
</details>

Details attributes Jump to this section

open Jump to this section

Boolean attribute that makes the details element expanded by default. Without this attribute, the element starts collapsed.

Use when the content should be visible initially, or when restoring a previously expanded state.

Already expanded

This content is visible by default because of the open attribute.

<details open>
    <summary>Already expanded</summary>
    <p>This content is visible by default.</p>
</details>

name Jump to this section

Groups multiple details elements together so that only one can be open at a time. When you open a details element with a name, all other details elements with the same name automatically close (accordion behavior).

Use for accordion-style interfaces, questionnaires with mutually exclusive sections, or any grouped content where only one section should be visible at a time.

Question 1

Answer to question 1. Opening another question will close this one.

Question 2

Answer to question 2. Try opening this one!

Question 3

Answer to question 3. Only one can be open at a time.

<details name="faq">
    <summary>Question 1</summary>
    <p>Answer to question 1</p>
</details>
<details name="faq">
    <summary>Question 2</summary>
    <p>Answer to question 2</p>
</details>
<details name="faq">
    <summary>Question 3</summary>
    <p>Answer to question 3</p>
</details>

Accessibility attributes Jump to this section

role Jump to this section

The <details> element has an implicit role of group, and <summary> has a role of button. Generally, you don't need to override these, but they can be customized if needed.

<details role="group">
    <summary role="button">Expandable section</summary>
    <p>Content</p>
</details>

aria-expanded Jump to this section

Automatically managed by the browser. When the details element is open, the summary has aria-expanded="true". When closed, it's aria-expanded="false". You don't need to set this manually.

<!-- Browser automatically sets aria-expanded -->
<details>
    <summary>Section title</summary>
    <p>Content</p>
</details>

aria-label Jump to this section

Provides an accessible label for the details element when additional context is needed beyond the summary text.

<details aria-label="Additional information about pricing">
    <summary>Pricing details</summary>
    <p>Detailed pricing information...</p>
</details>

aria-labelledby Jump to this section

References another element by its id to provide a label for the details element.

<h2 id="pricing-heading">Pricing Information</h2>
<details aria-labelledby="pricing-heading">
    <summary>View pricing tiers</summary>
    <p>Tier 1: $10/month</p>
    <p>Tier 2: $20/month</p>
</details>

aria-describedby Jump to this section

References descriptive text to provide additional context about the details element.

<p id="details-help">Click to expand and see more information</p>
<details aria-describedby="details-help">
    <summary>More info</summary>
    <p>Extended content here</p>
</details>

Common attributes Jump to this section

id Jump to this section

Unique identifier for the details element. Essential for JavaScript selection, CSS targeting, and linking.

<details id="advanced-options">
    <summary>Advanced Options</summary>
    <p>Configuration settings...</p>
</details>
<script>
  const details = document.getElementById('advanced-options');
  details.open = true; // Open programmatically
</script>

class Jump to this section

Assigns CSS class names for styling or JavaScript selection.

<details class="faq-item highlighted">
    <summary class="faq-question">What is your return policy?</summary>
    <p class="faq-answer">We accept returns within 30 days...</p>
</details>

data-* attributes Jump to this section

Custom attributes for storing application-specific data.

<details data-section="billing" data-priority="high" data-viewed="false">
    <summary>Billing Information</summary>
    <p>Payment details and invoices</p>
</details>
<script>
  const details = document.querySelector('details');
  console.log(details.dataset.section);   // "billing"
  console.log(details.dataset.priority);  // "high"
  console.log(details.dataset.viewed);    // "false"
  // Update on toggle
  details.addEventListener('toggle', () => {
      details.dataset.viewed = 'true';
  });
</script>

title Jump to this section

Provides tooltip text on hover. Not accessible to keyboard-only users or touch devices.

<details title="Click to expand for more information">
    <summary>Additional details</summary>
    <p>Content here</p>
</details>

Content structure Jump to this section

Summary content Jump to this section

The summary can contain inline elements, icons, or even nested HTML. However, it should remain focusable and clickable as a whole.

<details>
    <summary>
        <strong>Important:</strong> Read this carefully
    </summary>
    <p>Detailed information...</p>
</details>
<!-- With icon -->
<details>
    <summary>
        <span>Documentation</span>
    </summary>
    <p>User guide content...</p>
</details>

Details content Jump to this section

The content inside <details> (after the <summary>) can contain any HTML elements: paragraphs, lists, images, forms, tables, or even nested details elements.

<details>
    <summary>Product Features</summary>
    <ul>
        <li>Feature 1</li>
        <li>Feature 2</li>
        <li>Feature 3</li>
    </ul>
    <img src="product.jpg" alt="Product image">
</details>

Nested details Jump to this section

Details elements can be nested to create hierarchical disclosure widgets.

Parent section

Parent content here.

Nested section

Nested content here.

<details>
    <summary>Parent section</summary>
    <p>Parent content here.</p>
    <details>
        <summary>Nested section</summary>
        <p>Nested content here.</p>
    </details>
</details>

No summary element Jump to this section

If you omit the <summary> element, browsers will provide a default label (usually "Details"). Always include a summary for better accessibility and user experience.

<!-- Not recommended -->
<details>
    <p>Content without custom summary</p>
</details>
<!-- Recommended -->
<details>
    <summary>Clear descriptive label</summary>
    <p>Content with custom summary</p>
</details>

Styling basics Jump to this section

The <details> element provides built-in expand/collapse functionality with minimal styling required. You can style the summary, the disclosure marker, and the content separately.

Basic styling Jump to this section

Basic styled details

Content goes here with some basic styling applied.

details {
    border: 1px solid #e5e7eb;
    border-radius: 6px;
    padding: 12px;
}
summary {
    cursor: pointer;
    font-weight: 600;
}
details p {
    margin: 12px 0 0 0;
    color: #6b7280;
}

Remove default marker Jump to this section

The disclosure triangle/marker can be hidden using list-style or ::marker pseudo-element.

No marker

The default triangle marker has been removed.

summary {
    list-style: none;
    cursor: pointer;
}
/* Remove marker in WebKit browsers */
summary::-webkit-details-marker {
    display: none;
}
/* Remove marker (modern approach) */
summary::marker {
    display: none;
}

Custom marker/icon Jump to this section

Replace the default marker with custom icons or symbols.

Custom arrow

Click to see the arrow rotate!

summary {
    cursor: pointer;
    list-style: none;
    position: relative;
    padding-left: 24px;
}
summary::before {
    content: '▶';
    position: absolute;
    left: 0;
    transition: transform 0.2s;
}
details[open] summary::before {
    transform: rotate(90deg);
}

Plus/minus icon Jump to this section

+ Expand for more

Content revealed!

summary {
    cursor: pointer;
    list-style: none;
    position: relative;
    padding-left: 30px;
}
summary::before {
    content: '+';
    position: absolute;
    left: 0;
    width: 20px;
    height: 20px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #3b82f6;
    color: white;
    border-radius: 3px;
    font-weight: bold;
}
details[open] summary::before {
    content: '−';
}

Styled examples Jump to this section

Card style Jump to this section

Card-style details

Content in a nice card layout with shadow and rounded corners.

details {
    background: white;
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    padding: 0;
    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
    overflow: hidden;
}
summary {
    cursor: pointer;
    list-style: none;
    padding: 16px;
    background: #f9fafb;
    font-weight: 600;
    border-bottom: 1px solid #e5e7eb;
}
details > div {
    padding: 16px;
}

Accordion style Jump to this section

Section 1

Content for section 1.

Section 2

Content for section 2.

Section 3

Content for section 3.

.accordion {
    border: 1px solid #e5e7eb;
    border-radius: 6px;
    overflow: hidden;
}
.accordion details {
    border-bottom: 1px solid #e5e7eb;
    margin: 0;
}
.accordion details:last-child {
    border-bottom: none;
}
.accordion summary {
    cursor: pointer;
    list-style: none;
    padding: 14px 16px;
    background: white;
    font-weight: 500;
    transition: background 0.2s;
}
.accordion summary:hover {
    background: #f9fafb;
}
.accordion details > div {
    padding: 14px 16px;
    background: #fafafa;
}

FAQ style Jump to this section

Q What is your refund policy?

We offer a full refund within 30 days of purchase, no questions asked.

Q How long does shipping take?

Standard shipping takes 5-7 business days. Express shipping available.

.faq-item {
    border-bottom: 1px solid #e5e7eb;
    padding: 16px 0;
}
.faq-item summary {
    cursor: pointer;
    list-style: none;
    font-weight: 600;
    font-size: 16px;
    color: #1f2937;
    position: relative;
    padding-left: 28px;
}
.faq-item summary::before {
    content: 'Q';
    position: absolute;
    left: 0;
    color: #3b82f6;
    font-size: 20px;
}
.faq-item p {
    margin: 12px 0 0 28px;
    color: #6b7280;
    line-height: 1.6;
}

Colored border Jump to this section

Blue theme

Content with blue accent.

Green theme

Content with green accent.

Orange theme

Content with orange accent.

.details-blue {
    border-left: 4px solid #3b82f6;
    background: #eff6ff;
    padding: 12px 16px;
    border-radius: 4px;
}
.details-blue summary {
    color: #1e40af;
}
.details-green {
    border-left: 4px solid #10b981;
    background: #d1fae5;
}
.details-orange {
    border-left: 4px solid #f59e0b;
    background: #fef3c7;
}

Animated content Jump to this section

Click to see animation

This content slides down smoothly!

details[open] > div {
    animation: slideDown 0.3s ease-out;
}
@keyframes slideDown {
    from {
        opacity: 0;
        transform: translateY(-10px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

Summary hover effects Jump to this section

Hover over me

The summary changes color on hover!

summary {
    cursor: pointer;
    list-style: none;
    padding: 14px 16px;
    background: white;
    font-weight: 500;
    transition: all 0.2s;
}
summary:hover {
    background: #3b82f6;
    color: white;
}
summary:focus {
    outline: 2px solid #3b82f6;
    outline-offset: 2px;
}

Open state styling Jump to this section

Click to open

This details element changes appearance when open.

details {
    border: 1px solid #d1d5db;
    border-radius: 6px;
    padding: 12px;
    transition: all 0.2s;
}
details[open] {
    border-color: #3b82f6;
    background: #eff6ff;
}
details[open] summary {
    color: #1e40af;
}

Spacing and padding Jump to this section

/* Add space between content and summary */
details > *:not(summary) {
    margin-top: 12px;
}
/* Indent content */
details > *:not(summary) {
    margin-left: 24px;
}
/* Full-width summary with padding */
summary {
    padding: 16px;
    margin: -12px -16px 0;
}

JavaScript patterns Jump to this section

The <details> element fires a toggle event when opened or closed, making it easy to track state changes and create interactive behaviors.

Detect open/closed state Jump to this section

Check if a details element is open using the open property.

Click to toggle

Content is now visible!

const details = document.getElementById('my-details');
// Check if open
if (details.open) {
    console.log('Details is open');
} else {
    console.log('Details is closed');
}
// Get open property value
const isOpen = details.open; // true or false

Open and close programmatically Jump to this section

Control the details element's state with JavaScript.

Controlled details

This can be controlled with buttons.

const details = document.getElementById('my-details');
// Open
details.open = true;
// Close
details.open = false;
// Toggle
details.open = !details.open;

Listen for toggle events Jump to this section

The toggle event fires whenever the details element opens or closes.

Toggle me

Watch the message below!

Not toggled yet
const details = document.getElementById('my-details');
const output = document.getElementById('output');
details.addEventListener('toggle', (e) => {
    if (details.open) {
        output.textContent = 'Details opened!';
        console.log('User opened the details');
    } else {
        output.textContent = 'Details closed!';
        console.log('User closed the details');
    }
});
// Event object properties
details.addEventListener('toggle', (e) => {
    console.log('Target:', e.target);
    console.log('Is open:', e.target.open);
});

Open all / Close all Jump to this section

Control multiple details elements at once.

Section 1

Content for section 1.

Section 2

Content for section 2.

Section 3

Content for section 3.

// Open all details in container
function openAll(containerSelector) {
    const container = document.querySelector(containerSelector);
    container.querySelectorAll('details').forEach(details => {
        details.open = true;
    });
}
// Close all details
function closeAll(containerSelector) {
    const container = document.querySelector(containerSelector);
    container.querySelectorAll('details').forEach(details => {
        details.open = false;
    });
}
// Usage
openAll('.accordion');
closeAll('.faq-section');

Track which details are open Jump to this section

Keep track of open/closed state for all details elements.

Item A

Content A

Item B

Content B

Item C

Content C

Open items: 0
const container = document.getElementById('track-group');
const output = document.getElementById('track-output');
function updateCount() {
    const openDetails = container.querySelectorAll('details[open]');
    output.textContent = `Open items: ${openDetails.length}`;
}
// Listen to all details elements
container.querySelectorAll('details').forEach(details => {
    details.addEventListener('toggle', updateCount);
});
// Initial count
updateCount();

Save state to localStorage Jump to this section

Persist the open/closed state across page reloads.

Persistent details

This will remember if you opened it! Try refreshing the page after opening.

State is saved to localStorage
const details = document.getElementById('my-details');
const storageKey = 'details-state';
// Restore state on page load
const savedState = localStorage.getItem(storageKey);
if (savedState === 'open') {
    details.open = true;
}
// Save state on toggle
details.addEventListener('toggle', () => {
    localStorage.setItem(
        storageKey, 
        details.open ? 'open' : 'closed'
    );
});
// For multiple details elements
function saveAllStates() {
    const states = {};
    document.querySelectorAll('details[id]').forEach(d => {
        states[d.id] = d.open;
    });
    localStorage.setItem('all-details', JSON.stringify(states));
}
function restoreAllStates() {
    const states = JSON.parse(
        localStorage.getItem('all-details') || '{}'
    );
    Object.keys(states).forEach(id => {
        const details = document.getElementById(id);
        if (details) details.open = states[id];
    });
}

Accordion behavior (manual) Jump to this section

Create accordion behavior without using the name attribute (for older browsers or custom logic).

Panel 1

Only one panel can be open at a time.

Panel 2

Opening this closes the others.

Panel 3

Try opening different panels!

const accordion = document.getElementById('accordion');
const allDetails = accordion.querySelectorAll('details');
allDetails.forEach(details => {
    details.addEventListener('toggle', () => {
        if (details.open) {
            // Close all other details
            allDetails.forEach(other => {
                if (other !== details) {
                    other.open = false;
                }
            });
        }
    });
});

Animate with JavaScript Jump to this section

Create smooth animations by controlling height.

Smooth animation

This content animates smoothly when opening and closing!

const details = document.getElementById('animate-details');
const content = details.querySelector('div');
details.addEventListener('toggle', () => {
    if (details.open) {
        // Opening
        const height = content.scrollHeight;
        content.style.height = '0px';
        requestAnimationFrame(() => {
            content.style.transition = 'height 0.3s ease';
            content.style.height = height + 'px';
        });
        content.addEventListener('transitionend', function handler() {
            content.style.height = 'auto';
            content.removeEventListener('transitionend', handler);
        });
    } else {
        // Closing
        const height = content.scrollHeight;
        content.style.height = height + 'px';
        requestAnimationFrame(() => {
            content.style.transition = 'height 0.3s ease';
            content.style.height = '0px';
        });
    }
});

Prevent closing Jump to this section

Prevent a details element from being closed under certain conditions.

Try to close me!

This details element can't be closed. The toggle event is being prevented.

const details = document.getElementById('prevent-close');
details.addEventListener('toggle', (e) => {
    if (!details.open) {
        // Prevent closing
        e.preventDefault();
        details.open = true;
    }
});
// Or with a condition
details.addEventListener('toggle', (e) => {
    const hasUnsavedChanges = true; // Your condition
    if (!details.open && hasUnsavedChanges) {
        e.preventDefault();
        details.open = true;
        alert('Please save your changes first!');
    }
});

Count toggles Jump to this section

Track how many times a details element has been toggled.

Toggle counter

Keep toggling to see the count increase!

Toggle count: 0
const details = document.getElementById('count-toggles');
const display = document.getElementById('count-display');
let count = 0;
details.addEventListener('toggle', () => {
    count++;
    display.textContent = `Toggle count: ${count}`;
    // Track separately for opens and closes
    if (details.open) {
        console.log(`Opened ${count} times`);
    } else {
        console.log(`Closed ${count} times`);
    }
});

Dynamic content loading Jump to this section

Load content when details is opened for the first time.

Load content on demand
Loading...
const details = document.getElementById('lazy-load');
const contentDiv = document.getElementById('lazy-content');
let loaded = false;
details.addEventListener('toggle', async () => {
    if (details.open && !loaded) {
        // Simulate API call
        contentDiv.textContent = 'Loading...';
        // Replace with actual fetch
        await new Promise(resolve => setTimeout(resolve, 1000));
        contentDiv.innerHTML = `
            

Loaded Content

This was loaded dynamically!

`; loaded = true; } }); // With actual fetch details.addEventListener('toggle', async () => { if (details.open && !loaded) { try { const response = await fetch('/api/content'); const data = await response.json(); contentDiv.innerHTML = data.html; loaded = true; } catch (error) { contentDiv.textContent = 'Failed to load content'; } } });

Get all open details Jump to this section

Find all open details elements on the page.

// Get all open details
const openDetails = document.querySelectorAll('details[open]');
console.log(`${openDetails.length} details are open`);
// Get array of IDs
const openIds = Array.from(openDetails)
    .map(d => d.id)
    .filter(Boolean);
// Get array of summary text
const openSummaries = Array.from(openDetails)
    .map(d => d.querySelector('summary').textContent);
// Check if any details are open
const hasOpenDetails = document.querySelector('details[open]') !== null;