Backstage Pass is a festival site with no photography at all — the lineup typography and three drifting gradient washes do everything a hero image usually does. One HTML file, one stylesheet, one script, GSAP from a CDN. No build step.
Festival posters have always been a typography genre: the headliner huge, the supports cascading beneath in shrinking rows, the fine print in mono. The whole hierarchy of hype is just font size. So the site treats the lineup as the artwork — names at poster scale with a slowly shifting sunset-gradient fill — and keeps the rest ruthlessly informative: a real countdown to gates, a day-by-stage timetable, three honest pass tiers, and a "know before you go" band with zero hype in it. Euphoria in the display type, facts in the mono.
Headliner names are filled with a live gradient using background-clip:text over an
oversized, slowly-panning background — the text itself becomes a little piece of sunset:
.head-name{
background:linear-gradient(100deg,#ff3d8e,#ff8a3d 45%,#8b5cf6 90%);
background-size:300% 100%;
-webkit-background-clip:text; background-clip:text; color:transparent;
animation:grad-shift 11s ease-in-out infinite;
}
@keyframes grad-shift{
0%,100%{background-position:0% 50%} 50%{background-position:100% 50%}
}
Each day block cascades in once on scroll — day label, headliner, then every support name as its own
staggered element, with a touch of skewY so the settle feels like a needle drop:
gsap.fromTo(rows,
{ y:56, opacity:0, skewY:2.5 },
{ y:0, opacity:1, skewY:0, duration:1.05, stagger:0.07,
ease:'expo.out',
scrollTrigger:{ trigger:block, start:'top 78%', once:true } });
// and a slow scrub drift on the headliner while it's on screen
gsap.to(head, { x:44*dir, ease:'none',
scrollTrigger:{ trigger:block, start:'top bottom',
end:'bottom top', scrub:1.2 } });
No GSAP? The same rows animate with CSS transitions and nth-child delays, triggered by an
IntersectionObserver class. Reduced motion? Neither runs — the lineup is simply there, gradient frozen.
The scenery is three fixed, viewport-sized radial gradients — magenta, orange, violet — drifting on
30-second alternating loops. The cheap part matters: the blur is painted into the gradient
(alpha falloff to transparent), not a filter:blur(), and the only animated property is
transform, so the whole background costs the GPU almost nothing:
.wash-m{
background:radial-gradient(circle,
rgba(255,61,142,.42), rgba(255,61,142,.12) 42%, transparent 68%);
mix-blend-mode:screen; will-change:transform;
animation:drift-m 26s ease-in-out infinite alternate;
}
@keyframes drift-m{
from{transform:translate3d(0,0,0) scale(1)}
to {transform:translate3d(9vmax,7vmax,0) scale(1.12)}
}
A real timer to a fixed local datetime, not a fake urgency widget. Every second it recomputes the remaining time; a cell only re-renders (and pulses) when its value actually changes:
const GATES = new Date(2026, 7, 21, 14, 0, 0); // Aug 21, 14:00 local
const setCell = (el, v) => {
if (el.textContent === v) return; // no wasted paints
el.textContent = v;
el.classList.remove('tick');
void el.offsetWidth; // restart the pulse
el.classList.add('tick');
};
Accessibility drove the shape of it: the ticking digits are aria-hidden (a live region
announcing every second would be hostile), while a visually-hidden sentence states the date once.
Under reduced motion the countdown keeps ticking — it's information — but the pulse animation
is disabled. Without JavaScript, CSS swaps the digit grid for a static "Gates open Fri Aug 21" line.
role="tab" buttons with
aria-selected, roving tabindex and arrow-key/Home/End support swap three
role="tabpanel" grids. Without JS, all three days render stacked with their headings —
nothing is lost.overflow-x:auto region with a label), so the page itself never scrolls horizontally..ics — a one-line file download with the
real gates-open datetime, no backend required.Every path is relative and there is no build, so deploying is three commands:
gh repo create backstage-pass --public --source . --push
gh api --method POST /repos/USER/backstage-pass/pages \
-f 'source[branch]=main' -f 'source[path]=/'
# live at https://USER.github.io/backstage-pass/ in ~1 min
A .nojekyll file keeps Pages from touching the folder structure. That's the whole deploy.
Backstage Pass is a design-showcase concept — the festival, the artists and the passes are fictional, and there is no ticketing behind the buttons. See the repository README for the full demo-vs-real map.
← Back to Backstage Pass