CSS Accordions: 16 Examples, From details to ARIA Buttons

You can build a CSS accordion with two HTML elements and no script. Details wraps each item, summary holds the label, and a shared name attribute keeps one panel open at a time.

Below are 16 CSS accordion examples you can try and copy, followed by a guide to methods, animated height, accessibility and find-in-page support.

By Bogdan Sandu · Updated October 26, 2026

Accordions built on details and summary

Six accordions with no script at all. The browser handles the toggle, the keyboard and the open state, and the CSS only restyles the summary, the marker and the panel. Two of them use the name attribute to keep one panel open at a time.

Help center FAQ with a custom marker

  • details / summary
  • ::-webkit-details-marker
  • content alt text

Shipping and returns

Quick answers from the Parcelry support team.

How long does standard delivery take?

Orders placed before 2 pm ship the same day. Standard delivery arrives in 2 to 4 working days, and the tracking link lands in your inbox as soon as the label prints.

Can I change the address after ordering?

Yes, until the parcel leaves our warehouse. Open the order in your account and choose Edit address, or reply to the confirmation email.

What is your return window?

You have 30 days from delivery to send unworn items back. Print a free label from the returns page and drop the parcel at any partner locker.

When will my refund arrive?

We refund within two working days of the parcel reaching us. Your bank may take another three to five days to show it.

Click a header to open or close it.

The default triangle goes away with list-style: none plus the WebKit marker rule, and summary::after draws a square badge that swaps a plus for a minus when the item is open. The content value uses the slash syntax for alternative text, so screen readers skip the symbol and announce only the question and the expanded state.

View code
.ac-faq {
  box-sizing: border-box;
  width: min(100%, 580px);
  padding: 28px clamp(18px, 5vw, 34px) 10px;
  border-radius: 18px;
  background: #fff;
  color: #1b1f2e;
  box-shadow: 0 1px 2px rgba(20, 24, 40, .06), 0 14px 34px rgba(20, 24, 40, .08);
}
.ac-faq__title {
  margin: 0 0 6px;
  font-family: "Fraunces", Georgia, serif;
  font-size: 26px;
  font-weight: 600;
  line-height: 1.2;
}
.ac-faq__sub {
  margin: 0 0 18px;
  color: #535a6f;
  font-size: 15px;
}
.ac-faq__item {
  border-top: 1px solid #e3e6ef;
}
.ac-faq__item summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 16px 0;
  border-radius: 6px;
  list-style: none;
  font-size: 16px;
  font-weight: 600;
  line-height: 1.4;
  cursor: pointer;
}
.ac-faq__item summary::-webkit-details-marker {
  display: none;
}
.ac-faq__item summary::after {
  content: "+";
  content: "+" / "";
  display: grid;
  flex: none;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: 9px;
  background: #edf0ff;
  color: #2b3bc4;
  font-size: 20px;
  font-weight: 600;
  line-height: 1;
}
.ac-faq__item[open] summary::after {
  content: "\2212";
  content: "\2212" / "";
  background: #2b3bc4;
  color: #fff;
}
.ac-faq__item summary:hover {
  color: #2b3bc4;
}
.ac-faq__item summary:focus-visible {
  outline: 3px solid #2b3bc4;
  outline-offset: 4px;
}
.ac-faq__body p {
  margin: 0 0 18px;
  padding-right: min(46px, 8%);
  color: #474d60;
  font-size: 15px;
  line-height: 1.6;
}
@media (prefers-reduced-motion: no-preference) {
  .ac-faq__item summary::after {
    transition: background-color .2s ease, color .2s ease;
  }
}

Exclusive accordion with the name attribute

  • details name
  • exclusive accordion
  • rotated chevron

Before you book

Harbor Lane Inn house policies

Check-in and check-outFrom 3 pm

Check-in runs from 3 pm to 10 pm at the front desk. Check-out is by 11 am, and we can hold your bags until the evening ferry.

CancellationFree until 48 h

Cancel up to 48 hours before arrival for a full refund. Later cancellations are charged the first night.

PetsDogs welcome

Two garden rooms take dogs up to 25 kg for a small cleaning fee. Let us know when you book so we can set out a bed and bowls.

Parking8 spaces

Free parking behind the inn, first come first served. Overflow parking is at the harbor lot, a four minute walk away.

Click a header to open or close it.

All four details elements share name="ac-stay", so opening one closes whichever was open. No script and no radio inputs are involved. Browsers older than Chrome 120, Safari 17.2 or Firefox 130 ignore the attribute and simply let several panels stay open, which is a safe fallback.

View code
.ac-stay {
  box-sizing: border-box;
  width: min(100%, 540px);
  border: 1px solid #d9d2c3;
  border-radius: 14px;
  background: #fbf8f2;
  color: #1f2b2a;
  overflow: hidden;
}
.ac-stay__head {
  padding: 22px 22px 18px;
  background: #0f4c4a;
  color: #f4efe4;
}
.ac-stay__title {
  margin: 0;
  font-family: "Fraunces", Georgia, serif;
  font-size: 24px;
  font-weight: 600;
  line-height: 1.2;
}
.ac-stay__sub {
  margin: 4px 0 0;
  color: #cfe0dc;
  font-size: 14px;
}
.ac-stay__item + .ac-stay__item {
  border-top: 1px solid #e4ddcf;
}
.ac-stay__item summary {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 12px;
  padding: 16px 22px;
  list-style: none;
  cursor: pointer;
}
.ac-stay__item summary::-webkit-details-marker {
  display: none;
}
.ac-stay__item summary::after {
  content: "";
  width: 8px;
  height: 8px;
  margin-left: 4px;
  border-right: 2px solid #0f4c4a;
  border-bottom: 2px solid #0f4c4a;
  transform: translateY(-2px) rotate(45deg);
}
.ac-stay__item[open] summary::after {
  transform: translateY(2px) rotate(225deg);
}
.ac-stay__q {
  font-weight: 600;
}
.ac-stay__meta {
  padding: 3px 9px;
  border-radius: 99px;
  background: #e5eeec;
  color: #0f4c4a;
  font-size: 13px;
  font-weight: 600;
}
.ac-stay__item summary:hover .ac-stay__q {
  text-decoration: underline;
  text-underline-offset: 3px;
}
.ac-stay__item summary:focus-visible {
  outline: 3px solid #c2551a;
  outline-offset: -3px;
}
.ac-stay__a {
  margin: 0;
  padding: 0 22px 18px;
  color: #3d4847;
  font-size: 15px;
  line-height: 1.6;
}
@media (prefers-reduced-motion: no-preference) {
  .ac-stay__item summary::after {
    transition: transform .25s ease;
  }
}

Animated open and close with ::details-content

  • ::details-content
  • interpolate-size
  • @supports

Brown butter banana bread

1 loaf20 min prep55 min bake

Ingredients8 items
  • 115 g unsalted butter
  • 3 very ripe bananas
  • 150 g light brown sugar
  • 2 eggs, room temperature
  • 1 tsp vanilla extract
  • 220 g plain flour
  • 1 tsp baking soda, pinch of salt
  • 60 g toasted walnuts
Method5 steps
  1. Melt the butter and cook it until it smells nutty and turns amber. Let it cool for 10 minutes.
  2. Mash the bananas, then whisk in the butter, sugar, eggs and vanilla.
  3. Fold in the flour, soda and salt until no dry streaks remain, then add the walnuts.
  4. Scrape into a lined loaf tin and bake at 175 C for 50 to 55 minutes.
  5. Cool in the tin for 15 minutes before turning out.
StorageKeeps 4 days

Wrap the cooled loaf and keep it at room temperature for up to four days, or freeze slices for a month and toast them from frozen.

Click a header to open or close it.

interpolate-size: allow-keywords on the wrapper lets height transition to auto, and ::details-content is the box that holds everything after the summary. Transitioning content-visibility with allow-discrete keeps the content visible until the closing animation ends. Both rules sit inside @supports and a no-preference motion query, so Firefox and Safari get an instant, working toggle.

View code
.ac-recipe {
  box-sizing: border-box;
  width: min(100%, 520px);
  padding: 24px clamp(18px, 5vw, 30px) 8px;
  border: 1px solid #ead9c1;
  border-radius: 16px;
  background: #fffaf2;
  color: #3b2614;
  interpolate-size: allow-keywords;
}
.ac-recipe__head {
  padding-bottom: 14px;
}
.ac-recipe__title {
  margin: 0;
  font-family: "Fraunces", Georgia, serif;
  font-size: 26px;
  font-weight: 600;
  line-height: 1.2;
}
.ac-recipe__facts {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 12px 0 0;
}
.ac-recipe__facts span {
  padding: 4px 10px;
  border-radius: 99px;
  background: #f3e4cd;
  color: #5a3a1c;
  font-size: 13px;
  font-weight: 600;
}
.ac-recipe__item {
  border-top: 1px solid #ead9c1;
}
.ac-recipe__item summary {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 0;
  list-style: none;
  font-family: "Fraunces", Georgia, serif;
  font-size: 20px;
  font-weight: 600;
  cursor: pointer;
}
.ac-recipe__item summary::-webkit-details-marker {
  display: none;
}
.ac-recipe__item summary::before {
  content: "";
  flex: none;
  width: 8px;
  height: 8px;
  border-right: 2px solid #b0481a;
  border-bottom: 2px solid #b0481a;
  transform: rotate(-45deg);
}
.ac-recipe__item[open] summary::before {
  transform: rotate(45deg);
}
.ac-recipe__item summary:focus-visible {
  outline: 3px solid #b0481a;
  outline-offset: 2px;
  border-radius: 4px;
}
.ac-recipe__count {
  margin-left: auto;
  color: #765537;
  font-family: "DM Sans", system-ui, sans-serif;
  font-size: 13px;
  font-weight: 600;
}
.ac-recipe__list,
.ac-recipe__steps {
  margin: 0;
  padding: 0 0 18px 26px;
  font-size: 15px;
  line-height: 1.6;
}
.ac-recipe__list {
  columns: 2 180px;
  column-gap: 28px;
}
.ac-recipe__steps li + li {
  margin-top: 6px;
}
.ac-recipe__steps li::marker {
  color: #b0481a;
  font-weight: 700;
}
.ac-recipe__text {
  margin: 0;
  padding: 0 0 18px 20px;
  font-size: 15px;
  line-height: 1.6;
}
@media (prefers-reduced-motion: no-preference) {
  .ac-recipe__item summary::before {
    transition: transform .3s ease;
  }
  @supports (interpolate-size: allow-keywords) and selector(::details-content) {
    .ac-recipe__item::details-content {
      height: 0;
      overflow: clip;
      transition: height .4s ease, content-visibility .4s allow-discrete;
    }
    .ac-recipe__item[open]::details-content {
      height: auto;
    }
  }
}

Nested docs sidebar

  • nested details
  • two name groups
  • aria-current

Click a header to open or close it.

A documentation sidebar where top-level sections and the subsections inside Guides are both exclusive. The outer details share one name and the inner ones share a different name, so opening a subsection never closes its parent. The current page link carries aria-current="page" and gets a visible marker.

View code
.ac-docs {
  box-sizing: border-box;
  width: min(100%, 300px);
  padding: 18px 12px;
  border: 1px solid #dfe3ea;
  border-radius: 14px;
  background: #fff;
  color: #20252f;
  font-size: 15px;
}
.ac-docs__brand {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 0 8px 12px;
  font-weight: 700;
}
.ac-docs__brand span {
  padding: 2px 8px;
  border-radius: 6px;
  background: #eef1f6;
  color: #4b5466;
  font-family: ui-monospace, Consolas, monospace;
  font-size: 12px;
  font-weight: 500;
}
.ac-docs summary {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 36px;
  padding: 0 8px;
  border-radius: 8px;
  list-style: none;
  font-weight: 600;
  cursor: pointer;
}
.ac-docs summary::-webkit-details-marker {
  display: none;
}
.ac-docs summary::before {
  content: "";
  flex: none;
  width: 6px;
  height: 6px;
  border-right: 2px solid #6b7488;
  border-bottom: 2px solid #6b7488;
  transform: rotate(-45deg);
}
.ac-docs details[open] > summary::before {
  transform: rotate(45deg);
}
.ac-docs summary:hover {
  background: #f2f4f8;
}
.ac-docs summary:focus-visible,
.ac-docs a:focus-visible {
  outline: 2px solid #3a5bd9;
  outline-offset: 1px;
}
.ac-docs__sub > summary {
  font-weight: 500;
  color: #384152;
}
.ac-docs__links {
  margin: 2px 0 6px 11px;
  padding: 0 0 0 10px;
  border-left: 1px solid #e2e6ee;
  list-style: none;
}
.ac-docs__links a {
  display: flex;
  align-items: center;
  min-height: 32px;
  padding: 0 10px;
  border-radius: 8px;
  color: #3f4859;
  text-decoration: none;
}
.ac-docs__links a:hover {
  background: #f2f4f8;
  color: #20252f;
}
.ac-docs__links a[aria-current="page"] {
  background: #ebefff;
  color: #2a46c2;
  font-weight: 600;
  box-shadow: inset 3px 0 0 #2a46c2;
}
@media (prefers-reduced-motion: no-preference) {
  .ac-docs summary::before {
    transition: transform .2s ease;
  }
}

Dark pricing FAQ

  • details name
  • container queries
  • dark theme

Billing questions

Plans, seats and invoices in plain terms. Still stuck? Talk to our team.

Can I switch plans mid-cycle?

Yes. Upgrades apply right away and we charge the prorated difference. Downgrades start at your next renewal, so you keep what you paid for.

What counts as a seat?

Anyone who can edit a board. Viewers and guests with comment access are free on every plan.

Do you offer refunds on annual plans?

Within 14 days of purchase, in full. After that we refund unused months if you close the workspace.

Is there a nonprofit discount?

Registered nonprofits and schools get 40 percent off any paid plan. Send us your registration number to apply it.

Click a header to open or close it.

A billing FAQ for a pricing page on a dark theme. The list is exclusive through a shared name, and a container query moves the intro beside the questions only when the component itself has room, not the viewport. The open item gets a filled lime icon and a lighter surface so the state reads without relying on the rotation alone.

View code
.ac-bill {
  box-sizing: border-box;
  width: min(100%, 760px);
  container-type: inline-size;
  color: #e9edf2;
}
.ac-bill__grid {
  display: grid;
  gap: 22px;
}
@container (min-width: 600px) {
  .ac-bill__grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.7fr);
    gap: 36px;
  }
}
.ac-bill__title {
  margin: 0 0 10px;
  font-family: "Fraunces", Georgia, serif;
  font-size: 28px;
  font-weight: 600;
  line-height: 1.15;
}
.ac-bill__intro p {
  margin: 0;
  color: #a9b2bf;
  font-size: 15px;
  line-height: 1.6;
}
.ac-bill__intro a {
  color: #d4f05c;
  text-underline-offset: 3px;
}
.ac-bill__intro a:focus-visible {
  outline: 2px solid #d4f05c;
  outline-offset: 2px;
}
.ac-bill__list {
  display: grid;
  gap: 8px;
}
.ac-bill__item {
  border: 1px solid #262c35;
  border-radius: 12px;
  background: #14181e;
}
.ac-bill__item[open] {
  border-color: #353d49;
  background: #1b2028;
}
.ac-bill__item summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  position: relative;
  padding: 14px 14px 14px 18px;
  border-radius: 12px;
  list-style: none;
  font-weight: 600;
  line-height: 1.4;
  cursor: pointer;
}
.ac-bill__item summary::-webkit-details-marker {
  display: none;
}
.ac-bill__item summary::after {
  content: "";
  flex: none;
  width: 30px;
  height: 30px;
  border: 1px solid #3a424f;
  border-radius: 50%;
  background: #1f252e;
  box-shadow: inset 0 0 0 30px #1f252e;
}
.ac-bill__item summary::before {
  content: "";
  position: absolute;
  right: 26px;
  top: 50%;
  z-index: 1;
  width: 7px;
  height: 7px;
  margin-top: -6px;
  border-right: 2px solid #e9edf2;
  border-bottom: 2px solid #e9edf2;
  transform: rotate(45deg);
}
.ac-bill__item[open] summary::after {
  border-color: #d4f05c;
  box-shadow: inset 0 0 0 30px #d4f05c;
}
.ac-bill__item[open] summary::before {
  margin-top: -2px;
  border-color: #14181e;
  transform: rotate(225deg);
}
.ac-bill__item summary:focus-visible {
  outline: 2px solid #d4f05c;
  outline-offset: 2px;
}
.ac-bill__a {
  margin: 0;
  padding: 0 18px 16px;
  color: #b7bfcb;
  font-size: 15px;
  line-height: 1.6;
}
@media (prefers-reduced-motion: no-preference) {
  .ac-bill__item summary::before {
    transition: transform .25s ease, margin-top .25s ease;
  }
  .ac-bill__item summary::after {
    transition: box-shadow .25s ease, border-color .25s ease;
  }
}

Three icon morphs: plus, chevron, arrow

  • pseudo-element bars
  • inline SVG
  • details[open]
Offline mode

Notes and tasks sync the moment you reconnect, with edits merged line by line.

Shared spaces

Invite up to 50 people per space and set each one to edit, comment or view.

Audit log

Every change is stored for 400 days and can be exported as CSV for review.

Click a header to open or close it.

Three feature cards, each with a different open icon to copy. The plus is two bars where the vertical one rotates flat into a minus, the chevron is an SVG polyline that flips, and the arrow turns a quarter and fills its circle. All three are driven by details[open], so the icon can never disagree with the real state.

View code
.ac-icons {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  align-items: start;
  gap: 14px;
  width: min(100%, 760px);
}
.ac-icons__card {
  border: 1px solid #dcdfe6;
  border-radius: 14px;
  background: #fff;
  color: #1c2130;
}
.ac-icons__card[open] {
  border-color: #1c2130;
}
.ac-icons__card summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 56px;
  padding: 0 16px 0 18px;
  border-radius: 14px;
  list-style: none;
  font-weight: 700;
  cursor: pointer;
}
.ac-icons__card summary::-webkit-details-marker {
  display: none;
}
.ac-icons__card summary:focus-visible {
  outline: 3px solid #e2503a;
  outline-offset: 2px;
}
.ac-icons__text {
  margin: 0;
  padding: 0 18px 18px;
  color: #4a5063;
  font-size: 15px;
  line-height: 1.55;
}
.ac-icons__plus {
  position: relative;
  flex: none;
  width: 18px;
  height: 18px;
}
.ac-icons__plus::before,
.ac-icons__plus::after {
  content: "";
  position: absolute;
  top: 8px;
  left: 0;
  width: 18px;
  height: 2px;
  border-radius: 2px;
  background: #e2503a;
}
.ac-icons__plus::after {
  transform: rotate(90deg);
}
.ac-icons__card[open] .ac-icons__plus::after {
  transform: rotate(0deg);
}
.ac-icons__chev {
  flex: none;
  fill: none;
  stroke: #1c2130;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.ac-icons__card[open] .ac-icons__chev {
  transform: rotate(180deg);
}
.ac-icons__arrow {
  display: grid;
  flex: none;
  place-items: center;
  width: 30px;
  height: 30px;
  border: 1.5px solid #1c2130;
  border-radius: 50%;
  color: #1c2130;
}
.ac-icons__arrow svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.ac-icons__card[open] .ac-icons__arrow {
  background: #1c2130;
  color: #fff;
  transform: rotate(90deg);
}
@media (prefers-reduced-motion: no-preference) {
  .ac-icons__plus::after,
  .ac-icons__chev,
  .ac-icons__arrow {
    transition: transform .3s ease, background-color .3s ease, color .3s ease;
  }
}

Button-driven accordions with ARIA

When you need behavior that details cannot give you, such as an expand all control, a live summary line or panels that find-in-page can open, a button inside a heading with aria-expanded is the pattern the W3C APG describes. Each script here is a few lines and only flips state.

APG accordion with a 0fr to 1fr grid animation

  • aria-expanded
  • grid-template-rows
  • role="region"

Merino crew sweater

$128

  • Relaxed fit, true to size
  • Ribbed collar, cuffs and hem
  • Model is 183 cm and wears a medium

100 percent extra fine merino, 17.5 micron. Hand wash cold or use the wool cycle, then dry flat away from direct heat. Full care guide

Free shipping over $100, delivered in 3 to 5 working days. Returns are free for 30 days, and exchanges ship the day we receive the original.

Click a header, or Tab to it and press Enter or Space.

The markup follows the W3C APG accordion pattern: a button inside a heading, aria-expanded on the button, aria-controls pointing at a region labelled by that button. The panel is a one-row grid that transitions from 0fr to 1fr, which animates to the real content height in every current browser. visibility: hidden, switched after the transition, keeps links in closed panels out of the tab order.

View code
.ac-prod {
  box-sizing: border-box;
  width: min(100%, 460px);
  padding: 22px 24px 10px;
  border-radius: 4px;
  background: #fff;
  color: #171717;
  box-shadow: 0 1px 0 #e7e5e0, 0 10px 30px rgba(23, 23, 23, .07);
}
.ac-prod__top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding-bottom: 16px;
  border-bottom: 1px solid #171717;
}
.ac-prod__name,
.ac-prod__price {
  margin: 0;
  font-size: 19px;
  font-weight: 700;
}
.ac-prod__h {
  margin: 0;
  border-bottom: 1px solid #e4e1db;
}
.ac-prod__btn {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  min-height: 52px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .08em;
  text-align: left;
  text-transform: uppercase;
  cursor: pointer;
}
.ac-prod__btn:focus-visible {
  outline: 2px solid #171717;
  outline-offset: 3px;
}
.ac-prod__icon {
  position: relative;
  width: 14px;
  height: 14px;
}
.ac-prod__icon::before,
.ac-prod__icon::after {
  content: "";
  position: absolute;
  top: 6px;
  left: 0;
  width: 14px;
  height: 2px;
  background: currentColor;
}
.ac-prod__icon::after {
  transform: rotate(90deg);
}
.ac-prod__btn[aria-expanded="true"] .ac-prod__icon::after {
  transform: rotate(0deg);
}
.ac-prod__panel {
  display: grid;
  grid-template-rows: 0fr;
}
.ac-prod__h:has(> [aria-expanded="true"]) + .ac-prod__panel {
  grid-template-rows: 1fr;
}
.ac-prod__inner {
  min-height: 0;
  overflow: hidden;
  visibility: hidden;
}
.ac-prod__h:has(> [aria-expanded="true"]) + .ac-prod__panel .ac-prod__inner {
  visibility: visible;
}
.ac-prod__inner ul,
.ac-prod__inner p {
  margin: 14px 0 18px;
  color: #45433f;
  font-size: 15px;
  line-height: 1.6;
}
.ac-prod__inner ul {
  padding-left: 18px;
}
.ac-prod__inner a {
  color: #171717;
  text-underline-offset: 3px;
}
.ac-prod__inner a:focus-visible {
  outline: 2px solid #171717;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .ac-prod__panel {
    transition: grid-template-rows .35s ease;
  }
  .ac-prod__inner {
    transition: visibility 0s .35s;
  }
  .ac-prod__h:has(> [aria-expanded="true"]) + .ac-prod__panel .ac-prod__inner {
    transition-delay: 0s;
  }
  .ac-prod__icon::after {
    transition: transform .3s ease;
  }
}

Settings panel with icons and live meta text

  • hidden attribute
  • @starting-style
  • role="switch"

Workspace settings

Click a header, or Tab to it and press Enter or Space.

Each row shows an icon, a label and a summary line that the script rewrites whenever a switch inside changes, so people can scan their settings without opening every row. Closed panels use the hidden attribute, which removes them from the tab order and the accessibility tree, and @starting-style fades a panel in when it appears.

View code
.ac-set {
  box-sizing: border-box;
  width: min(100%, 480px);
  padding: 8px;
  border: 1px solid #e1e4ea;
  border-radius: 18px;
  background: #fff;
  color: #161a23;
}
.ac-set__title {
  margin: 10px 12px 12px;
  font-size: 18px;
  font-weight: 700;
}
.ac-set__row {
  border-radius: 12px;
}
.ac-set__row + .ac-set__row {
  margin-top: 4px;
}
.ac-set__row:has(> .ac-set__h [aria-expanded="true"]) {
  background: #f5f7fa;
}
.ac-set__h {
  margin: 0;
  font-size: inherit;
}
.ac-set__btn {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
  min-height: 64px;
  padding: 10px 12px;
  border: 0;
  border-radius: 12px;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.ac-set__btn:hover {
  background: #f0f2f6;
}
.ac-set__btn:focus-visible {
  outline: 2px solid #1d6b4f;
  outline-offset: 2px;
}
.ac-set__icon {
  display: grid;
  flex: none;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 11px;
  background: #fff1e6;
  color: #b24a0b;
}
.ac-set__icon--privacy {
  background: #ebf1ff;
  color: #2d4fc4;
}
.ac-set__icon--security {
  background: #e7f6ef;
  color: #1d6b4f;
}
.ac-set__icon svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.ac-set__text {
  display: grid;
  flex: 1 1 auto;
  gap: 2px;
  min-width: 0;
}
.ac-set__label {
  font-weight: 600;
}
.ac-set__meta {
  color: #5b6272;
  font-size: 14px;
}
.ac-set__chev {
  flex: none;
  width: 8px;
  height: 8px;
  margin-right: 6px;
  border-right: 2px solid #5b6272;
  border-bottom: 2px solid #5b6272;
  transform: rotate(45deg);
}
.ac-set__btn[aria-expanded="true"] .ac-set__chev {
  transform: rotate(225deg);
}
.ac-set__panel {
  display: grid;
  gap: 2px;
  padding: 0 12px 10px 66px;
}
.ac-set__panel[hidden] {
  display: none;
}
.ac-set__switch {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 44px;
  font-size: 15px;
  cursor: pointer;
}
.ac-set__switch input {
  flex: none;
  position: relative;
  width: 42px;
  height: 24px;
  margin: 0;
  border-radius: 12px;
  background: #b9bfca;
  appearance: none;
  cursor: pointer;
}
.ac-set__switch input::before {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .25);
}
.ac-set__switch input:checked {
  background: #1d6b4f;
}
.ac-set__switch input:checked::before {
  transform: translateX(18px);
}
.ac-set__switch input:focus-visible {
  outline: 2px solid #1d6b4f;
  outline-offset: 2px;
}
@media (max-width: 420px) {
  .ac-set__panel {
    padding-left: 12px;
  }
}
@media (prefers-reduced-motion: no-preference) {
  .ac-set__chev,
  .ac-set__switch input::before {
    transition: transform .2s ease;
  }
  .ac-set__panel {
    transition: opacity .25s ease, translate .25s ease;
  }
  @starting-style {
    .ac-set__panel {
      opacity: 0;
      translate: 0 -6px;
    }
  }
}

Course syllabus with expand all

  • details
  • toggle event
  • expand / collapse all

Course content

4 modules, 15 lessons, 2 h 52 min

Type on screens4 lessons, 41 min
  • Reading distance and size08:12
  • Line length and measure11:40
  • Leading that breathes09:05
  • Exercise: a readable article page12:30
Pairing typefaces3 lessons, 34 min
  • Contrast without chaos10:22
  • Superfamilies09:48
  • Exercise: a landing page duo14:10
Hierarchy and scale4 lessons, 46 min
  • Modular scales12:02
  • Weight before size08:44
  • Fluid type with clamp()13:15
  • Exercise: a dashboard header11:59
Performance and loading4 lessons, 51 min
  • Subsetting and formats13:30
  • font-display choices11:05
  • Fallback metrics12:48
  • Exercise: zero layout shift13:37

Click a header, or Tab to it and press Enter or Space.

The modules stay native details elements, and a short script adds the one thing CSS cannot do: open or close every module at once. The button label is recalculated on each toggle event, so it says Collapse all only when every module is actually open, however they got that way.

View code
.ac-course {
  box-sizing: border-box;
  width: min(100%, 600px);
  border: 1px solid #d6d9e0;
  border-radius: 10px;
  background: #fff;
  color: #1a1d26;
  overflow: hidden;
}
.ac-course__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 18px 20px;
  background: #f6f4ef;
  border-bottom: 1px solid #d6d9e0;
}
.ac-course__title {
  margin: 0;
  font-size: 18px;
  font-weight: 700;
}
.ac-course__sum {
  margin: 2px 0 0;
  color: #575d6c;
  font-size: 14px;
}
.ac-course__all {
  min-height: 40px;
  padding: 0 16px;
  border: 1.5px solid #1a1d26;
  border-radius: 8px;
  background: #fff;
  color: #1a1d26;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.ac-course__all:hover {
  background: #1a1d26;
  color: #fff;
}
.ac-course__all:focus-visible {
  outline: 3px solid #6b3fd4;
  outline-offset: 2px;
}
.ac-course__mod + .ac-course__mod {
  border-top: 1px solid #e3e5ea;
}
.ac-course__mod summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 12px;
  padding: 14px 20px;
  list-style: none;
  cursor: pointer;
}
.ac-course__mod summary::-webkit-details-marker {
  display: none;
}
.ac-course__mod summary::before {
  content: "";
  flex: none;
  width: 7px;
  height: 7px;
  border-right: 2px solid #6b3fd4;
  border-bottom: 2px solid #6b3fd4;
  transform: rotate(-45deg);
}
.ac-course__mod[open] summary::before {
  transform: rotate(45deg);
}
.ac-course__mod summary:hover {
  background: #faf9f6;
}
.ac-course__mod summary:focus-visible {
  outline: 3px solid #6b3fd4;
  outline-offset: -3px;
}
.ac-course__name {
  flex: 1 1 auto;
  font-weight: 700;
}
.ac-course__info {
  color: #575d6c;
  font-size: 14px;
}
.ac-course__lessons {
  margin: 0;
  padding: 0 20px 12px 39px;
  list-style: none;
}
.ac-course__lessons li {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 0;
  font-size: 15px;
}
.ac-course__lessons li + li {
  border-top: 1px dashed #e3e5ea;
}
.ac-course__lessons li span:last-child {
  flex: none;
  color: #575d6c;
  font-family: ui-monospace, Consolas, monospace;
  font-size: 13px;
}
@media (prefers-reduced-motion: no-preference) {
  .ac-course__mod summary::before {
    transition: transform .2s ease;
  }
}

Searchable panels with hidden="until-found"

  • hidden="until-found"
  • beforematch
  • feature detection

Gravel bike service manual

Closed sections open with the headers below.

Click a header, or Tab to it and press Enter or Space.

Closed panels carry hidden="until-found", so find-in-page can match text inside them and the browser opens the panel itself, firing beforematch first so the script can set aria-expanded. Browsers without the attribute value treat it as plain hidden, and the buttons still work. The hint line is chosen by checking for onbeforematch, and the padding sits on an inner element because the hidden panel keeps its box.

View code
.ac-man {
  box-sizing: border-box;
  width: min(100%, 520px);
  padding: 22px 22px 12px;
  border-radius: 6px;
  background: #fdfdfb;
  color: #1e2320;
  box-shadow: inset 0 0 0 1px #d4d8cf;
}
.ac-man__title {
  margin: 0;
  font-size: 20px;
  font-weight: 800;
}
.ac-man__hint {
  margin: 6px 0 14px;
  padding: 8px 12px;
  border-left: 3px solid #d0461e;
  background: #f6f2ea;
  color: #4a4f46;
  font-size: 14px;
}
.ac-man__h {
  margin: 0;
  font-size: inherit;
  border-top: 1px solid #e1e4dc;
}
.ac-man__btn {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 4px 12px;
  width: 100%;
  min-height: 50px;
  padding: 10px 4px 10px 26px;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-weight: 700;
  text-align: left;
  cursor: pointer;
  position: relative;
}
.ac-man__btn::before {
  content: "";
  position: absolute;
  left: 4px;
  top: 50%;
  width: 0;
  height: 0;
  margin-top: -5px;
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  border-left: 7px solid #d0461e;
}
.ac-man__btn[aria-expanded="true"]::before {
  transform: rotate(90deg);
}
.ac-man__btn:hover {
  color: #a2340f;
}
.ac-man__btn:focus-visible {
  outline: 2px solid #d0461e;
  outline-offset: 1px;
}
.ac-man__km {
  color: #5c6157;
  font-family: ui-monospace, Consolas, monospace;
  font-size: 12px;
  font-weight: 500;
}
.ac-man__inner {
  padding: 0 4px 14px 26px;
}
.ac-man__inner p {
  margin: 0;
  color: #3d433b;
  font-size: 15px;
  line-height: 1.6;
}
@media (prefers-reduced-motion: no-preference) {
  .ac-man__btn::before {
    transition: transform .2s ease;
  }
}

Photo, gallery and timeline accordions

Three layouts that go past a plain stack of questions: an itinerary with photo thumbnails, a horizontal gallery whose panels widen on hover or focus, and release notes on a timeline.

Itinerary cards with photos

  • details name
  • object-fit
  • :has() / [open]
The cliff above the fjordDay 1, 11 km, 4 h
A deep blue fjord between grey granite cliffs, seen from a rocky ledge

A steady climb through pine and bare granite ends on a flat ledge 600 m above the water. Lunch is at the ledge, and the ferry back leaves at 5 pm.

Over the pass roadDay 2, 64 km by van
A single-lane road winding through a green valley below steep grassy cliffs under low cloud

A slow drive over the single-track pass, with stops for short walks to the ridge. Expect wind and fast-moving cloud, even in summer.

Waterfall gorge loopDay 3, 8 km, 3 h
A waterfall dropping into a mossy green gorge surrounded by pine forest

A forest loop down to the base of the falls and back along the rim. The path is wet and steep in places, so bring shoes with grip.

A tour itinerary where each day is a card. Closed cards show a square thumbnail, and the open card swaps it for a wide photo with the day details. The thumbnail has empty alt text because the summary already names the day, while the large photo inside the panel gets a real description.

View code
.ac-trip {
  display: grid;
  gap: 10px;
  width: min(100%, 520px);
}
.ac-trip__day {
  border-radius: 16px;
  background: #fff;
  color: #18211f;
  box-shadow: 0 1px 2px rgba(16, 30, 28, .08), 0 6px 18px rgba(16, 30, 28, .06);
  overflow: hidden;
}
.ac-trip__day summary {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 16px 10px 10px;
  border-radius: 16px;
  list-style: none;
  cursor: pointer;
}
.ac-trip__day summary::-webkit-details-marker {
  display: none;
}
.ac-trip__day summary::after {
  content: "";
  flex: none;
  width: 8px;
  height: 8px;
  margin-left: auto;
  border-right: 2px solid #2f6f62;
  border-bottom: 2px solid #2f6f62;
  transform: rotate(45deg);
}
.ac-trip__day[open] summary::after {
  transform: rotate(225deg);
}
.ac-trip__day summary:focus-visible {
  outline: 3px solid #2f6f62;
  outline-offset: -3px;
}
.ac-trip__thumb {
  flex: none;
  width: 56px;
  height: 56px;
  border-radius: 10px;
  object-fit: cover;
}
.ac-trip__day[open] .ac-trip__thumb {
  display: none;
}
.ac-trip__day[open] summary {
  padding: 16px 18px 12px;
}
.ac-trip__text {
  display: grid;
  gap: 2px;
  min-width: 0;
}
.ac-trip__name {
  font-size: 17px;
  font-weight: 700;
}
.ac-trip__meta {
  color: #4f5d59;
  font-size: 14px;
}
.ac-trip__body {
  padding: 0 18px 18px;
}
.ac-trip__photo {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  border-radius: 10px;
  object-fit: cover;
}
.ac-trip__body p {
  margin: 12px 0 0;
  color: #3b4744;
  font-size: 15px;
  line-height: 1.6;
}
@media (prefers-reduced-motion: no-preference) {
  .ac-trip__day summary::after {
    transition: transform .25s ease;
  }
}

Horizontal image accordion

  • flex-grow transition
  • :has()
  • container queries

Hover a panel, or Tab through the links.

A gallery of trip links where the hovered or focused panel grows and the rest shrink to slim strips with a vertical label. It is a list of links, not a disclosure widget, so every title stays in the accessibility tree and no ARIA is needed. :has() keeps the first panel open while nothing is hovered, and a container query turns the row into a column when the component is narrow.

View code
.ac-hz {
  width: min(100%, 860px);
  container-type: inline-size;
}
.ac-hz__list {
  display: flex;
  gap: 8px;
  height: 380px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.ac-hz__item {
  flex: 1 1 0;
  min-width: 0;
  min-height: 0;
}
.ac-hz__list:not(:has(.ac-hz__item:hover, .ac-hz__link:focus-visible)) .ac-hz__item:first-child,
.ac-hz__item:hover,
.ac-hz__item:has(.ac-hz__link:focus-visible) {
  flex-grow: 5;
}
.ac-hz__link {
  position: relative;
  display: block;
  height: 100%;
  border-radius: 16px;
  color: #fff;
  text-decoration: none;
  overflow: hidden;
}
.ac-hz__link img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.ac-hz__link:focus-visible {
  outline: 3px solid #ffcf4a;
  outline-offset: 3px;
}
.ac-hz__label {
  position: absolute;
  bottom: 12px;
  left: 50%;
  padding: 10px 5px;
  border-radius: 99px;
  background: rgba(10, 12, 16, .78);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .04em;
  white-space: nowrap;
  writing-mode: vertical-rl;
  transform: translateX(-50%) rotate(180deg);
}
.ac-hz__info {
  position: absolute;
  right: 12px;
  bottom: 12px;
  left: 12px;
  display: grid;
  gap: 2px;
  padding: 12px 14px;
  border-radius: 12px;
  background: rgba(10, 12, 16, .8);
  opacity: 0;
}
.ac-hz__title {
  font-size: 17px;
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ac-hz__meta {
  color: #e2e5ea;
  font-size: 14px;
  white-space: nowrap;
}
.ac-hz__list:not(:has(.ac-hz__item:hover, .ac-hz__link:focus-visible)) .ac-hz__item:first-child .ac-hz__info,
.ac-hz__item:hover .ac-hz__info,
.ac-hz__link:focus-visible .ac-hz__info {
  opacity: 1;
}
.ac-hz__list:not(:has(.ac-hz__item:hover, .ac-hz__link:focus-visible)) .ac-hz__item:first-child .ac-hz__label,
.ac-hz__item:hover .ac-hz__label,
.ac-hz__link:focus-visible .ac-hz__label {
  opacity: 0;
}
.ac-hz__info,
.ac-hz__label {
  transition: opacity .2s ease;
}
@container (max-width: 520px) {
  .ac-hz__list {
    flex-direction: column;
    height: 460px;
  }
  .ac-hz__label {
    top: 50%;
    bottom: auto;
    left: 12px;
    padding: 4px 10px;
    writing-mode: horizontal-tb;
    transform: translateY(-50%);
  }
}
@media (prefers-reduced-motion: no-preference) {
  .ac-hz__item {
    transition: flex-grow .5s cubic-bezier(.3, .7, .2, 1);
  }
}

Release notes on a timeline

  • details in a list
  • timeline dots
  • time element

Release notes

  1. 4.2.0Shared budgets
    • NewInvite a partner to a budget and edit it together in real time.
    • FixedRecurring transfers no longer duplicate on the first of the month.
  2. 4.1.3Import fixes
    • FixedCSV imports keep accents in payee names.
    • ChangedImported rows are now grouped by statement date.
  3. 4.1.0Goals view
    • NewSet savings goals with a target date and see the monthly amount needed.

Each release is a details element inside an ordered list, drawn on a vertical line with a dot that fills when the release is open. The newest release starts open with the open attribute in the HTML, so nothing shifts after load. Change types use text badges with their own colors, never color alone.

View code
.ac-log {
  box-sizing: border-box;
  width: min(100%, 560px);
  padding: 24px clamp(16px, 4vw, 28px) 10px;
  border-radius: 14px;
  background: #fffdf7;
  color: #22201b;
  box-shadow: inset 0 0 0 1px #e8e1cf;
}
.ac-log__title {
  margin: 0 0 16px;
  font-family: "Fraunces", Georgia, serif;
  font-size: 24px;
  font-weight: 600;
}
.ac-log__list {
  margin: 0;
  padding: 0;
  list-style: none;
}
.ac-log__entry {
  position: relative;
  padding: 0 0 6px 26px;
}
.ac-log__entry::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 6px;
  width: 2px;
  background: #e2d9c3;
}
.ac-log__entry:last-child::before {
  bottom: auto;
  height: 24px;
}
.ac-log__rel summary {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  padding: 12px 8px;
  border-radius: 8px;
  list-style: none;
  cursor: pointer;
}
.ac-log__rel summary::-webkit-details-marker {
  display: none;
}
.ac-log__rel summary::before {
  content: "";
  position: absolute;
  top: 16px;
  left: -26px;
  width: 10px;
  height: 10px;
  border: 2px solid #8a6a1f;
  border-radius: 50%;
  background: #fffdf7;
}
.ac-log__rel[open] summary::before {
  background: #8a6a1f;
}
.ac-log__rel summary:hover {
  background: #f6f0e1;
}
.ac-log__rel summary:focus-visible {
  outline: 2px solid #8a6a1f;
  outline-offset: 0;
}
.ac-log__ver {
  padding: 2px 7px;
  border-radius: 6px;
  background: #22201b;
  color: #fffdf7;
  font-family: ui-monospace, Consolas, monospace;
  font-size: 13px;
}
.ac-log__name {
  flex: 1 1 auto;
  font-weight: 700;
}
.ac-log__date {
  color: #67604f;
  font-size: 13px;
}
.ac-log__changes {
  margin: 0;
  padding: 0 8px 14px;
  list-style: none;
}
.ac-log__changes li {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 5px 0;
  color: #3f3b31;
  font-size: 15px;
  line-height: 1.5;
}
.ac-log__tag {
  flex: none;
  min-width: 58px;
  padding: 1px 0;
  border-radius: 5px;
  font-size: 12px;
  font-weight: 700;
  text-align: center;
}
.ac-log__tag--new {
  background: #dff3e6;
  color: #17603a;
}
.ac-log__tag--fix {
  background: #fde7e1;
  color: #9a3112;
}
.ac-log__tag--chg {
  background: #e4ecfb;
  color: #2a4d96;
}

Accordions inside a phone frame

Accordions do most of their work on small screens. These three show a filter sheet, a collapsed checkout summary and long sections with sticky headers, each at a real 390px width.

Mobile filter sheet

  • details
  • :has(:checked)
  • type="reset"

Filters

Category
Size (EU)
Price
Features

Tap a section header.

A shop filter sheet where each facet is a details element. A dot appears next to any facet that has a selection, using :has() with :checked, so the state is visible even when the section is closed. Clear all is a plain reset button, and the chips are real checkboxes with labels that show a focus ring through :has(:focus-visible).

View code
.ac-flt {
  display: flex;
  flex-direction: column;
  height: 100%;
  margin: 0;
  background: #fff;
  color: #15171c;
}
.ac-flt__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 18px;
  border-bottom: 1px solid #e6e7eb;
}
.ac-flt__title {
  margin: 0;
  font-size: 18px;
  font-weight: 700;
}
.ac-flt__clear {
  min-height: 36px;
  padding: 0 4px;
  border: 0;
  background: none;
  color: #1f5fd1;
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}
.ac-flt__clear:focus-visible,
.ac-flt__apply:focus-visible {
  outline: 3px solid #1f5fd1;
  outline-offset: 2px;
}
.ac-flt__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 0 18px;
}
.ac-flt__sec {
  border-bottom: 1px solid #e6e7eb;
}
.ac-flt__sec summary {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 54px;
  list-style: none;
  font-weight: 700;
  cursor: pointer;
}
.ac-flt__sec summary::-webkit-details-marker {
  display: none;
}
.ac-flt__sec summary::after {
  content: "";
  width: 8px;
  height: 8px;
  margin: -4px 4px 0 auto;
  border-right: 2px solid #15171c;
  border-bottom: 2px solid #15171c;
  transform: rotate(45deg);
}
.ac-flt__sec[open] summary::after {
  margin-top: 4px;
  transform: rotate(225deg);
}
.ac-flt__sec summary:focus-visible {
  outline: 3px solid #1f5fd1;
  outline-offset: -3px;
  border-radius: 6px;
}
.ac-flt__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #1f5fd1;
  visibility: hidden;
}
.ac-flt__sec:has(input:checked:not([value="any"])) .ac-flt__dot {
  visibility: visible;
}
.ac-flt__opts {
  display: grid;
  padding-bottom: 10px;
}
.ac-flt__check {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 44px;
  font-size: 15px;
  cursor: pointer;
}
.ac-flt__check input {
  width: 20px;
  height: 20px;
  margin: 0;
  accent-color: #1f5fd1;
}
.ac-flt__n {
  margin-left: auto;
  color: #5d6270;
  font-size: 14px;
}
.ac-flt__chips {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
  gap: 8px;
  padding-bottom: 16px;
}
.ac-flt__chip {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 44px;
  border: 1.5px solid #cfd2da;
  border-radius: 10px;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}
.ac-flt__chip input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}
.ac-flt__chip:has(input:checked) {
  border-color: #15171c;
  background: #15171c;
  color: #fff;
}
.ac-flt__chip:has(input:focus-visible) {
  outline: 3px solid #1f5fd1;
  outline-offset: 2px;
}
.ac-flt__foot {
  padding: 12px 18px 18px;
  border-top: 1px solid #e6e7eb;
}
.ac-flt__apply {
  width: 100%;
  min-height: 50px;
  border: 0;
  border-radius: 12px;
  background: #15171c;
  color: #fff;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
@media (prefers-reduced-motion: no-preference) {
  .ac-flt__sec summary::after {
    transition: transform .2s ease, margin-top .2s ease;
  }
}

Collapsed order summary at checkout

  • details
  • label swap on [open]
  • tabular-nums

Kettle & Fern

Show order summaryHide order summary $84.00
  • Pour-over kettleMatte black$56.00
  • Ceramic dripperOat$22.00
  • Paper filters, 100 packQuantity 2$6.00
Subtotal
$84.00
Shipping
Calculated next
Total
USD $84.00

Tap a section header.

Mobile checkouts often fold the cart into one bar above the form so the shipping fields stay near the top. The summary text switches between Show and Hide with two spans and display: none, which also keeps the hidden wording away from screen readers. The total stays visible in the bar in both states.

View code
.ac-co {
  background: #fff;
  color: #1c1b19;
}
.ac-co__store {
  margin: 0;
  padding: 16px 18px;
  font-family: "Fraunces", Georgia, serif;
  font-size: 22px;
  font-weight: 600;
  text-align: center;
  border-bottom: 1px solid #e6e2da;
}
.ac-co__sum {
  border-bottom: 1px solid #e6e2da;
  background: #f7f5f0;
}
.ac-co__sum summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 56px;
  padding: 0 18px;
  list-style: none;
  cursor: pointer;
}
.ac-co__sum summary::-webkit-details-marker {
  display: none;
}
.ac-co__sum summary:focus-visible {
  outline: 3px solid #1d5e8c;
  outline-offset: -3px;
}
.ac-co__toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  color: #1d5e8c;
  font-size: 15px;
  font-weight: 600;
}
.ac-co__toggle::after {
  content: "";
  width: 7px;
  height: 7px;
  margin-top: -4px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
}
.ac-co__sum[open] .ac-co__toggle::after {
  margin-top: 4px;
  transform: rotate(225deg);
}
.ac-co__hide,
.ac-co__sum[open] .ac-co__show {
  display: none;
}
.ac-co__sum[open] .ac-co__hide {
  display: inline;
}
.ac-co__total {
  font-size: 18px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.ac-co__panel {
  padding: 4px 18px 18px;
}
.ac-co__items {
  margin: 0;
  padding: 0;
  list-style: none;
}
.ac-co__items li {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 0;
}
.ac-co__thumb {
  position: relative;
  flex: none;
  width: 52px;
  height: 52px;
  border: 1px solid #ddd8ce;
  border-radius: 10px;
  background: #2a2a2a;
}
.ac-co__thumb--dripper {
  background: #e9dcc4;
}
.ac-co__thumb--filter {
  background: #fbfaf6;
}
.ac-co__thumb span {
  position: absolute;
  top: -7px;
  right: -7px;
  display: grid;
  place-items: center;
  min-width: 20px;
  height: 20px;
  border-radius: 10px;
  background: #5f5a51;
  color: #fff;
  font-size: 12px;
  font-weight: 700;
}
.ac-co__item {
  display: grid;
  flex: 1 1 auto;
  min-width: 0;
  font-size: 15px;
  font-weight: 600;
}
.ac-co__item span {
  color: #625d54;
  font-size: 13px;
  font-weight: 400;
}
.ac-co__price {
  font-size: 15px;
  font-variant-numeric: tabular-nums;
}
.ac-co__code {
  display: grid;
  gap: 6px;
  margin: 12px 0 14px;
  font-size: 14px;
  font-weight: 600;
}
.ac-co__code-row {
  display: flex;
  gap: 8px;
}
.ac-co__code input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 44px;
  padding: 0 12px;
  border: 1px solid #c9c3b7;
  border-radius: 8px;
  background: #fff;
  font: inherit;
}
.ac-co__code button {
  min-height: 44px;
  padding: 0 16px;
  border: 0;
  border-radius: 8px;
  background: #1c1b19;
  color: #fff;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
.ac-co__code input:focus-visible,
.ac-co__code button:focus-visible {
  outline: 3px solid #1d5e8c;
  outline-offset: 1px;
}
.ac-co__rows {
  margin: 0;
}
.ac-co__rows div {
  display: flex;
  justify-content: space-between;
  padding: 4px 0;
  font-size: 15px;
}
.ac-co__rows dt {
  color: #514c44;
}
.ac-co__rows dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
}
.ac-co__rows .ac-co__grand {
  margin-top: 6px;
  padding-top: 10px;
  border-top: 1px solid #e6e2da;
  font-size: 17px;
  font-weight: 700;
}
.ac-co__grand dt {
  color: #1c1b19;
}
@media (prefers-reduced-motion: no-preference) {
  .ac-co__toggle::after {
    transition: transform .2s ease, margin-top .2s ease;
  }
}

Long sections with sticky headers

  • position: sticky
  • details
  • scroll container

Larkspur Apartments

Resident handbook, updated September 2026

Move-in and keys

Collect your keys and fob from the leasing office on move-in day between 9 am and 5 pm. Bring photo ID and the confirmation email.

Each unit gets two door keys, one mailbox key and two fobs. Extra fobs cost $25 and can be ordered from the resident portal.

Book the service elevator at least 48 hours ahead. Moves run Monday to Saturday, 8 am to 6 pm, and the padded walls go up on request.

Quiet hours

Quiet hours run from 10 pm to 7 am every day. Keep music, tools and furniture moves to daytime hours.

If a neighbor is too loud, message the night concierge through the portal. Repeated complaints go to the property manager.

Trash and recycling

Chutes on every floor take bagged household trash. Recycling goes to the blue bins in the garage, flattened and dry.

Bulky items need a pickup slot, booked through the portal for Tuesday mornings.

Parking and bikes

Garage spaces are assigned. The bike room is on level P1 and opens with your fob.

Repairs

Report repairs in the portal. Urgent leaks and lockouts go to the 24 hour line printed inside your front door.

Scroll inside the handbook, then tap a header to close its section.

When panels are long, the header that would close them scrolls away. Making each summary position: sticky inside the scroll container keeps the open section title pinned at the top, so closing it is always one tap away. The scroll area is a labelled, focusable region so keyboard users can scroll it too.

View code
.ac-hb {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: #f3f1ec;
  color: #1f2421;
}
.ac-hb__bar {
  padding: 16px 18px 14px;
  background: #24403a;
  color: #f3f1ec;
}
.ac-hb__name {
  margin: 0;
  font-family: "Fraunces", Georgia, serif;
  font-size: 21px;
  font-weight: 600;
}
.ac-hb__sub {
  margin: 2px 0 0;
  color: #cddbd6;
  font-size: 13px;
}
.ac-hb__scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.ac-hb__scroll:focus-visible {
  outline: 3px solid #b8572a;
  outline-offset: -3px;
}
.ac-hb__sec summary {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 52px;
  padding: 0 18px;
  border-bottom: 1px solid #ddd8cc;
  background: #fff;
  list-style: none;
  font-weight: 700;
  cursor: pointer;
}
.ac-hb__sec summary::-webkit-details-marker {
  display: none;
}
.ac-hb__sec summary::after {
  content: "+";
  content: "+" / "";
  color: #b8572a;
  font-size: 22px;
  font-weight: 500;
  line-height: 1;
}
.ac-hb__sec[open] summary::after {
  content: "\2212";
  content: "\2212" / "";
}
.ac-hb__sec summary:focus-visible {
  outline: 3px solid #b8572a;
  outline-offset: -3px;
}
.ac-hb__body {
  padding: 14px 18px 6px;
}
.ac-hb__body p {
  margin: 0 0 12px;
  color: #3a403c;
  font-size: 15px;
  line-height: 1.6;
}

Which CSS accordion method should you use?

Native details and summary, with a shared name attribute when only one panel should stay open at a time.

Radio inputs, checkboxes and :target links exist because details used to be unreliable. These days they cost you semantics and give very little back.

In practice, a CSS accordion here means any stack of expandable panels that toggles without script handlers. A button wired with aria-expanded needs JavaScript to flip its state, so it falls outside this comparison.

The details interface itself is old. MDN’s browser compatibility data dates it to Chrome 12, Firefox 49 and Safari 6.

MethodSemanticsPanels openBrowser floor
details and summaryNative disclosure widgetSeveral at onceEvery current browser
details with nameNative disclosure widgetOne at a timeChrome 120, Safari 17.2, Firefox 130
Radio inputsRadio button, no expanded stateOne at a timeSelector support only
CheckboxCheckbox, no expanded stateSeveral at onceSelector support only
:target linksPlain links and URL fragmentsOne at a timeSelector support only

If several panels can stay open, plain details is enough. For one panel at a time in current browsers, add name. And if you need one at a time with no script on browsers older than the versions in the table, radio inputs will do it, though you accept the accessibility cost.

Checkbox and :target accordions never beat details. Skip them in new builds.

How to build a CSS accordion with details and summary

Wrap each item in a details element and put the visible label in a summary. The browser takes care of the toggle and the open state, and keyboard operation comes along with it.

Both elements are standard HTML, so the accordion still works with the stylesheet switched off.

  1. Write the markup, one details per item, with the summary first and the content after it. MDN’s reference says summary must be the first child, otherwise the browser inserts a default label (typically “Details”). Add the open attribute to any item that should start expanded.
  2. Style the summary. Set cursor, padding, a border between items, and visible hover and focus states.
  3. Remove the default triangle by setting list-style to none, and add summary::-webkit-details-marker with display none for Safari, as MDN documents.
  4. Add a custom icon. Draw it with summary::after and rotate it in a rule scoped to details[open].
  5. Style the panel itself. Target details::details-content for padding and background, wrapped in an @supports selector(::details-content) block.

The @supports wrapper matters because ::details-content shipped late. Per the Web Platform DX web-features dataset, Chrome added it in 131 (2024-11-12), Safari in 18.4 (2025-03-31) and Firefox in 143 (2025-09-16), which made it Baseline Newly Available on 2025-09-16.

In browsers without it, the accordion still opens and closes. It just keeps the default panel styling.

One catch from MDN is that the role assigned to summary varies across browsers. Test the widget on more than one platform before calling it accessible.

Exclusive accordions with the details name attribute

Give several details elements the same name value and the browser closes the open one whenever another opens.

Chrome for Developers and the Web Platform DX web-features dataset (checked October 2026) list these support dates for the name attribute:

  • Chrome and Chrome Android 120, released 2023-12-05
  • Safari and Safari on iOS 17.2, released 2023-12-11
  • Firefox 130, released 2024-09-03
  • Baseline status: Newly Available since 2024-09-03, expected to reach Widely Available on 2027-03-03

Firefox was the last engine to ship it, so the practical floor is September 2024. Check the Can I use table before dropping a fallback for older devices.

The name groups elements, not their DOM order, so grouped panels can sit in different containers. A new name value starts a new group, which lets one page hold several exclusive accordions.

For older browsers, Chrome for Developers publishes a polyfill built on the toggle event. When a named details opens, the script removes the open attribute from the others in the group.

Browsers that never fire toggle skip the script entirely. The same page calls that acceptable progressive enhancement.

The documentation also adds a caution. Exclusive accordions save space, but people may have to open many items to find an answer, and anyone comparing two items at once gets frustrated.

Default to several panels open. Use name when each answer is long and independent, like a support FAQ where readers want one answer and nothing else.

How do checkbox, radio and :target accordions work?

They keep the open state outside a details element, either in a checked input or in the URL fragment, and a selector reveals the matching panel.

Checkbox accordions

A hidden checkbox sits before a label and a content block. Clicking the label toggles the checkbox, and the :checked pseudo-class plus a sibling combinator shows the panel.

Codrops published this pattern in 2012 and it spread because it needs no script. Each checkbox is independent, so several panels stay open together.

Radio accordions

Swap the checkbox for a radio input, give every item the same name, and the browser allows one open panel. That is the same rule the details name attribute now provides natively.

The upside is single-open behavior in browsers that predate the name attribute. The downside is that a radio can’t be unchecked by clicking it again, so the open panel has no built-in way to close. It also announces itself as a radio button, not as an expandable section.

:target accordions

Each header links to a section ID, and the panel matching :target opens. GrahamTheDev’s DEV Community walkthrough pairs this with a grid-rows trick for animated height.

  • Every click changes the URL fragment, so the Back button steps through panel states.
  • Only one panel can open, because a page has a single :target.
  • Collapsing needs a second link pointing at a different fragment.
  • The page also scrolls toward the target on each click.

Where these methods break

Wikimedia’s Phabricator task T243126 records the trade-off. The checkbox hack spread across skins and Vector menus to cut JavaScript, and the proposed accessibility fix (updating aria-expanded by script) brings JavaScript back.

The same discussion notes that the checkbox keeps its inherent role, because ARIA should not override it with role=”button”.

My take is to skip all of them wherever the name attribute works. Radio inputs only make sense on script-free pages that must support browsers older than Chrome 120, Safari 17.2 or Firefox 130.

Animating CSS accordion height without JavaScript

Animating panel height in plain CSS comes down to interpolate-size with ::details-content, a grid-template-rows switch from 0fr to 1fr, or a max-height transition. On a native details element, only the first animates both opening and closing.

The shared problem is that height: auto is not a value the browser can interpolate by default. Each technique is a different workaround for it.

Support splits along engine lines. The name attribute and ::details-content ship in Chrome, Safari and Firefox, but interpolate-size does not, so animated auto height is a Chromium feature for now.

::details-content with interpolate-size

Opt in with interpolate-size: allow-keywords on :root. Then start details::details-content at height 0 with overflow clip, transition height and content-visibility (with the allow-discrete keyword), and set height auto under details[open].

It animates opening and closing on plain details markup, with no wrapper div and no guessed heights. The catch is that auto-height animation only runs where interpolate-size exists. The pseudo-element also defaults to display block, where it used to behave like display contents, so children that rely on height 100% can break.

Chrome for Developers documents a workaround for that last problem: display contents on the open pseudo-element. It can’t be combined with a height animation, because a display contents box has no height to animate.

Per the Web Platform DX web-features dataset, interpolate-size (specified in CSS Values and Units Level 5 by the CSS Working Group) shipped in Chrome 129 on 2024-09-17 and Edge 129 on 2024-09-19. Firefox (bug 1945962, vendor position positive) and Safari (WebKit bug 295132, vendor position support) lack it as of October 2026.

Gate the animation with @supports (interpolate-size: allow-keywords). Browsers outside the gate keep a working accordion without motion, and for most content that beats a fixed-height box with a scrollbar.

grid-template-rows from 0fr to 1fr

DockYard’s October 2024 write-up calls CSS Grid the cleanest browser-safe route to intrinsic-size animation.

The wrapper uses display grid with grid-template-rows 0fr and a transition on that property. The open state switches it to 1fr, and the inner element needs overflow hidden so the collapsed row clips its content.

One quirk. Between 0 and 1fr, Grid can briefly create a second row and the animation stutters. DockYard fixes it with grid-row: 1 / span 2 on the container.

It works in every major browser, takes content of any height and needs no script. But on a native details element the wrapper animates only on opening. Chrome for Developers tested a wrapper div and saw no closing animation.

The pattern stays the standard choice for button-driven accordions, where an attribute such as data-open flips the state, as in DockYard’s example.

max-height transitions

Skip it in new builds.

A max-height transition runs between 0 and the cap you declare, not the real content height. Put a 1000px cap on a 200px panel and most of the duration passes with nothing visibly moving, and the close lags.

ProsCons
Works in every browserNeeds a guessed upper bound
One property to learnTiming drifts from the real height
Fine for fixed-height panelsTaller content gets clipped

It still suits panels with a known height. Variable copy needs one of the two techniques above.

Respecting reduced motion

Wrap every transition in a prefers-reduced-motion check. WCAG 2.3.3 Animation from Interactions (Level AAA) asks that motion triggered by interaction can be turned off unless it carries information, and W3C’s Technique C39 shows the media query route.

  • Set the duration through a CSS variable and zero it under prefers-reduced-motion: reduce, as the DEV Community accordion walkthrough does.
  • Or write static styles first and add motion only under prefers-reduced-motion: no-preference.

Chrome for Developers leaves the check out of its ::details-content demos for brevity and says production code should include it.

What makes a CSS accordion accessible?

Each header needs to be a real control with an announced expanded or collapsed state, and every action has to work from the keyboard alone.

Native details and summary supply that behavior by default. W3C’s WCAG 2.2 text notes that standard HTML controls already meet Success Criterion 4.1.2 (Name, Role, Value, Level A) when used according to specification, so the burden falls on components you script or fake.

Semantics a custom accordion must add

The W3C ARIA Authoring Practices Guide sets the baseline for anything built from buttons and panels, using ARIA states and properties. The title goes inside an element with role button, aria-expanded is true while the panel is visible, and aria-controls holds the ID of the panel content. If the accordion doesn’t allow collapsing, an open header also gets aria-disabled set to true.

Keyboard operation

KeyActionStatus
Enter or Space on a headerExpands a collapsed panel, collapses an open one if allowedRequired
Tab and Shift+TabMove through all focusable elements, panel contents includedRequired
Down and Up arrowsMove to the next or previous headerOptional
Home and EndMove to the first or last headerOptional

Native summary covers the two required rows. The optional arrow-key movement between headers is absent from details, and adding it takes script.

A checkbox or radio accordion whose input uses display none never receives focus, so keyboard users can’t reach the header at all. Hide the input with a visually-hidden pattern instead, and tie the label to it with the for attribute.

Focus visibility

WCAG 2.4.7 Focus Visible (Level AA) means summary and label need a visible focus indicator. Removing the default outline without a replacement fails it.

WCAG 2.2 added 2.4.11 Focus Not Obscured (Minimum) (Level AA). A sticky navigation bar that covers a focused summary after the page scrolls runs into it. A scroll-padding-top on the root keeps focused headers clear of the bar.

Testing the result

Deque’s March 2021 study of its axe-powered automated testing found it covered 57 percent of accessibility issues by volume, above the 20 to 30 percent commonly assumed. It is the vendor’s own tooling and data set, so treat the figure as a best case for Lighthouse or axe DevTools.

Run both for the cheap catches, then test by hand. Tab to each header and toggle it with Enter and Space, checking that the focus ring stays visible and unobscured once a panel opens. Then listen with NVDA, JAWS or VoiceOver for the announced state.

Searchability and layout shift in collapsed panels

A collapsed panel built carelessly costs two things: text that find-in-page can’t reach, and height changes that move the page.

Find-in-page and fragment links

Native details elements are searchable in Chromium browsers and in Firefox 148 or later, and partly in Safari 26.2. Per the HTML Standard as summarized by Chrome for Developers, a closed details expands automatically when the browser scrolls to hidden content after find-in-page, a text fragment link or an element fragment link.

Custom panels need hidden=until-found. Chrome for Developers documents it as available from Chrome 102 (May 2022), with a beforematch event that fires just before the browser reveals the section.

The documentation attaches a few caveats. The attribute applies content-visibility hidden, not display none, so the element keeps its box and borders still render. Put borders on a nested element. Panels also have to open without find-in-page, since not every visitor searches.

Browsers without support fall back to plain hidden content. Chrome for Developers suggests detecting them by checking for onbeforematch on HTMLElement.prototype, but Safari 26.2 added the beforematch event with only partial support for the attribute value (MDN’s compatibility data notes it does not scroll correctly to the match), so the check passes in Safari. Test in Safari instead of relying on it alone.

GOV.UK Frontend’s accordion adopted the attribute in v4.5.0, so the browser’s own search can open its sections.

Support has moved since then. The v4.5.0 release notes (January 2023) limited it to recent Chrome, Edge and Samsung Internet. Firefox has now shipped hidden=until-found (Firefox 148, released 2026-02-24), while Safari 26.2 supports it only partially, so the Web Platform DX dataset still lists Safari as missing it. Auto-expanding closed details elements on find-in-page and hidden=until-found were both part of the details element focus area in Interop 2025.

Check the browsers you target before relying on it.

Layout shift

web.dev’s CLS guide sets a good Cumulative Layout Shift score at 0.1 or less, measured at the 75th percentile of page loads across mobile and desktop. Each shift scores impact fraction times distance fraction, and CLS is one of the Core Web Vitals.

The same guide says a shift only hurts when the user does not expect it. A tap on a summary is an expected cause, so the risk sits elsewhere. A panel opened by script after first paint, from a URL hash for example, pushes everything below it down, so set the open attribute in the HTML instead. Late-loading content or images without declared dimensions inside an open panel change its height too.

Height animation also reflows the content below on every frame, while opacity and transform do not. Keep durations short on long pages.

When a CSS accordion is the wrong choice

Skip the accordion when the hidden text is what most visitors came to read, or when the page needs controls CSS can’t provide.

It’s also a poor fit when panels hold a single short sentence, since the header then costs more than the content, or when the content must be visible on first load. Need open-all and close-all buttons, analytics on every toggle, or arrow-key movement between headers? Details fires a toggle event, but wiring any of that takes script. And if the sections are peer views of one subject, tabs are the better pattern.

Accordion or tabs?

DimensionAccordionTabs
Keyboard modelTab through headers, arrows optionalArrow keys move within the tab list
Panels visibleOne, several or allOne at a time
FitsLong lists of independent sectionsPeer views of one subject

The W3C APG gives each pattern its own keyboard model. A layout that swaps tabs for an accordion at a breakpoint must implement both correctly, so pick one unless the design demands the switch.

When a framework fits better

The accordion component in Bootstrap runs on the framework’s collapse plugin. Buttons carry data-bs-toggle=”collapse” and data-bs-target with aria-expanded and aria-controls set in the markup, and data-bs-parent groups panels into single-open behavior (Bootstrap 5.3 documentation).

The cost is loading Bootstrap’s JavaScript bundle for a widget that details handles natively.

Tailwind CSS goes the other way. An accordion styled with Tailwind utilities adds classes to native markup and no behavior of its own.

I’d use native details when styling is the only gap, and a framework accordion when its JavaScript already ships with the site. If the content should simply be visible, use neither.

CSS Accordion FAQ

Can you nest one accordion inside another?

Yes. A details element can sit inside another panel, and each keeps its own open state.

Give the inner group a different name value, because a new name value starts a separate exclusive group (Chrome for Developers).

Can search engines read text inside a closed panel?

They can. Closed panel text stays in the page markup, and Chrome for Developers says hidden=until-found makes collapsed content accessible to search engines, with Google Search forming links that scroll to the revealed fragment.

The documentation makes no claim about ranking weight.

How do I link straight to one accordion panel?

Put an ID on an element inside the panel and link to it. In Chromium browsers and Firefox 148 or later (partially in Safari 26.2), a closed details expands when element fragment navigation, find-in-page or a text fragment link scrolls to hidden content, per the HTML Standard as summarized by Chrome for Developers.

Custom panels need hidden=until-found for the same behavior.

What a Production-Ready CSS Accordion Needs

Build on native details and summary, and treat every enhancement as removable. If animation or single-open grouping fails in a browser, the panels still open and close.

Start with plain details, visible focus and the open state set in HTML. Add a shared name value only where one panel at a time helps readers, and put animation behind @supports with a reduced-motion rule. Each layer fails safely, so the one before it keeps working.

The trade-off is that Firefox and Safari visitors get an instant toggle where Chromium visitors get motion.

The fallback layer disappears the day Firefox and Safari both ship interpolate-size, since one animation rule would then serve all three engines. Position verified October 2026.

When sections are peers instead of a stack, tabs built in CSS are the next component to build.