Home / Snippets / UI Components /
Expandable card
Cards that reveal hidden content via <details>/<summary> — browser-native toggle, no JavaScript.
What is the cascade?
The cascade is the algorithm CSS uses to resolve conflicts when multiple rules target the same element. It weighs origin, specificity, and source order to determine which declaration wins. Understanding it is the foundation of writing predictable CSS.
What are cascade layers?
Cascade layers (@layer) let you explicitly control the order of specificity battles by grouping rules into named layers. Rules in a later layer always beat earlier ones, regardless of selector specificity — giving you full control over the cascade without resorting to !important.
Quick implementation
/* HTML:
<details class="exp-card">
<summary>Card title</summary>
<div class="exp-card-body">Hidden content here.</div>
</details> */
.exp-card {
background: oklch(0.19 0.02 260);
border-radius: 0.75rem;
border: 1px solid oklch(0.28 0.02 260);
overflow: hidden;
}
.exp-card summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.25rem;
cursor: pointer;
font-weight: 600;
list-style: none;
user-select: none;
}
.exp-card summary::-webkit-details-marker { display: none; }
.exp-card summary::after {
content: '';
width: 0.625rem;
height: 0.625rem;
border-right: 2px solid oklch(0.72 0.19 265);
border-bottom: 2px solid oklch(0.72 0.19 265);
transform: rotate(45deg);
flex-shrink: 0;
transition: transform 0.25s ease;
}
.exp-card[open] summary::after {
transform: rotate(-135deg);
}
.exp-card summary:focus-visible {
outline: 2px solid oklch(0.72 0.19 265);
outline-offset: -2px;
}
.exp-card-body {
padding: 0 1.25rem 1.25rem;
color: oklch(0.63 0.02 260);
font-size: 0.9rem;
line-height: 1.7;
}
/* Progressive enhancement: smooth height animation
where interpolate-size is supported (Chrome 129+) */
@supports (interpolate-size: allow-keywords) {
.exp-card {
interpolate-size: allow-keywords;
}
.exp-card-body {
height: 0;
overflow: hidden;
transition: height 0.3s ease;
}
.exp-card[open] .exp-card-body {
height: auto;
}
}
@media (prefers-reduced-motion: reduce) {
.exp-card summary::after,
.exp-card-body {
transition: none;
}
}
Prompt this to your LLM
Includes role, constraints, two framework variants, and edge cases to handle.
You are a senior frontend engineer building accessible disclosure widgets.
Goal: An expandable card component built on native <details>/<summary> elements with a rotating chevron indicator and smooth height animation. No JavaScript.
Technical constraints:
- Use <details> and <summary> as the structural base — not div+button with JS toggling.
- Hide the default disclosure marker with list-style: none and ::-webkit-details-marker { display: none }.
- Build the chevron using a square ::after pseudo-element with two borders and transform: rotate(45deg), rotating to -135deg when [open].
- Use @supports (interpolate-size: allow-keywords) to progressively enable height: auto transitions.
- Use oklch() for all color values.
- Apply overflow: hidden on the card to cleanly clip content during height animation.
- Wrap all transitions in @media (prefers-reduced-motion: reduce).
Framework variant (pick one):
A) Vanilla HTML + CSS only — no JS, works in all modern browsers.
B) React component — accepts title, children, and defaultOpen props; uses the native details element as its root rather than a synthetic toggle.
Edge cases to handle:
- Browsers without interpolate-size support: card still opens/closes instantly without broken layout.
- Long summary text: chevron should not wrap to next line — use flex with flex-shrink: 0 on the indicator.
- Nested content: padding on exp-card-body must not collapse with the card border in closed state.
- Focus management: focus-visible ring should be inset on summary, not clipped by card overflow: hidden.
Return HTML + CSS.
Why this matters in 2026
FAQ sections, settings panels, and content-heavy cards all need a pattern for hiding and revealing information on demand. The <details>/<summary> HTML elements give you this interaction — with keyboard support, screen-reader announcements, and an open/closed state — built into the browser. No JavaScript state management, no ARIA juggling. CSS 2026 adds interpolate-size: allow-keywords to unlock height transitions to auto, finally solving the long-running "you can't transition to height: auto" problem.
The logic
When a <details> element has the open attribute, its content is rendered; otherwise it is hidden. CSS targets the [open] attribute selector to rotate the chevron 180° — from a down-pointing arrow to an up-pointing one — signalling state without text. The height animation relies on interpolate-size: allow-keywords set on the card, which tells the browser to interpolate between height: 0 and height: auto. This is wrapped in @supports so browsers without the feature still get the functional but instant toggle.
Accessibility & performance
The <summary> element is natively focusable and announced by screen readers as a button with expanded/collapsed state — you get full ARIA semantics without writing a single ARIA attribute. The chevron indicator is a CSS pseudo-element with no text content, so it is correctly ignored by assistive technology. The :focus-visible ring uses a negative outline-offset to stay inside the card boundary even with overflow: hidden. Height and transform animations are GPU-composited, keeping the expand/collapse at 60fps without triggering layout recalculation on sibling elements.