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-zine

Where 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.