Retro Zine
Y2K / 90s collage — cream paper bg with halftone dots and scanlines, Polaroid-style avatar sticker, big chunky Bungee name with offset shadow, scrolling marquee bio, and tilted clashing-color buttons. Renders one latest-post block as a Polaroid.
Preview
Source
tsx
"use client";
import {
Instagram,
Twitter,
Youtube,
Mail,
Globe,
ShoppingBag,
PlayCircle,
Linkedin,
Music2,
Sparkles,
type LucideIcon,
} from "lucide-react";
import type { LinkIcon, LinksInBioData, RichBlock } from "../types";
import { defaultData } from "../defaultData";
const ICONS: Record<LinkIcon, LucideIcon> = {
instagram: Instagram,
twitter: Twitter,
tiktok: Music2,
youtube: Youtube,
spotify: Music2,
apple: Music2,
linkedin: Linkedin,
email: Mail,
globe: Globe,
shop: ShoppingBag,
play: PlayCircle,
};
const PAPER = "#fff5d6";
const INK = "#1c1d2b";
const HOT_PINK = "#ff3d8a";
const ELECTRIC = "#3a5cff";
const SLIME = "#9bff5b";
const TANGERINE = "#ff8a3d";
const BUTTON_PALETTE = [
{ bg: HOT_PINK, fg: "#fff" },
{ bg: ELECTRIC, fg: "#fff" },
{ bg: SLIME, fg: INK },
{ bg: TANGERINE, fg: INK },
{ bg: PAPER, fg: INK },
{ bg: INK, fg: PAPER },
];
function PolaroidLatestPost({
block,
}: {
block: Extract<RichBlock, { kind: "latest-post" }>;
}) {
return (
<a
href={block.href}
className="block w-full rotate-[-2deg] bg-white p-3 pb-12 shadow-[6px_8px_0_0_rgba(28,29,43,0.95)] transition-transform hover:rotate-[-1deg]"
style={{ maxWidth: "360px" }}
>
<div className="aspect-square overflow-hidden">
<img
src={block.image}
alt={block.title}
width={600}
height={600}
loading="lazy"
className="h-full w-full object-cover"
style={{ filter: "saturate(0.85) contrast(1.05)" }}
/>
</div>
<p
className="mt-3 text-center"
style={{
fontFamily: "'Caveat', cursive",
fontSize: "1.4rem",
color: INK,
lineHeight: 1.1,
}}
>
{block.title}
</p>
{block.meta && (
<p
className="mt-1 text-center text-[10px] uppercase tracking-[0.3em]"
style={{ color: INK, opacity: 0.6 }}
>
{block.meta}
</p>
)}
</a>
);
}
export interface LinksInBioRetroZineProps {
data?: LinksInBioData;
}
export default function LinksInBioRetroZine({
data = defaultData,
}: LinksInBioRetroZineProps) {
const latestPost = data.richBlocks?.find(
(b): b is Extract<RichBlock, { kind: "latest-post" }> => b.kind === "latest-post"
);
return (
<>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Caveat:[email protected]&family=Bungee&family=Space+Grotesk:[email protected]&display=swap"
/>
<style>{`
@keyframes retrozine-marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
`}</style>
<section
className="relative min-h-screen w-full overflow-hidden px-4 py-10"
style={{
background: PAPER,
color: INK,
fontFamily: "'Space Grotesk', sans-serif",
}}
>
{/* Halftone dot texture */}
<div
aria-hidden
className="pointer-events-none absolute inset-0"
style={{
backgroundImage: `radial-gradient(${INK}25 1px, transparent 1px)`,
backgroundSize: "10px 10px",
opacity: 0.35,
}}
/>
{/* Scanlines */}
<div
aria-hidden
className="pointer-events-none absolute inset-0"
style={{
background:
"repeating-linear-gradient(0deg, rgba(28,29,43,0.05) 0, rgba(28,29,43,0.05) 1px, transparent 1px, transparent 4px)",
}}
/>
{/* Floating sticker accents */}
<div
aria-hidden
className="pointer-events-none absolute -top-4 right-4 hidden rotate-[15deg] sm:block"
style={{ color: HOT_PINK }}
>
<Sparkles className="h-12 w-12" />
</div>
<div
aria-hidden
className="pointer-events-none absolute bottom-10 left-4 hidden -rotate-12 text-5xl sm:block"
style={{ fontFamily: "'Bungee', sans-serif", color: ELECTRIC, opacity: 0.6 }}
>
★
</div>
<div className="relative mx-auto w-full max-w-[480px]">
{/* Avatar sticker */}
<div className="flex justify-center">
<div
className="rotate-[-4deg] bg-white p-3 pb-8 shadow-[6px_8px_0_0_rgba(28,29,43,0.95)]"
style={{ width: "180px" }}
>
<img
src={data.avatar}
alt={data.name}
width={300}
height={300}
loading="eager"
className="h-[150px] w-full object-cover"
style={{ filter: "saturate(0.9) contrast(1.05)" }}
/>
<p
className="mt-2 text-center"
style={{
fontFamily: "'Caveat', cursive",
fontSize: "1.2rem",
color: INK,
lineHeight: 1,
}}
>
{data.name.split(" ")[0]}!
</p>
</div>
</div>
{/* Big chunky name */}
<h1
className="mt-6 text-center"
style={{
fontFamily: "'Bungee', sans-serif",
fontSize: "clamp(2.4rem, 9vw, 3.4rem)",
lineHeight: 1,
color: INK,
textShadow: `4px 4px 0 ${HOT_PINK}`,
}}
>
{data.name}
</h1>
{data.handle && (
<p
className="mt-2 text-center text-base"
style={{
fontFamily: "'Caveat', cursive",
color: ELECTRIC,
}}
>
{data.handle} · est. now
</p>
)}
{/* Marquee bio */}
{data.bio && (
<div
className="mt-6 overflow-hidden border-y-4 py-2"
style={{ borderColor: INK, background: SLIME }}
>
<div
className="flex whitespace-nowrap"
style={{
animation: "retrozine-marquee 28s linear infinite",
willChange: "transform",
}}
>
{Array.from({ length: 2 }).map((_, k) => (
<span
key={k}
className="mx-6 inline-flex items-center gap-3 text-sm font-semibold uppercase tracking-widest"
>
{Array.from({ length: 4 }).map((__, j) => (
<span key={j} className="inline-flex items-center gap-3">
<span>{data.bio}</span>
<span style={{ color: HOT_PINK }}>✦</span>
</span>
))}
</span>
))}
</div>
</div>
)}
{/* Buttons */}
<div className="mt-7 space-y-3">
{data.links.map((link, i) => {
const Icon = link.icon ? ICONS[link.icon] : null;
const palette = BUTTON_PALETTE[i % BUTTON_PALETTE.length];
const tilt = i % 2 === 0 ? "rotate-[-1deg]" : "rotate-[1deg]";
return (
<a
key={link.label}
href={link.href}
className={`block w-full ${tilt} border-[3px] px-4 py-3.5 text-center font-semibold transition-transform hover:translate-y-[-2px] hover:rotate-0`}
style={{
borderColor: INK,
background: palette.bg,
color: palette.fg,
boxShadow: `5px 5px 0 0 ${INK}`,
fontFamily: "'Space Grotesk', sans-serif",
}}
>
<span className="inline-flex items-center justify-center gap-2.5">
{Icon && <Icon className="h-4 w-4" strokeWidth={2.4} />}
<span className="text-[15px]">{link.label}</span>
{link.badge && (
<span
className="ml-1 rounded-sm px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider"
style={{
background: palette.fg,
color: palette.bg,
}}
>
{link.badge}
</span>
)}
</span>
</a>
);
})}
</div>
{/* Polaroid latest post */}
{latestPost && (
<div className="mt-10 flex justify-center">
<PolaroidLatestPost block={latestPost} />
</div>
)}
{/* Socials sticker bar */}
{data.socials && data.socials.length > 0 && (
<div
className="mt-10 flex flex-wrap items-center justify-center gap-3 rotate-[1deg] border-[3px] p-3"
style={{
borderColor: INK,
background: PAPER,
boxShadow: `5px 5px 0 0 ${INK}`,
}}
>
<span className="text-[10px] font-bold uppercase tracking-[0.22em]">
Find me ↓
</span>
{data.socials.map((s, i) => {
const Icon = ICONS[s.type] ?? Globe;
const c = BUTTON_PALETTE[(i + 1) % BUTTON_PALETTE.length];
return (
<a
key={i}
href={s.href}
aria-label={s.type}
className="flex h-8 w-8 items-center justify-center border-2 transition-transform hover:rotate-12"
style={{ borderColor: INK, background: c.bg, color: c.fg }}
>
<Icon className="h-4 w-4" strokeWidth={2.4} />
</a>
);
})}
</div>
)}
<p
className="mt-10 text-center text-[11px] uppercase tracking-[0.32em]"
style={{ color: INK, opacity: 0.6 }}
>
Made hot · {new Date().getFullYear()}
</p>
</div>
</section>
</>
);
} Claude Code Instructions
CLI Install
npx innovations add retro-zineWhere to use it
A loud, playful Y2K-zine link-in-bio page. Hardcoded paper/ink palette with hot pink, electric blue, slime green, and tangerine accents. Loads Caveat, Bungee, and Space Grotesk from Google Fonts.
Pass a typed 'data' prop (LinksInBioData from src/registry/links-in-bio/types.ts) to swap content. With no prop it renders sample data.
Add a richBlocks entry with kind 'latest-post' to render a Polaroid-style featured post below the link buttons.
In Astro:
import LinksInBioRetroZine from '../components/innovations/links-in-bio/retro-zine';
<LinksInBioRetroZine client:load data={myProfile} />
In Next.js:
import LinksInBioRetroZine from '@/components/innovations/links-in-bio/retro-zine';
Best for: indie creators, musicians, illustrators, fashion brands targeting Gen Z. Includes a continuous bio-text marquee animation; for prefers-reduced-motion friendliness, wrap the marquee in your own media query.