---
name: curtain-reveal
description: Use when you want "Dramatic, theatrical, premium" - Opens or closes panels like stage curtains.
---

# Curtain Reveal

> **Category:** Loading / Transition  -  **Personality:** Dramatic, theatrical, premium
>
> **Best use:** Launch pages, presentations
>
> **Ratings:** Professional 3/5 - Casual 3/5 - Exotic 4/5 - Visual impact 4/5 - Difficulty 3/5 - Performance cost 2/5

## What it is

Curtain Reveal covers your content with two solid panels styled like stage curtains, then slides them apart on a trigger to unveil what's behind — a hero, a product shot, a title slide. There's no real navigation: the panels are just transformed elements sitting on top of normal content, so it's a pure presentational *beat of anticipation* before the payoff. It earns its keep on launch pages, product unveilings and presentation intros, where the slow, weighted slide (plus theatrical dressing — velvet pleats, a scalloped valance, spotlights) makes a plain hero feel like an event.

## Dependencies / CDN

**None — pure CSS transitions + ~12 lines of vanilla JS** to toggle one class. No animation library.

The demo only pulls two Google Fonts (optional — swap for your own):

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=Playfair+Display:ital,wght@0,700;0,800;1,500;1,700;1,800&display=swap" rel="stylesheet">
```

## HTML

The reveal needs three things: a clipped stage, the content, and two curtain panels over it. Everything else (valance, playbill, spotlights) is theatrical dressing.

```html
<div class="cr-theater" id="crTheater">
  <!-- the content that gets revealed -->
  <div class="cr-scene" id="crScene">
    <h2>Sound takes <em>center stage</em>.</h2>
    <!-- ...hero copy, product image, CTAs... -->
  </div>

  <!-- the two curtains, on top of the scene -->
  <div class="cr-curtain cr-left"  aria-hidden="true"></div>
  <div class="cr-curtain cr-right" aria-hidden="true"></div>
  <div class="cr-valance" aria-hidden="true"></div><!-- optional scalloped pelmet -->

  <!-- replayable trigger -->
  <button id="crBtn" type="button" aria-pressed="false" aria-controls="crScene">
    Raise the curtain
  </button>
</div>
```

## CSS

```css
/* Stage: clips the off-screen panels; in-flow .cr-scene sets its height */
.cr-theater{position:relative; overflow:hidden; border-radius:24px; isolation:isolate; background:#0b0405}
.cr-scene{position:relative; z-index:1; min-height:620px; display:grid; place-items:center; padding:72px 30px}

/* THE CURTAINS — two panels, each just over half-width so they overlap (no centre gap) */
.cr-curtain{
  position:absolute; top:0; bottom:0; width:54%; z-index:5; will-change:transform;
  transition:transform 1.5s cubic-bezier(.66,0,.2,1);     /* heavy, weighted pull */
  background-color:#6e0f1b;
  background-image:
    /* vertical velvet pleats: dark valley -> bright ridge -> dark valley, repeating */
    repeating-linear-gradient(90deg,
      rgba(0,0,0,.58) 0, rgba(0,0,0,.04) 16px,
      rgba(255,255,255,.13) 32px, rgba(0,0,0,.04) 48px, rgba(0,0,0,.58) 64px),
    radial-gradient(130% 70% at 50% 0, rgba(255,255,255,.14), transparent 42%),  /* top sheen */
    linear-gradient(180deg,#8a1626,#5a0c16 60%,#3c0810);                          /* base velvet */
}
.cr-left { left:0;  transform-origin:left center;  box-shadow:inset -46px 0 70px -22px rgba(0,0,0,.65)}
.cr-right{ right:0; transform-origin:right center; box-shadow:inset  50px 0 80px -22px rgba(0,0,0,.72)}

/* THE REVEAL — slide each panel fully past its own edge (101% clears soft shadows) */
.cr-theater.is-open .cr-left { transform:translateX(-101%) }
.cr-theater.is-open .cr-right{ transform:translateX( 101%) }

/* let the content settle in just after the curtains start to part */
.cr-scene{opacity:0; transform:translateY(20px) scale(.985);
  transition:opacity .7s ease .4s, transform .9s cubic-bezier(.2,.7,.2,1) .4s}
.cr-theater.is-open .cr-scene{opacity:1; transform:none}

/* optional scalloped valance via a repeating radial-gradient mask */
.cr-valance{position:absolute; top:0; left:0; right:0; height:88px; z-index:7; --r:38px;
  background:#6e0f1b;
  -webkit-mask:radial-gradient(var(--r) at 50% 100%,#0000 99%,#000) 0 0/calc(2*var(--r)) 100% repeat-x;
          mask:radial-gradient(var(--r) at 50% 100%,#0000 99%,#000) 0 0/calc(2*var(--r)) 100% repeat-x}

/* reduced motion: the reveal still happens, just instantly */
@media (prefers-reduced-motion: reduce){
  .cr-curtain,.cr-scene{transition-duration:.001ms}
}
```

## JavaScript

All the JS does is toggle one class — CSS owns every pixel of motion, so it stays reduced-motion-safe with no loops.

```js
(function(){
  var theater = document.getElementById('crTheater'),
      btn     = document.getElementById('crBtn');
  if(!theater || !btn) return;

  function setOpen(open){
    theater.classList.toggle('is-open', open);
    btn.setAttribute('aria-pressed', open ? 'true' : 'false');
    btn.textContent = open ? 'Lower the curtain' : 'Raise the curtain';
  }
  btn.addEventListener('click', function(){
    setOpen(!theater.classList.contains('is-open'));
  });
  setOpen(false); // start closed; click toggles open/closed → replayable
})();
```

## How it works

- **Two panels, one transform.** Each curtain is absolutely positioned over the stage at `width:54%` (`left:0` / `right:0`). The `.is-open` class translates them out by `±101%` of their *own* width; `overflow:hidden` on the stage clips the part that leaves. That's the entire effect.
- **They overlap on purpose.** 54% + 54% > 100%, so the panels meet (and slightly overlap) at the centre — no hairline gap shows through mid-slide. Inner `box-shadow` on each panel darkens the meeting edge into a believable centre fold.
- **Weight comes from the easing.** `cubic-bezier(.66,0,.2,1)` over `1.5s` starts heavy, accelerates, then settles — like real fabric being hauled aside. A linear or fast ease kills the drama.
- **The content settles, it doesn't pop.** `.cr-scene` fades + lifts with a `0.4s` delay, so it resolves *as* the curtains clear rather than being there the whole time.
- **Velvet is faked with gradients.** A `repeating-linear-gradient` of dark-valley → bright-ridge bands gives vertical pleats; a top radial sheen and a vertical base gradient add depth. The valance's scalloped lip is a repeating `radial-gradient` used as a `mask`.

## Customization

- **Direction.** Swap `translateX` for `translateY(-101%)` / `translateY(101%)` and stack the panels top/bottom for a vertical "lift". Or use a single panel that wipes one way.
- **Pace & drama.** Tune the `1.5s` duration and the bezier. Longer + heavier = more ceremony; shorter = a snappy UI transition.
- **Material.** Recolour the three `background-image` layers — deep blue/silver for a gala, charcoal/neon for a product drop, or drop the pleats entirely for flat designer panels. Increase `rgba(255,255,255,.13)` ridge alpha for shinier satin.
- **Auto-play.** Call `setOpen(true)` after a short `setTimeout` (or on `load`) for a page-intro loader instead of a button.
- **More panels.** Three or four narrower strips with staggered `transition-delay` reads like a louvre/blind reveal.

## Accessibility & performance

- **Cheap by construction.** Only `transform` and `opacity` animate — both run on the compositor, no layout/paint thrash. That's why Performance cost is 2/5 even with the velvet gradients (gradients paint once).
- **Honour `prefers-reduced-motion`.** Collapse the transition to `~0ms` so the content still reveals instantly without the long slide; don't remove the reveal itself. Any ambient loops (drifting dust, swaying light beams) should be set to `animation:none` under the same query.
- **Keep the content real.** The revealed markup stays in the DOM and in tab order the whole time — it's only visually covered. Don't `display:none` it (you'd lose the transition *and* hide it from assistive tech).
- **Wire the control.** The trigger is a real `<button>` with `aria-pressed` and `aria-controls` pointing at the scene; mark every curtain/valance/spotlight layer `aria-hidden="true"` so screen readers skip the scenery.

## Gotchas

- **`overflow:hidden` on the stage is mandatory** — without it the panels translated off-screen create a horizontal scrollbar. (Never use `width:100vw` either.)
- **Use `101%`, not `100%`.** Panels carry a soft outer shadow; `100%` can leave a faint shadow sliver clinging to the edge. The extra 1% pushes it fully out.
- **Make the panels overlap.** Two panels at exactly `50%` will flash a 1px seam of background during the slide; `54%` (or any value >50%) cures it.
- **Animate `transform`, never `left`/`width`.** Animating layout properties is janky and defeats the GPU path.
- **The reveal can't transition if you toggle `display`.** Keep panels in flow and just transform them.
- **A global reduced-motion reset can zero your duration.** If a shared stylesheet sets `transition-duration:.001ms` on `*`, your curtain snaps instead of sliding under reduced motion — that's correct behaviour, not a bug.
