Transitions.dev

Home / Transitions

Panel reveal

Panel open / close transition

Open the live demo Install with the Skill

CSS

Namespaced classes and motion tokens you can retune. The reduced-motion guard ships with the snippet.

/* Transitions.dev — Panel reveal */

/*
  Usage:
    <div class="t-panel-slide" data-open="false">
      <!-- your panel contents -->
    </div>

  The panel slides on the Y axis, fades opacity 0 ↔ 1,
  and cross-blurs --panel-blur ↔ 0, all on the same
  duration / ease so a shorter travel (e.g. 50% of the
  panel height) still reads as a full open / close.
  Wrap it in your own container with `overflow: hidden`
  if you want the closed state fully clipped. Set
  --panel-translate-y to the travel distance (e.g. half
  the panel's own height).
*/

:root {
  --panel-open-dur: 400ms;
  --panel-close-dur: 350ms;
  --panel-translate-y: calc(187px * 0.5);
  --panel-blur: 2px;
  --panel-ease: cubic-bezier(0.22, 1, 0.36, 1);
}

.t-panel-slide {
  transform: translateY(var(--panel-translate-y));
  opacity: 0;
  filter: blur(var(--panel-blur));
  pointer-events: none;
  transition:
    transform var(--panel-close-dur) var(--panel-ease),
    opacity   var(--panel-close-dur) var(--panel-ease),
    filter    var(--panel-close-dur) var(--panel-ease);
  will-change: transform, opacity, filter;
}
.t-panel-slide[data-open="true"] {
  transform: translateY(0);
  opacity: 1;
  filter: blur(0);
  pointer-events: auto;
  transition:
    transform var(--panel-open-dur) var(--panel-ease),
    opacity   var(--panel-open-dur) var(--panel-ease),
    filter    var(--panel-open-dur) var(--panel-ease);
}

@media (prefers-reduced-motion: reduce) {
  .t-panel-slide { transition: none !important; }
}

React

Self-contained component; the styles inject themselves on first import.

// Transitions.dev — Panel reveal (React, self-contained)
// Drop into any React project — no extra CSS file needed.

import { useState } from "react";

// ── Styles ──────────────────────────────────────────────
// Auto-injected on first import. Idempotent (guarded by
// the element id) and SSR-safe (no-ops without document).
const __TRANSITION_STYLES = `
:root {
  --panel-open-dur: 400ms;
  --panel-close-dur: 350ms;
  --panel-translate-y: calc(187px * 0.5);
  --panel-blur: 2px;
  --panel-ease: cubic-bezier(0.22, 1, 0.36, 1);
}

.t-panel-slide {
  transform: translateY(var(--panel-translate-y));
  opacity: 0;
  filter: blur(var(--panel-blur));
  pointer-events: none;
  transition:
    transform var(--panel-close-dur) var(--panel-ease),
    opacity   var(--panel-close-dur) var(--panel-ease),
    filter    var(--panel-close-dur) var(--panel-ease);
  will-change: transform, opacity, filter;
}
.t-panel-slide[data-open="true"] {
  transform: translateY(0);
  opacity: 1;
  filter: blur(0);
  pointer-events: auto;
  transition:
    transform var(--panel-open-dur) var(--panel-ease),
    opacity   var(--panel-open-dur) var(--panel-ease),
    filter    var(--panel-open-dur) var(--panel-ease);
}

@media (prefers-reduced-motion: reduce) {
  .t-panel-slide { transition: none !important; }
}
`;
if (typeof document !== "undefined" && !document.getElementById("transitions-p3")) {
  const __style = document.createElement("style");
  __style.id = "transitions-p3";
  __style.textContent = __TRANSITION_STYLES;
  document.head.appendChild(__style);
}

// Pair with the CSS from the CSS tab (.t-panel-slide + data-open).
export function PanelReveal({ children }) {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button type="button" onClick={() => setOpen((v) => !v)}>
        {open ? "Hide" : "Show"} panel
      </button>

      {/* Wrap the panel in a container that clips the travel area. */}
      <div style={{ overflow: "hidden" }}>
        <div className="t-panel-slide" data-open={open}>
          {children}
        </div>
      </div>
    </>
  );
}