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.

Next.jsTypeScriptTailwind CSSMotionVercel

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:

KeywordMonthly volumePosition at T+14
halloween rave gdańsk1,200#2
scooter koncert 20264,800#4
ergo arena halloween320#1
bilety halloween rave880#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:

PhotoOrigin (JPEG)AVIF servedSaving
Hero (1920×1080)5.8 MB290 kB95 %
Lineup card (600×600)480 kB32 kB93 %
Gallery thumb (400×400)220 kB18 kB92 %

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.