The Daily Motion
Vol. XIILate edition2 cents
The Daily Motion
Vol. XIILate edition2 cents
import { useEffect, useRef, useState } from 'react';
const css = `
.card-broadsheet { flex: none; --paper: light-dark(#fbfcfd, #e5e6e9); --print: light-dark(#373a40, #121417); position: relative; width: 214px; height: 222px; color: var(--print); font-family: Georgia, 'Times New Roman', serif; perspective: 700px; perspective-origin: 50% 40%; }
.card-broadsheet__sheet { position: absolute; inset: 0; transform: translateY(45px) rotate(-1.5deg); transform-style: preserve-3d; transition: transform 0.7s cubic-bezier(0.3, 1.1, 0.5, 1); }
.card-broadsheet[data-open='true'] .card-broadsheet__sheet { transform: none; }
.card-broadsheet[data-spin='true'] .card-broadsheet__sheet { animation: card-broadsheet-spin 1.15s cubic-bezier(0.22, 0.7, 0.3, 1) both; }
@keyframes card-broadsheet-spin {
0% { transform: translateY(45px) rotate(-1090deg) scale(0.04); }
82% { transform: translateY(45px) rotate(-1.5deg) scale(1.08); }
90% { transform: translateY(45px) rotate(-1.5deg) scale(0.985); }
100% { transform: translateY(45px) rotate(-1.5deg) scale(1); }
}
.card-broadsheet__top, .card-broadsheet__flap { position: absolute; left: 0; right: 0; box-sizing: border-box; padding: 7px 10px; background: var(--paper); }
.card-broadsheet__top { top: 0; height: 132px; transform: translateZ(1px); box-shadow: 0 0 0 1px color-mix(in oklab, var(--print) 12%, transparent), 0 8px 18px -8px light-dark(rgba(0, 0, 0, 0.17), rgba(0, 0, 0, 0.55)); }
.card-broadsheet__flap { top: 132px; height: 90px; padding-top: 7px; transform-origin: 50% 0; transform: rotateX(-178deg); transition: transform 0.7s cubic-bezier(0.3, 1.1, 0.5, 1); box-shadow: 0 0 0 1px color-mix(in oklab, var(--print) 12%, transparent), 0 8px 18px -8px light-dark(rgba(0, 0, 0, 0.17), rgba(0, 0, 0, 0.55)); pointer-events: none; }
.card-broadsheet[data-open='true'] .card-broadsheet__flap { transform: rotateX(0); pointer-events: auto; }
.card-broadsheet__crease { position: absolute; left: 0; right: 0; top: 0; height: 16px; background: linear-gradient(to bottom, color-mix(in oklab, var(--print) 22%, transparent), transparent); pointer-events: none; }
.card-broadsheet__name { margin: 0; font-size: 19px; font-weight: 700; letter-spacing: -0.01em; line-height: 1.05; text-align: center; }
.card-broadsheet__dateline { display: flex; justify-content: space-between; margin: 3px 0 0; padding: 2px 0; border-top: 3px double var(--print); border-bottom: 1px solid var(--print); font-size: 7px; letter-spacing: 0.05em; text-transform: uppercase; }
.card-broadsheet__head { margin: 5px 0 0; font-family: 'Times New Roman', Times, serif; font-size: 27px; font-weight: 700; letter-spacing: -0.03em; line-height: 0.98; text-align: center; text-transform: uppercase; text-wrap: balance; }
.card-broadsheet__more { display: block; width: 100%; margin-top: 5px; padding: 3px 0 0; border: 0; border-top: 1px solid var(--print); background: none; color: inherit; font: italic 700 10.5px/1.3 Georgia, 'Times New Roman', serif; text-align: center; cursor: pointer; }
.card-broadsheet__more:hover { text-decoration: underline; }
.card-broadsheet__more:focus-visible { outline: 2px solid var(--print); outline-offset: 1px; }
.card-broadsheet__body { display: grid; grid-template-columns: 76px 1fr 1fr; gap: 7px; }
.card-broadsheet__photo { height: 74px; background: radial-gradient(circle at 50% 50%, var(--print) 0.9px, transparent 1.15px) 0 0 / 3px 3px, radial-gradient(circle at 70% 26%, var(--paper) 12px, transparent 13px), linear-gradient(170deg, color-mix(in oklab, var(--print) 26%, var(--paper)), color-mix(in oklab, var(--print) 58%, var(--paper))); }
.card-broadsheet__col { margin: 0; font-size: 6.2px; line-height: 1.22; text-align: justify; hyphens: auto; }
.card-broadsheet__col::first-letter { font-size: 15px; float: left; line-height: 0.86; padding-right: 2px; font-weight: 700; }
.card-broadsheet__col + .card-broadsheet__col::first-letter { font-size: inherit; float: none; padding: 0; font-weight: 400; }
@media (prefers-reduced-motion: reduce) {
.card-broadsheet[data-spin='true'] .card-broadsheet__sheet { animation: none; }
.card-broadsheet__sheet, .card-broadsheet__flap { transition: none; }
}
`;
export function BroadsheetCard() {
const root = useRef<HTMLElement>(null);
const [open, setOpen] = useState(false);
const [spin, setSpin] = useState(false);
// It only spins in once it is on screen, or nobody would see it land.
useEffect(() => {
const el = root.current;
if (!el) return;
const io = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setSpin(true);
io.disconnect();
}
});
io.observe(el);
return () => io.disconnect();
}, []);
return (
<>
<style>
{css}
</style>
<article ref={root} className="card-broadsheet" data-open={open} data-spin={spin && !open} onAnimationEnd={() => setSpin(false)}>
<div className="card-broadsheet__sheet">
<div className="card-broadsheet__top">
<p className="card-broadsheet__name">The Daily Motion</p>
<p className="card-broadsheet__dateline">
<span>Vol. XII</span>
<span>Late edition</span>
<span>2 cents</span>
</p>
<h3 className="card-broadsheet__head">Easing is back</h3>
<button type="button" className="card-broadsheet__more" aria-expanded={open} onClick={() => setOpen((o) => !o)}>
{open ? 'Fold it back up' : 'Continued below the fold'}
</button>
</div>
<div className="card-broadsheet__flap" aria-hidden={!open}>
<span className="card-broadsheet__crease" />
<div className="card-broadsheet__body">
<div className="card-broadsheet__photo" />
<p className="card-broadsheet__col">Linear motion, long the choice of the hurried, was seen leaving town on Tuesday. Witnesses describe a slow start and a gentle landing.</p>
<p className="card-broadsheet__col">Designers across the city report that objects now arrive as though they had weight. Asked for comment, the curve would only say that timing is everything.</p>
</div>
</div>
</div>
</article>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardBroadsheet(el) on the mounted element instead.
<article class="card-broadsheet" data-open="false" data-spin="false">
<div class="card-broadsheet__sheet">
<div class="card-broadsheet__top">
<p class="card-broadsheet__name">The Daily Motion</p>
<p class="card-broadsheet__dateline">
<span>Vol. XII</span>
<span>Late edition</span>
<span>2 cents</span>
</p>
<h3 class="card-broadsheet__head">Easing is back</h3>
<button type="button" class="card-broadsheet__more" aria-expanded="false">Continued below the fold</button>
</div>
<div class="card-broadsheet__flap" aria-hidden="true">
<span class="card-broadsheet__crease"></span>
<div class="card-broadsheet__body">
<div class="card-broadsheet__photo"></div>
<p class="card-broadsheet__col">Linear motion, long the choice of the hurried, was seen leaving town on Tuesday. Witnesses describe a slow start and a gentle landing.</p>
<p class="card-broadsheet__col">Designers across the city report that objects now arrive as though they had weight. Asked for comment, the curve would only say that timing is everything.</p>
</div>
</div>
</div>
</article>.card-broadsheet { flex: none; --paper: light-dark(#fbfcfd, #e5e6e9); --print: light-dark(#373a40, #121417); position: relative; width: 214px; height: 222px; color: var(--print); font-family: Georgia, 'Times New Roman', serif; perspective: 700px; perspective-origin: 50% 40%; }
.card-broadsheet__sheet { position: absolute; inset: 0; transform: translateY(45px) rotate(-1.5deg); transform-style: preserve-3d; transition: transform 0.7s cubic-bezier(0.3, 1.1, 0.5, 1); }
.card-broadsheet[data-open='true'] .card-broadsheet__sheet { transform: none; }
.card-broadsheet[data-spin='true'] .card-broadsheet__sheet { animation: card-broadsheet-spin 1.15s cubic-bezier(0.22, 0.7, 0.3, 1) both; }
@keyframes card-broadsheet-spin {
0% { transform: translateY(45px) rotate(-1090deg) scale(0.04); }
82% { transform: translateY(45px) rotate(-1.5deg) scale(1.08); }
90% { transform: translateY(45px) rotate(-1.5deg) scale(0.985); }
100% { transform: translateY(45px) rotate(-1.5deg) scale(1); }
}
.card-broadsheet__top, .card-broadsheet__flap { position: absolute; left: 0; right: 0; box-sizing: border-box; padding: 7px 10px; background: var(--paper); }
.card-broadsheet__top { top: 0; height: 132px; transform: translateZ(1px); box-shadow: 0 0 0 1px color-mix(in oklab, var(--print) 12%, transparent), 0 8px 18px -8px light-dark(rgba(0, 0, 0, 0.17), rgba(0, 0, 0, 0.55)); }
.card-broadsheet__flap { top: 132px; height: 90px; padding-top: 7px; transform-origin: 50% 0; transform: rotateX(-178deg); transition: transform 0.7s cubic-bezier(0.3, 1.1, 0.5, 1); box-shadow: 0 0 0 1px color-mix(in oklab, var(--print) 12%, transparent), 0 8px 18px -8px light-dark(rgba(0, 0, 0, 0.17), rgba(0, 0, 0, 0.55)); pointer-events: none; }
.card-broadsheet[data-open='true'] .card-broadsheet__flap { transform: rotateX(0); pointer-events: auto; }
.card-broadsheet__crease { position: absolute; left: 0; right: 0; top: 0; height: 16px; background: linear-gradient(to bottom, color-mix(in oklab, var(--print) 22%, transparent), transparent); pointer-events: none; }
.card-broadsheet__name { margin: 0; font-size: 19px; font-weight: 700; letter-spacing: -0.01em; line-height: 1.05; text-align: center; }
.card-broadsheet__dateline { display: flex; justify-content: space-between; margin: 3px 0 0; padding: 2px 0; border-top: 3px double var(--print); border-bottom: 1px solid var(--print); font-size: 7px; letter-spacing: 0.05em; text-transform: uppercase; }
.card-broadsheet__head { margin: 5px 0 0; font-family: 'Times New Roman', Times, serif; font-size: 27px; font-weight: 700; letter-spacing: -0.03em; line-height: 0.98; text-align: center; text-transform: uppercase; text-wrap: balance; }
.card-broadsheet__more { display: block; width: 100%; margin-top: 5px; padding: 3px 0 0; border: 0; border-top: 1px solid var(--print); background: none; color: inherit; font: italic 700 10.5px/1.3 Georgia, 'Times New Roman', serif; text-align: center; cursor: pointer; }
.card-broadsheet__more:hover { text-decoration: underline; }
.card-broadsheet__more:focus-visible { outline: 2px solid var(--print); outline-offset: 1px; }
.card-broadsheet__body { display: grid; grid-template-columns: 76px 1fr 1fr; gap: 7px; }
.card-broadsheet__photo { height: 74px; background: radial-gradient(circle at 50% 50%, var(--print) 0.9px, transparent 1.15px) 0 0 / 3px 3px, radial-gradient(circle at 70% 26%, var(--paper) 12px, transparent 13px), linear-gradient(170deg, color-mix(in oklab, var(--print) 26%, var(--paper)), color-mix(in oklab, var(--print) 58%, var(--paper))); }
.card-broadsheet__col { margin: 0; font-size: 6.2px; line-height: 1.22; text-align: justify; hyphens: auto; }
.card-broadsheet__col::first-letter { font-size: 15px; float: left; line-height: 0.86; padding-right: 2px; font-weight: 700; }
.card-broadsheet__col + .card-broadsheet__col::first-letter { font-size: inherit; float: none; padding: 0; font-weight: 400; }
@media (prefers-reduced-motion: reduce) {
.card-broadsheet[data-spin='true'] .card-broadsheet__sheet { animation: none; }
.card-broadsheet__sheet, .card-broadsheet__flap { transition: none; }
}function initCardBroadsheet(root) {
var more = root.querySelector('.card-broadsheet__more');
var flap = root.querySelector('.card-broadsheet__flap');
var open = false;
// It only spins in once it is on screen, or nobody would see it land.
var io = new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) {
root.setAttribute('data-spin', 'true');
io.disconnect();
}
});
function onEnd() {
root.setAttribute('data-spin', 'false');
}
function onClick() {
open = !open;
root.setAttribute('data-open', String(open));
root.setAttribute('data-spin', 'false');
more.setAttribute('aria-expanded', String(open));
more.textContent = open ? 'Fold it back up' : 'Continued below the fold';
flap.setAttribute('aria-hidden', String(!open));
}
io.observe(root);
root.addEventListener('animationend', onEnd);
more.addEventListener('click', onClick);
return function () {
io.disconnect();
root.removeEventListener('animationend', onEnd);
more.removeEventListener('click', onClick);
};
}
document.querySelectorAll('.card-broadsheet').forEach(initCardBroadsheet);Goal: Create a "Broadsheet" card.
Visual behavior: An article as a folded newspaper. It spins in like the old film trope and lands; Continued below the fold really does swing the bottom half down, with dense serif type all the way.
Technique:
- The paper is two panels. The lower one hangs from the fold and rests turned almost fully behind the upper one; opening transitions it to flat in perspective while the sheet slides up to stay centred.
- An IntersectionObserver starts the spin the first time the card is seen. The photo is a halftone made of a repeating radial gradient over a soft gradient.
Accessibility: An article with a heading and one button that keeps focus as it toggles, with aria-expanded telling whether the fold is open. The fold is aria-hidden while closed. Under prefers-reduced-motion it does not spin and opens at once.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Broadsheet" card.
Visual behavior: An article as a folded newspaper. It spins in like the old film trope and lands; Continued below the fold really does swing the bottom half down, with dense serif type all the way.
Technique:
- The paper is two panels. The lower one hangs from the fold and rests turned almost fully behind the upper one; opening transitions it to flat in perspective while the sheet slides up to stay centred.
- An IntersectionObserver starts the spin the first time the card is seen. The photo is a halftone made of a repeating radial gradient over a soft gradient.
Accessibility: An article with a heading and one button that keeps focus as it toggles, with aria-expanded telling whether the fold is open. The fold is aria-hidden while closed. Under prefers-reduced-motion it does not spin and opens at once.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
An article as a folded newspaper. It spins in like the old film trope and lands; Continued below the fold really does swing the bottom half down, with dense serif type all the way.
An article with a heading and one button that keeps focus as it toggles, with aria-expanded telling whether the fold is open. The fold is aria-hidden while closed. Under prefers-reduced-motion it does not spin and opens at once.
Pick your hand
Your profile6 of 9
Next: Add a photo