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”.
Page Load (a starting point)
- Go to Check Current Date
Check Current Date
- Go to Month == 6?
- Reached from Page Load
Decision: Month == 6?
- Go to Pride Month! 🏳️🌈 Rainbow Border, when Yes
- Go to Month == 8?, when No
- Reached from Check Current Date
Pride Month! 🏳️🌈 Rainbow Border
- Nothing leads out of this step.
- Reached from Month == 6?, when Yes
- Reached from Current Date in Pride Week?, when Yes
Decision: Month == 8?
- Go to Normal Border, when No
- Go to Calculate Bank Holiday, when Yes
- Reached from Month == 6?, when No
Normal Border
- Nothing leads out of this step.
- Reached from Month == 8?, when No
- Reached from Current Date in Pride Week?, when No
Calculate Bank Holiday
- Go to Find Last Monday of August
- Reached from Month == 8?, when Yes
Find Last Monday of August
- Go to Pride Week = 7 Days Before
- Reached from Calculate Bank Holiday
Pride Week = 7 Days Before
- Go to Current Date in Pride Week?
- Reached from Find Last Monday of August
Decision: Current Date in Pride Week?
- Go to Pride Month! 🏳️🌈 Rainbow Border, when Yes
- Go to Normal Border, when No
- Reached from Pride Week = 7 Days Before
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-shadowlayers 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/LondonwithIntl.DateTimeFormator 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 withsetTimeout(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=1orNEXT_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

Normal Border
Most of the year

Pride Border 🏳️🌈
June & Manchester Pride week
Related posts
- Automating my GitHub avatar sync, which keeps the avatar image itself up to date.
- AI coding tools: when they started earning their keep, on how I decide what to automate.