32 CSS Accordions — Vertical & Horizontal29 / 32

Pure CSSMIT licensed

Curtain Slide Horizontal Accordion — clip-path Reveal Panels

Every panel is always full width, stacked in the same grid cell — the active one is revealed by an animated clip-path wipe that sweeps across like a stage curtain, driven by a slim tab rail on the left. Unlike flex-grow accordions, content NEVER reflows: it's uncovered, not resized.

Published Updated

Live Demo
Try it

The code

<section class="acc-19" aria-label="Curtain slide clip-path accordion demo">
  <div class="acc-19__shell" role="group" aria-label="Quarterly report sections">
    <div class="acc-19__rail">
      <input type="radio" name="acc-19" id="acc-19-a" checked><label class="acc-19__tab" for="acc-19-a">Revenue</label>
      <input type="radio" name="acc-19" id="acc-19-b"><label class="acc-19__tab" for="acc-19-b">Customers</label>
      <input type="radio" name="acc-19" id="acc-19-c"><label class="acc-19__tab" for="acc-19-c">Product</label>
      <input type="radio" name="acc-19" id="acc-19-d"><label class="acc-19__tab" for="acc-19-d">Team</label>
    </div>
    <div class="acc-19__stage">
      <div class="acc-19__panel" data-p="a"><b>$4.2M ARR</b><p>Up 38% year over year. Net revenue retention held at 121% while CAC payback improved to 11 months.</p><span class="acc-19__stat"><i>+38%</i>YoY growth</span></div>
      <div class="acc-19__panel" data-p="b"><b>1,940 teams</b><p>Enterprise logos doubled this quarter. The self-serve funnel now converts 4.1% of signups within 14 days.</p><span class="acc-19__stat"><i>2×</i>enterprise logos</span></div>
      <div class="acc-19__panel" data-p="c"><b>31 releases</b><p>Headline: the plugin API shipped to GA. Median time-to-resolution for P1 bugs dropped to 6 hours.</p><span class="acc-19__stat"><i>GA</i>plugin API</span></div>
      <div class="acc-19__panel" data-p="d"><b>58 people</b><p>Nine hires, zero regretted attrition. Engineering is now 40% of the company across four time zones.</p><span class="acc-19__stat"><i>9</i>new hires</span></div>
    </div>
  </div>
</section>
.acc-19,
.acc-19 *,
.acc-19 *::before,
.acc-19 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.acc-19 {
  --accent: oklch(0.6 0.19 20);
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f7f3f2;
  padding: 44px 24px;
}

.acc-19__shell {
  display: flex;
  width: min(760px,100%);
  min-height: 340px;
  background: #fff;
  border-radius: 18px;
  box-shadow: 0 24px 60px -34px rgba(80,30,25,.35);
  overflow: hidden;
}

.acc-19__rail {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 18px 14px;
  border-right: 1.5px solid #f0e6e4;
  background: #fbf8f7;
}

.acc-19__rail input {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.acc-19__tab {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 48px;
  padding: 0 18px 0 22px;
  font-size: 14.5px;
  font-weight: 650;
  color: #7d6a66;
  cursor: pointer;
  border-radius: 10px;
  transition: color .2s,background .2s;
}

.acc-19__tab::before {
  content: "";
  position: absolute;
  left: 6px;
  top: 50%;
  translate: 0 -50%;
  width: 3.5px;
  height: 0;
  border-radius: 3px;
  background: var(--accent);
  transition: height .25s;
}

.acc-19__tab:hover {
  background: #f4ecea;
}

.acc-19__rail input:checked + .acc-19__tab {
  color: var(--accent);
  background: #f7edeb;
}

.acc-19__rail input:checked + .acc-19__tab::before {
  height: 22px;
}

.acc-19__rail input:focus-visible + .acc-19__tab {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

.acc-19__stage {
  flex: 1;
  display: grid;
  position: relative;
}

.acc-19__panel {
  grid-area: 1/1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 14px;
  padding: 36px 40px;
  clip-path: inset(0 0 0 100%);
  visibility: hidden;
  transition: clip-path .5s cubic-bezier(.65,0,.2,1),visibility 0s .5s;
}

.acc-19__shell:has(#acc-19-a:checked) .acc-19__panel[data-p=a],
.acc-19__shell:has(#acc-19-b:checked) .acc-19__panel[data-p=b],
.acc-19__shell:has(#acc-19-c:checked) .acc-19__panel[data-p=c],
.acc-19__shell:has(#acc-19-d:checked) .acc-19__panel[data-p=d] {
  clip-path: inset(0);
  visibility: visible;
  transition: clip-path .5s cubic-bezier(.65,0,.2,1),visibility 0s;
  z-index: 1;
}

.acc-19__panel b {
  font-size: 34px;
  font-weight: 850;
  color: #2b1d1a;
  letter-spacing: -.02em;
}

.acc-19__panel p {
  font-size: 15px;
  line-height: 1.65;
  color: #5d4b47;
  max-width: 52ch;
}

.acc-19__stat {
  display: flex;
  align-items: baseline;
  gap: 10px;
  font-size: 13px;
  font-weight: 600;
  color: #7d6a66;
}

.acc-19__stat i {
  font-style: normal;
  font-size: 22px;
  font-weight: 800;
  color: var(--accent);
}

@media (max-width: 600px) {
  .acc-19__shell {
    flex-direction: column;
  }

  .acc-19__rail {
    flex-direction: row;
    overflow-x: auto;
    border-right: none;
    border-bottom: 1.5px solid #f0e6e4;
  }

  .acc-19__panel {
    padding: 26px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .acc-19__panel {
    transition: visibility 0s;
  }

  .acc-19__tab,
    .acc-19__tab::before {
    transition: none;
  }
}
/* No JavaScript — all panels stack in one grid cell at full size; :has(radio:checked) transitions the active panel's clip-path from inset(0 0 0 100%) to inset(0), wiping it in like a curtain with zero content reflow. Inactive panels get visibility:hidden so keyboard/AT users never land in covered content. */
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Accordions — Vertical & Horizontal from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Curtain Slide Horizontal Accordion — clip-path Reveal Panels
Source: https://codefronts.com/navigation/css-accordions/curtain-slide/

Every panel is always full width, stacked in the same grid cell — the active one is revealed by an animated clip-path wipe that sweeps across like a stage curtain, driven by a slim tab rail on the left. Unlike flex-grow accordions, content NEVER reflows: it's uncovered, not resized.
## HTML
```html
<section class="acc-19" aria-label="Curtain slide clip-path accordion demo">
  <div class="acc-19__shell" role="group" aria-label="Quarterly report sections">
    <div class="acc-19__rail">
      <input type="radio" name="acc-19" id="acc-19-a" checked><label class="acc-19__tab" for="acc-19-a">Revenue</label>
      <input type="radio" name="acc-19" id="acc-19-b"><label class="acc-19__tab" for="acc-19-b">Customers</label>
      <input type="radio" name="acc-19" id="acc-19-c"><label class="acc-19__tab" for="acc-19-c">Product</label>
      <input type="radio" name="acc-19" id="acc-19-d"><label class="acc-19__tab" for="acc-19-d">Team</label>
    </div>
    <div class="acc-19__stage">
      <div class="acc-19__panel" data-p="a"><b>$4.2M ARR</b><p>Up 38% year over year. Net revenue retention held at 121% while CAC payback improved to 11 months.</p><span class="acc-19__stat"><i>+38%</i>YoY growth</span></div>
      <div class="acc-19__panel" data-p="b"><b>1,940 teams</b><p>Enterprise logos doubled this quarter. The self-serve funnel now converts 4.1% of signups within 14 days.</p><span class="acc-19__stat"><i>2×</i>enterprise logos</span></div>
      <div class="acc-19__panel" data-p="c"><b>31 releases</b><p>Headline: the plugin API shipped to GA. Median time-to-resolution for P1 bugs dropped to 6 hours.</p><span class="acc-19__stat"><i>GA</i>plugin API</span></div>
      <div class="acc-19__panel" data-p="d"><b>58 people</b><p>Nine hires, zero regretted attrition. Engineering is now 40% of the company across four time zones.</p><span class="acc-19__stat"><i>9</i>new hires</span></div>
    </div>
  </div>
</section>
```
## CSS
```css
.acc-19,
.acc-19 *,
.acc-19 *::before,
.acc-19 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.acc-19 {
  --accent: oklch(0.6 0.19 20);
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f7f3f2;
  padding: 44px 24px;
}

.acc-19__shell {
  display: flex;
  width: min(760px,100%);
  min-height: 340px;
  background: #fff;
  border-radius: 18px;
  box-shadow: 0 24px 60px -34px rgba(80,30,25,.35);
  overflow: hidden;
}

.acc-19__rail {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 18px 14px;
  border-right: 1.5px solid #f0e6e4;
  background: #fbf8f7;
}

.acc-19__rail input {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.acc-19__tab {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 48px;
  padding: 0 18px 0 22px;
  font-size: 14.5px;
  font-weight: 650;
  color: #7d6a66;
  cursor: pointer;
  border-radius: 10px;
  transition: color .2s,background .2s;
}

.acc-19__tab::before {
  content: "";
  position: absolute;
  left: 6px;
  top: 50%;
  translate: 0 -50%;
  width: 3.5px;
  height: 0;
  border-radius: 3px;
  background: var(--accent);
  transition: height .25s;
}

.acc-19__tab:hover {
  background: #f4ecea;
}

.acc-19__rail input:checked + .acc-19__tab {
  color: var(--accent);
  background: #f7edeb;
}

.acc-19__rail input:checked + .acc-19__tab::before {
  height: 22px;
}

.acc-19__rail input:focus-visible + .acc-19__tab {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

.acc-19__stage {
  flex: 1;
  display: grid;
  position: relative;
}

.acc-19__panel {
  grid-area: 1/1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 14px;
  padding: 36px 40px;
  clip-path: inset(0 0 0 100%);
  visibility: hidden;
  transition: clip-path .5s cubic-bezier(.65,0,.2,1),visibility 0s .5s;
}

.acc-19__shell:has(#acc-19-a:checked) .acc-19__panel[data-p=a],
.acc-19__shell:has(#acc-19-b:checked) .acc-19__panel[data-p=b],
.acc-19__shell:has(#acc-19-c:checked) .acc-19__panel[data-p=c],
.acc-19__shell:has(#acc-19-d:checked) .acc-19__panel[data-p=d] {
  clip-path: inset(0);
  visibility: visible;
  transition: clip-path .5s cubic-bezier(.65,0,.2,1),visibility 0s;
  z-index: 1;
}

.acc-19__panel b {
  font-size: 34px;
  font-weight: 850;
  color: #2b1d1a;
  letter-spacing: -.02em;
}

.acc-19__panel p {
  font-size: 15px;
  line-height: 1.65;
  color: #5d4b47;
  max-width: 52ch;
}

.acc-19__stat {
  display: flex;
  align-items: baseline;
  gap: 10px;
  font-size: 13px;
  font-weight: 600;
  color: #7d6a66;
}

.acc-19__stat i {
  font-style: normal;
  font-size: 22px;
  font-weight: 800;
  color: var(--accent);
}

@media (max-width: 600px) {
  .acc-19__shell {
    flex-direction: column;
  }

  .acc-19__rail {
    flex-direction: row;
    overflow-x: auto;
    border-right: none;
    border-bottom: 1.5px solid #f0e6e4;
  }

  .acc-19__panel {
    padding: 26px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .acc-19__panel {
    transition: visibility 0s;
  }

  .acc-19__tab,
    .acc-19__tab::before {
    transition: none;
  }
}
```

## JavaScript
```js
/* No JavaScript — all panels stack in one grid cell at full size; :has(radio:checked) transitions the active panel's clip-path from inset(0 0 0 100%) to inset(0), wiping it in like a curtain with zero content reflow. Inactive panels get visibility:hidden so keyboard/AT users never land in covered content. */
```

How this works

A different mechanic from demos 15–18: all four content panels occupy the same grid cell (stacked via grid-area:1/1) at full size, and the active one is shown by transitioning clip-path: inset(0 100% 0 0)inset(0). Because panels never change size, their internal layout is rock solid — no delayed fades needed, no mid-animation reflow; the wipe simply uncovers finished content. This is the right horizontal-accordion architecture when panels hold complex layouts (stats, forms, charts).

Selection lives in a vertical tab rail: real radios with visible labels, an accent bar sliding to the active tab. The outgoing panel wipes out to the RIGHT while the incoming wipes in from the left — two clip transitions with the checked one on top via z-index, reading as one continuous curtain pass.

Techniques used: stacked grid-area:1/1 panels (zero reflow architecture) · animated clip-path inset() wipe · checked-on-top z-index choreography · visible radio tab rail with sliding accent bar · content-complexity-safe (no size change ever)

Make it yours

  • Wipe direction: change which inset side goes to 100% (top/bottom for vertical curtains).
  • Wipe speed/easing on the clip-path transition.
  • The tab rail works on the right or as a top row — it's just flex.
  • Panels can hold anything (forms, charts) since they never resize.

Gotchas — read before shipping

  • Stacked panels are all in the DOM at full size — hide inactive ones from assistive tech and tabbing with visibility:hidden after the wipe (the transition-delay trick here) so keyboard users don't tab into covered content.
  • clip-path transitions repaint the panel; keep heavy filters/shadows OFF the clipped element itself.
  • Radio labels in the rail must stay visible text (not icon-only) — they're the accessible names.
  • inset() basic-shape needs no polygon math — resist the polygon() version unless you need diagonal wipes.

Browser support

ChromeSafariFirefoxEdge
111+15.4+121+111+

Floor set by :has(), oklch() as this demo is written. The core technique itself goes back further — Chrome 55+.

clip-path basic shapes are long-baseline. visibility fallback keeps a11y correct everywhere.

Techniques used in this demo

Search CodeFronts

Loading…