Halloween Rave 2026 - Scooter @ Ergo Arena, Gdańsk
Marketing site for an electronic music festival headlined by Scooter. Dark festival aesthetic, lineup sections, countdown timer, ticketing. Targets the Polish electronic music market.
Scooter + 4 DJs
Brief
Halloween Rave 2026 is an electronic music festival at Ergo Arena (Gdańsk/Sopot) on October 30, 2026. Scooter headlines, with support from DJ Antoine, Merlin, Vanesa Hardt and Doom. The promoter wanted a marketing site targeted exclusively at the Polish market - one locale, one domain, one audience: rave and hard-dance fans 25–40 who discover events on Instagram, TikTok, and Google searches like "scooter koncert 2026". Sales run through a local ticketing partner; the site is a conversion funnel.
The runway from kickoff to live was 9 days. The promoter had hero photos, lineup graphics, and a venue plan ready. I delivered the stack, frontend, ticketing integration, and SEO setup.
Why dark mode + neon
A halloween rave fan is scrolling on their phone in bed on a Saturday night. Light mode is physically uncomfortable in that context and outright kills the rave aesthetic. I went with near-black background (#0A0A0F), neon accents in toxic green (#39FF14), purple (#9D00FF) and UV magenta (#FF00C8). Geist Mono for counters, Inter for body, high contrast for accessibility (AA for body, AAA for CTAs).
The palette isn't decoration - it's a signal to the audience. "This event is for you" has to be obvious within 0.4 s, otherwise they swipe away.
Lineup grid + Motion
The lineup is the page's hero. I built a grid with a staggered entrance animation revealing 5 artists via motion:
import { motion } from 'motion/react';
const stagger = { animate: { transition: { staggerChildren: 0.08 } } };
const fadeUp = {
initial: { opacity: 0, y: 24, filter: 'blur(8px)' },
animate: { opacity: 1, y: 0, filter: 'blur(0px)', transition: { duration: 0.5 } },
};
export function LineupGrid({ artists }: { artists: Artist[] }) {
return (
<motion.ul
variants={stagger}
initial="initial"
whileInView="animate"
viewport={{ once: true, amount: 0.3 }}
className="grid grid-cols-2 gap-4 md:grid-cols-3"
>
{artists.map((a) => (
<motion.li key={a.slug} variants={fadeUp}>
<ArtistCard artist={a} />
</motion.li>
))}
</motion.ul>
);
}viewport={{ once: true }} prevents re-triggers on scroll up/down. staggerChildren: 0.08 gives 5 cards a 400 ms total animation - noticeable but not annoying.
Countdown timer with pause-on-blur
The countdown is the hero element. A naive setInterval implementation on mobile drifts when the tab sleeps (Safari throttles to once per 60 s). I used requestAnimationFrame + the Page Visibility API:
'use client';
import { useEffect, useState } from 'react';
const TARGET = new Date('2026-10-30T20:00:00+01:00').getTime();
export function Countdown() {
const [remaining, setRemaining] = useState(() => TARGET - Date.now());
useEffect(() => {
let raf = 0;
let lastTick = performance.now();
const tick = (now: number) => {
if (document.hidden) {
// tab hidden - don't count, save battery
raf = requestAnimationFrame(tick);
return;
}
if (now - lastTick >= 1000) {
lastTick = now;
setRemaining(TARGET - Date.now());
}
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
const onVis = () => {
// resync immediately on return, otherwise countdown shows stale data
lastTick = performance.now();
setRemaining(TARGET - Date.now());
};
document.addEventListener('visibilitychange', onVis);
return () => {
cancelAnimationFrame(raf);
document.removeEventListener('visibilitychange', onVis);
};
}, []);
return <CountdownDigits ms={Math.max(0, remaining)} />;
}Measured: pause-on-blur saved roughly 3–5 % battery/hour on low-end Android, which on a festival promo page where users keep the tab open is noticeable.
Ticketing CTA polymorphism
The Polish vendor uses its own embed/iframe + deeplink scheme. Instead of writing a special component, I abstracted the integration:
type Vendor = 'eBilet' | 'goingApp' | 'kupBilecik';
interface TicketProvider {
buyUrl(eventId: string, tier?: string): string;
trackingId: string;
}The CTA component takes vendor and renders the right href plus analytics tracking. When the promoter adds another ticketing partner, it's 5 lines in the provider, not a page refactor.
SEO play
The Polish search market is dominated by Google (~95 %), with Bing at 5–7 % in some demographics. I targeted long-tail keywords:
| Keyword | Monthly volume | Position at T+14 |
|---|---|---|
halloween rave gdańsk | 1,200 | #2 |
scooter koncert 2026 | 4,800 | #4 |
ergo arena halloween | 320 | #1 |
bilety halloween rave | 880 | #3 |
Structured data (MusicEvent schema.org), Open Graph with the neon hero, sitemap.xml + robots.txt with explicit allow. No hreflang - the site is Polish-only, no locale switcher.
Asset handling
The laser-show hero photos look great but they're 6 MB JPEGs. Next/Image works wonders:
| Photo | Origin (JPEG) | AVIF served | Saving |
|---|---|---|---|
| Hero (1920×1080) | 5.8 MB | 290 kB | 95 % |
| Lineup card (600×600) | 480 kB | 32 kB | 93 % |
| Gallery thumb (400×400) | 220 kB | 18 kB | 92 % |
The gallery below the fold is lazy-loaded via loading="lazy" + IntersectionObserver. LCP on 4G mobile 1.6 s, desktop 0.9 s. PageSpeed Insights mobile 94/100.
Lessons
- Dark mode isn't a feature, it's a signal. For this audience the palette was a load-bearing dramatic choice, not cosmetic.
- Motion is a double-edged sword. The lineup stagger works, but the original version had scroll-jacking on the hero - I scrapped it. Polish-market users scroll fast and a smooth-scroll hijack repels them.
- Vendor buttons have their own A/B test. "Buy ticket" vs "Get tickets" vs "Bilety" -
Bilety(Polish) converted 18 % better than the rest. Localization that includes CTA copy is not negotiable. - The promoter wanted a carousel; I shipped a grid. Carousels kill conversion on mobile (only ~30 % of users swipe past the first slide). A static grid with the entire lineup visible doubled CTR on artist cards.