Innovations

Creator Tiles

Photo hero with a fade to paper, circular avatar overlap, then a 2-column bento where every tile is visual: live-now strip with pulsing red dot, full-width YouTube preview tile, podcast cover tile, featured product tile, gradient freebie email-grab card, plus plain link tiles for the rest.

Preview

Source

tsx
"use client";

import { useState } from "react";
import {
  Instagram,
  Twitter,
  Youtube,
  Mail,
  Globe,
  ShoppingBag,
  PlayCircle,
  Linkedin,
  Music2,
  ArrowUpRight,
  Play,
  Radio,
  Headphones,
  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 INK = "#0e0e12";
const PAPER = "#f6f3ed";
const ACCENT = "#5b3eff";
const ACCENT2 = "#ff5fa3";
const PILL = "#1c1c22";

function tileBase(extra = "") {
  return `relative overflow-hidden rounded-2xl border transition-all hover:-translate-y-0.5 ${extra}`;
}

function YouTubeTile({
  thumb,
  caption,
  href,
}: {
  thumb: string;
  caption: string;
  href: string;
}) {
  return (
    <a
      href={href}
      className={tileBase("col-span-2 bg-black text-white")}
      style={{ borderColor: "rgba(0,0,0,0)" }}
    >
      <img
        src={thumb}
        alt=""
        width={800}
        height={500}
        loading="lazy"
        className="aspect-[16/9] w-full object-cover transition-transform duration-500 hover:scale-[1.04]"
      />
      <div
        aria-hidden
        className="absolute inset-0"
        style={{
          background:
            "linear-gradient(180deg, rgba(0,0,0,0) 50%, rgba(0,0,0,0.85) 100%)",
        }}
      />
      <div className="absolute left-3 top-3 flex items-center gap-1.5 rounded-full bg-red-600 px-2 py-1 text-[10px] font-bold uppercase tracking-wider">
        <Play className="h-3 w-3 fill-white" /> YouTube
      </div>
      <div className="absolute inset-x-3 bottom-3">
        <p className="line-clamp-2 text-sm font-semibold leading-snug">{caption}</p>
      </div>
    </a>
  );
}

function PodcastTile({ block }: { block: Extract<RichBlock, { kind: "podcast-episode" }> }) {
  return (
    <a
      href={block.href}
      className={tileBase("bg-white p-3")}
      style={{ borderColor: "rgba(14,14,18,0.06)" }}
    >
      <div className="overflow-hidden rounded-xl">
        <img
          src={block.cover}
          alt=""
          width={400}
          height={400}
          loading="lazy"
          className="aspect-square w-full object-cover"
        />
      </div>
      <p className="mt-2 flex items-center gap-1 text-[10px] font-semibold uppercase tracking-[0.2em]" style={{ color: ACCENT }}>
        <Headphones className="h-3 w-3" /> Podcast · {block.duration}
      </p>
      <p
        className="mt-1 line-clamp-2 text-sm font-semibold leading-snug"
        style={{ color: INK }}
      >
        {block.title}
      </p>
    </a>
  );
}

function FeaturedProductTile({
  block,
}: {
  block: Extract<RichBlock, { kind: "featured-product" }>;
}) {
  return (
    <a
      href={block.href}
      className={tileBase("bg-white")}
      style={{ borderColor: "rgba(14,14,18,0.06)" }}
    >
      <div className="overflow-hidden">
        <img
          src={block.image}
          alt=""
          width={500}
          height={500}
          loading="lazy"
          className="aspect-[4/5] w-full object-cover transition-transform duration-500 hover:scale-[1.03]"
        />
      </div>
      <div className="p-3">
        <p className="text-[10px] font-semibold uppercase tracking-[0.2em]" style={{ color: ACCENT2 }}>
          Shop
        </p>
        <p
          className="mt-1 line-clamp-2 text-sm font-semibold leading-snug"
          style={{ color: INK }}
        >
          {block.title}
        </p>
        <p className="mt-1 text-sm font-bold" style={{ color: INK }}>
          {block.price}
        </p>
      </div>
    </a>
  );
}

function FreebieTile({ block }: { block: Extract<RichBlock, { kind: "freebie" }> }) {
  const [email, setEmail] = useState("");
  const [submitted, setSubmitted] = useState(false);
  return (
    <div
      className={tileBase("col-span-2 p-4")}
      style={{
        background: `linear-gradient(135deg, ${ACCENT} 0%, ${ACCENT2} 100%)`,
        borderColor: "rgba(0,0,0,0)",
        color: PAPER,
      }}
    >
      <p className="text-[10px] font-bold uppercase tracking-[0.25em]">Free</p>
      <p className="mt-1.5 text-base font-bold leading-snug">{block.title}</p>
      {submitted ? (
        <p className="mt-3 rounded-lg bg-white/15 px-3 py-2.5 text-sm">
          ✓ Sent — check your inbox.
        </p>
      ) : (
        <form
          onSubmit={(e) => {
            e.preventDefault();
            if (email) setSubmitted(true);
          }}
          className="mt-3 flex gap-2"
        >
          <input
            type="email"
            required
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder={block.placeholder ?? "Email"}
            className="min-w-0 flex-1 rounded-lg bg-white/15 px-3 py-2 text-sm text-white placeholder:text-white/60 focus:outline-none focus:ring-2 focus:ring-white/40"
          />
          <button
            type="submit"
            className="shrink-0 rounded-lg bg-white px-3 py-2 text-xs font-bold text-[color:var(--ink)]"
            style={{ ["--ink" as never]: INK }}
          >
            {block.cta ?? "Send"}
          </button>
        </form>
      )}
    </div>
  );
}

function LiveNowTile({ block }: { block: Extract<RichBlock, { kind: "live-now" }> }) {
  return (
    <a
      href={block.href}
      className={tileBase("col-span-2 flex items-center gap-3 px-4 py-4")}
      style={{ background: INK, color: PAPER, borderColor: "rgba(0,0,0,0)" }}
    >
      <span className="relative flex h-3 w-3 shrink-0">
        <span
          aria-hidden
          className="absolute inset-0 animate-ping rounded-full"
          style={{ background: "#ff3b3b", opacity: 0.6 }}
        />
        <span className="relative inline-block h-3 w-3 rounded-full" style={{ background: "#ff3b3b" }} />
      </span>
      <Radio className="h-4 w-4" strokeWidth={1.6} />
      <span className="flex-1 text-sm font-semibold">{block.label}</span>
      <span className="rounded-full bg-white/10 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider">
        {block.platform}
      </span>
      <ArrowUpRight className="h-4 w-4" />
    </a>
  );
}

function PlainLinkTile({ link }: { link: { label: string; href: string; icon?: LinkIcon; badge?: string } }) {
  const Icon = link.icon ? ICONS[link.icon] : ArrowUpRight;
  return (
    <a
      href={link.href}
      className={tileBase("flex items-start gap-2 bg-white p-4")}
      style={{ borderColor: "rgba(14,14,18,0.06)" }}
    >
      <span
        className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl"
        style={{ background: PAPER, color: INK }}
      >
        <Icon className="h-4 w-4" strokeWidth={1.6} />
      </span>
      <div className="min-w-0 flex-1">
        <p
          className="line-clamp-2 text-sm font-semibold leading-snug"
          style={{ color: INK }}
        >
          {link.label}
        </p>
        {link.badge && (
          <span
            className="mt-1.5 inline-block rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider"
            style={{ background: PILL, color: PAPER }}
          >
            {link.badge}
          </span>
        )}
      </div>
    </a>
  );
}

export interface LinksInBioCreatorTilesProps {
  data?: LinksInBioData;
}

export default function LinksInBioCreatorTiles({
  data = defaultData,
}: LinksInBioCreatorTilesProps) {
  const heroImage = data.heroImage ?? data.avatar;
  const podcast = data.richBlocks?.find(
    (b): b is Extract<RichBlock, { kind: "podcast-episode" }> => b.kind === "podcast-episode"
  );
  const product = data.richBlocks?.find(
    (b): b is Extract<RichBlock, { kind: "featured-product" }> => b.kind === "featured-product"
  );
  const freebie = data.richBlocks?.find(
    (b): b is Extract<RichBlock, { kind: "freebie" }> => b.kind === "freebie"
  );
  const live = data.richBlocks?.find(
    (b): b is Extract<RichBlock, { kind: "live-now" }> => b.kind === "live-now"
  );
  const youtubeLink =
    data.links.find((l) => l.icon === "youtube") ?? data.links[0];

  return (
    <>
      <link rel="preconnect" href="https://fonts.googleapis.com" />
      <link
        rel="stylesheet"
        href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
      />

      <section
        className="min-h-screen w-full pb-12"
        style={{ background: PAPER, color: INK, fontFamily: "'Inter', sans-serif" }}
      >
        {/* Hero */}
        <div className="relative h-60 w-full overflow-hidden">
          <img
            src={heroImage}
            alt=""
            width={1600}
            height={800}
            loading="eager"
            className="h-full w-full object-cover"
          />
          <div
            aria-hidden
            className="absolute inset-0"
            style={{
              background: `linear-gradient(180deg, rgba(14,14,18,0) 30%, ${PAPER} 100%)`,
            }}
          />
        </div>

        {/* Avatar overlap + identity */}
        <div className="relative mx-auto -mt-14 w-full max-w-[460px] px-4">
          <div className="flex flex-col items-center text-center">
            <img
              src={data.avatar}
              alt={data.name}
              width={120}
              height={120}
              loading="eager"
              className="h-28 w-28 rounded-full object-cover"
              style={{ border: `4px solid ${PAPER}`, boxShadow: "0 14px 32px rgba(14,14,18,0.18)" }}
            />
            <h1
              className="mt-3 text-[1.7rem] font-extrabold tracking-tight"
              style={{ color: INK }}
            >
              {data.name}
              {data.verified && (
                <span
                  className="ml-1.5 inline-block translate-y-[-2px] text-base"
                  style={{ color: ACCENT }}
                >

                </span>
              )}
            </h1>
            {data.handle && (
              <p className="text-sm" style={{ color: "rgba(14,14,18,0.55)" }}>
                {data.handle}
              </p>
            )}
            {data.bio && (
              <p
                className="mt-2 max-w-[360px] text-[14px] leading-relaxed"
                style={{ color: "rgba(14,14,18,0.7)" }}
              >
                {data.bio}
              </p>
            )}
          </div>

          {/* Bento grid */}
          <div className="mt-6 grid grid-cols-2 gap-3">
            {live && <LiveNowTile block={live} />}
            <YouTubeTile
              thumb="https://images.unsplash.com/photo-1492724441997-5dc865305da7?w=800&h=500&fit=crop&q=80"
              caption={youtubeLink?.label ?? "Latest YouTube video"}
              href={youtubeLink?.href ?? "#"}
            />
            {podcast && <PodcastTile block={podcast} />}
            {product && <FeaturedProductTile block={product} />}
            {freebie && <FreebieTile block={freebie} />}
            {data.links
              .filter((l) => l.icon !== "youtube")
              .slice(0, 4)
              .map((link) => (
                <PlainLinkTile key={link.label} link={link} />
              ))}
          </div>

          {/* Socials */}
          {data.socials && data.socials.length > 0 && (
            <div className="mt-7 flex items-center justify-center gap-3">
              {data.socials.map((s, i) => {
                const Icon = ICONS[s.type] ?? Globe;
                return (
                  <a
                    key={i}
                    href={s.href}
                    aria-label={s.type}
                    className="flex h-10 w-10 items-center justify-center rounded-full bg-white transition-transform hover:-translate-y-0.5"
                    style={{
                      border: "1px solid rgba(14,14,18,0.08)",
                      color: INK,
                    }}
                  >
                    <Icon className="h-4 w-4" strokeWidth={1.6} />
                  </a>
                );
              })}
            </div>
          )}

          <p
            className="mt-7 text-center text-[10px] uppercase tracking-[0.28em]"
            style={{ color: "rgba(14,14,18,0.45)" }}
          >
            Made with care
          </p>
        </div>
      </section>
    </>
  );
}
Claude Code Instructions

CLI Install

npx innovations add creator-tiles

Where to use it

A bento-style link-in-bio page where every tile feels distinct (no monolithic button stack). Theme-neutral palette (paper / ink with purple+pink accent gradient). Pass a typed 'data' prop (LinksInBioData from src/registry/links-in-bio/types.ts). Provide: - heroImage — wide image used as the top hero - richBlocks — opt into podcast-episode / featured-product / freebie / live-now tiles - A link with icon: 'youtube' is auto-promoted to the YouTube preview tile In Astro: import LinksInBioCreatorTiles from '../components/innovations/links-in-bio/creator-tiles'; <LinksInBioCreatorTiles client:load data={myProfile} /> In Next.js: import LinksInBioCreatorTiles from '@/components/innovations/links-in-bio/creator-tiles'; Best for: full-stack creators streaming multiple platforms (YouTube + podcast + Twitch + shop). Replace the placeholder YouTube thumb with your actual video thumbnail before going live.