Back to Code Bytes
3 min read
Page Side-by-side

Description

Page Side-by-side animates two views that live in the same container, with page 1 exiting left and page 2 exiting right. It suits list-to-detail patterns, multi-step wizards, or any two-screen flow where you want a spatial sense of direction without a full page reload. A data attribute on the parent is all the JS needs to drive it.

Example

Inbox

Meeting notes · Alice

Design review · Bob

Weekly standup · Team

Meeting notes · Alice

Tap ← Back to list to return

CSS

:root {
  --page-slide-dur: 200ms;
  --page-fade-dur: 200ms;
  --page-slide-distance: 8px;
  --page-blur: 3px;
  --page-stagger: 0ms;
  --page-exit-enabled: 1;
  --page-slide-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --page-fade-ease: cubic-bezier(0.22, 1, 0.36, 1);
}

.t-page-slide {
  position: relative;
}
.t-page-slide .t-page[data-page-id="1"] {
  --t-page-from-x: calc(var(--page-slide-distance) * -1);
}
.t-page-slide .t-page[data-page-id="2"] {
  --t-page-from-x: var(--page-slide-distance);
}
.t-page-slide .t-page {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  transform: translateX(
    calc(var(--t-page-from-x, 0px) * var(--page-exit-enabled))
  );
  filter: blur(calc(var(--page-blur) * var(--page-exit-enabled)));
  transition:
    opacity var(--page-fade-dur) var(--page-fade-ease),
    transform var(--page-slide-dur) var(--page-slide-ease),
    filter var(--page-slide-dur) var(--page-slide-ease);
  will-change: opacity, transform, filter;
}
.t-page-slide[data-page="1"] .t-page[data-page-id="1"],
.t-page-slide[data-page="2"] .t-page[data-page-id="2"] {
  opacity: 1;
  pointer-events: auto;
  transform: translateX(0);
  filter: blur(0);
  transition-delay: var(--page-stagger);
}

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

React

import { useState } from "react";
import "./page-side-by-side.css"; // paste the CSS above

export function PageSideBySide() {
  const [page, setPage] = useState<"1" | "2">("1");
  return (
    <div style={{ position: "relative", width: 280, height: 170 }}>
      <div className="t-page-slide" data-page={page}>
        <section className="t-page" data-page-id="1">
          <p>Inbox</p>
          {/* list rows */}
        </section>
        <section className="t-page" data-page-id="2">
          <p>Detail view</p>
          {/* detail content */}
        </section>
      </div>
      <button onClick={() => setPage((p) => (p === "1" ? "2" : "1"))}>
        {page === "1" ? "Open detail" : "Back to list"}
      </button>
    </div>
  );
}

Variables

VariableDefaultNotes
--page-slide-dur200mssourced from --p8-slide-dur
--page-fade-dur200mssourced from --p8-fade-dur
--page-slide-distance8pxsourced from --p8-distance
--page-blur3pxsourced from --p8-blur
--page-stagger0mssourced from --p8-stagger
--page-exit-enabled1sourced from --p8-exit-enabled
--page-slide-easecubic-bezier(0.22, 1, 0.36, 1)sourced from --p8-slide-ease
--page-fade-easecubic-bezier(0.22, 1, 0.36, 1)sourced from --p8-fade-ease

Credit

Adapted from Page Side-by-side on transitions.dev by Jakub Antalik. Original source: github.com/Jakubantalik/transitions.dev.