Skip to content

Seasonal avatar borders

August 7, 2025 (1y ago)

385 views

I wanted my site to feel alive without adding another manual chore. This component puts a rainbow ring around my avatar during Pride Month in June and during Manchester Pride week, and it decides at runtime in the browser from the client's date.

Architecture: runtime, not build

Diagram 1 as text

10 steps, 3 of them decisions. Starts at “Page Load”.

Core logic (TypeScript, client-side)

"use client";

import { useEffect, useState } from "react";

export function PrideAvatar({ children, className = "" }: { children: React.ReactNode; className?: string; }) {
  const [isPrideTime, setIsPrideTime] = useState(false);

  useEffect(() => {
    const checkPrideTime = () => {
      const now = new Date();
      const month = now.getMonth() + 1; // 1..12
      const date = now.getDate();

      // Pride Month: entire June
      if (month === 6) return setIsPrideTime(true);

      // Manchester Pride: week running up to UK August bank holiday (last Monday)
      if (month === 8) {
        const year = now.getFullYear();
        const lastDay = new Date(year, 8, 0).getDate();   // 31
        const lastDow = new Date(year, 7, lastDay).getDay(); // 0=Sun..1=Mon..
        const lastMonday = lastDow === 1 ? lastDay : lastDay - ((lastDow + 6) % 7);
        const start = Math.max(1, lastMonday - 7);        // handle spill into July
        if (date >= start && date <= lastMonday) return setIsPrideTime(true);
      }

      setIsPrideTime(false);
    };

    checkPrideTime();
    const interval = setInterval(checkPrideTime, 24 * 60 * 60 * 1000); // daily
    return () => clearInterval(interval);
  }, []);

  if (!isPrideTime) return <div className={`ring-2 ring-black dark:ring-white rounded-full ${className}`}>{children}</div>;

  return (
    <div className={`relative ${className}`}>
      <div
        className="rounded-full"
        style={{ boxShadow: `
          0 0 0 3px rgb(239 68 68),   /* red   */
          0 0 0 6px rgb(249 115 22),  /* orange*/
          0 0 0 9px rgb(250 204 21),  /* yellow*/
          0 0 0 12px rgb(34 197 94),  /* green */
          0 0 0 15px rgb(59 130 246), /* blue  */
          0 0 0 18px rgb(147 51 234)  /* purple*/
        `}}
      >
        {children}
      </div>
    </div>
  );
}

Why this approach works

  • There's nothing coupled to a deploy. No build-time flag, no server toggle, and the ring appears the moment the calendar flips.
  • Six hard-edged box-shadow layers around a small avatar are cheap to paint on modern GPUs.
  • The dates are predictable. Manchester Pride is derived from the UK August bank holiday, the last Monday of August, with the ring on from the previous Tuesday through that Monday.

Gotchas and improvements

  • Timezones. It currently uses the user’s local time. For UK-accurate behaviour worldwide, compute the dates in Europe/London with Intl.DateTimeFormat or a small helper, then compare.
  • Midnight drift. setInterval(24h) isn’t aligned to midnight and will drift. You can schedule the next run at local midnight with setTimeout(nextMidnight - now) and re-arm each time.
  • Testing and preview. Add a query flag or env override to force a state for screenshots, such as ?pride=1 or NEXT_PUBLIC_FORCE_PRIDE=true.
  • Accessibility. There's already a fallback ring, and the avatar needs to stay distinguishable on both light and dark themes. If you animate the ring later, guard it with @media (prefers-reduced-motion).

Usage

import Image from "next/image";
import { PrideAvatar } from "@/components/pride-avatar";

export default function ProfilePic() {
  return (
    <PrideAvatar className="w-24 h-24 overflow-hidden">
      <Image src="/me.webp" alt="Sam Ainsworth" width={96} height={96} />
    </PrideAvatar>
  );
}

Demo

my face

Normal Border

Most of the year

my face

Pride Border 🏳️‍🌈

June & Manchester Pride week

Written by Sam Ainsworth.

my face
© Sam Ainsworth 2024 - 2026. All Rights Reserved.privacy