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
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
| Variable | Default | Notes |
|---|---|---|
--page-slide-dur | 200ms | sourced from --p8-slide-dur |
--page-fade-dur | 200ms | sourced from --p8-fade-dur |
--page-slide-distance | 8px | sourced from --p8-distance |
--page-blur | 3px | sourced from --p8-blur |
--page-stagger | 0ms | sourced from --p8-stagger |
--page-exit-enabled | 1 | sourced from --p8-exit-enabled |
--page-slide-ease | cubic-bezier(0.22, 1, 0.36, 1) | sourced from --p8-slide-ease |
--page-fade-ease | cubic-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.