Accordion and Expansion Panel in pure CSS
Created:
Recently I needed to build a simple FAQ page for an SSG website and wanted to make it as light and simple as possible. So, I quickly looked for ‘accordion’ and ‘expansion panels’ examples in popular libraries. No one of them used <details>
tag. All went with a bunch of <div>
’s with <button>
and obviously applied some JS. Even though <details>
is especially suited for that kind of thing, and with 0 JS.
Yes, I know we can’t animate details’s height on state change (open-close) without JS, which is definitely an annoyance, but is it really necessary?
<details>
is a browser native solution, and a good one. Should I not forget it!
Now, let’s build an accordion with <details>
. Here Demo and Repo
First, we would need some HTML.
<details>
<summary>
<span>Some text ...</span>
<svg class="plus" viewBox="0 0 16 16">
<path
d="M8 0a1 1 0 0 1 1 1v6h6a1 1 0 1 1 0 2H9v6a1 1 0 1 1-2 0V9H1a1 1 0 0 1 0-2h6V1a1 1 0 0 1 1-1z"
/>
</svg>
</summary>
<div class="content">
<span>Some text ...</span>
</div>
</details>
SVG is ➕ icon on closed state, and will be rotated 45deg to ❌ in open state.
To get rid of triangle marker is suffice to change display
to flex
or grid
on <summary>
Let’s define variables. It helps me to update / finetune styling.
:root {
--text-color: hsl(0, 0%, 20%);
--bg-color: hsl(0, 0%, 100%);
--hs-base: 300, 100%;
--primary: hsl(var(--hs-base), 15%);
--summary-bg-color: hsl(var(--hs-base), 98%);
--summary-hover-bg-color: hsl(var(--hs-base), 90%);
--border: 3px solid hsl(var(--hs-base), 80%);
--border-radius: 0.66rem;
--padding-x: clamp(1rem, 5%, 2.5rem);
--transition-duration: 300ms;
--transition-timing-function: ease-in;
}
<details>
tag doesn’t need a lot of styling, just add borders.
details {
overflow: hidden;
border: var(--border);
border-top: none;
}
details:first-child {
border: var(--border);
border-radius: var(--border-radius) var(--border-radius) 0 0;
}
details:last-child {
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
And now <details>
’s children elements
summary {
display: grid;
grid-template-columns: 1fr 1em;
align-items: center;
gap: 2em;
font-size: calc(1rem + 2px);
font-weight: bold;
cursor: pointer; /* I wonder why is this not default behavior */
background-color: var(--summary-bg-color);
padding: 1.25rem var(--padding-x);
transition-property: color, background-color;
}
details[open] > summary {
font-style: italic;
}
summary:focus,
summary:hover {
outline: none;
background-color: var(--summary-hover-bg-color);
color: var(--primary); /* Icon also will change color */
}
details .content {
position: relative;
z-index: -1; /* for animation, hide behind summary */
background-color: var(--bg-color);
color: var(--text-color);
padding: 1px var(--padding-x);
}
svg {
width: 1em;
height: 1em;
fill: currentColor;
}
details[open] svg.plus {
transform: rotate(45deg);
transition-property: transform;
}
Nice thing about SVG in HTML is that we can set fill: currentColor
, which would be impossible with background-image: url("data:image/svg+xml, ...")
or <img src=”x.svg”>
.
Some animation if dear user is ok with it.
@keyframes appear {
0% {
opacity: 0;
transform: translateY(max(-2rem, -100%));
}
100% {
opacity: 1;
transform: translateY(1);
}
}
@media (prefers-reduced-motion: no-preference) {
details * {
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-timing-function);
}
details[open] .content {
animation: appear var(--transition-duration) var(
--transition-timing-function
);
}
}