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
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
.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
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 falseOpen 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!
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
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.
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
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!
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
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;
