Skip to content
Studio portrait used to demonstrate the layoutOne fileNo dependenciesReduced-motion safe

IrisSectionsLabel Beacon

Make the claim felt, not just read.

A photo anchors it. A few floating labels point at what matters, live, instead of waiting in a caption underneath.

field

Label Beacon

Everywhere else in this set, the field itself supplies the energy a badge would. Here a photograph does that work instead — a few labels float along its edges, reading like a live readout rather than decoration, the way a product shot points at what it does mid-frame instead of underneath it in a caption. The labels carry the same three facts `split-beacon`'s glass ledger proves, just staged differently.

Built to prove the anatomy holds outside pure abstraction too: swap the field, keep the photo and its labels, and the composition still reads.

  • background
  • one-column
  • centered
  • portrait
Family
field
Status
Available
Licence
Free

Install

No installation needed — self-contained, paste-in code.

Usage

Drop it straight into a page.

example.tsx
import Image from "next/image";
import { GradientBackground } from "./GradientBackground";

const TAG =
  "absolute inline-flex items-center gap-2 rounded-full border border-white/20 bg-white/10 px-3 py-1.5 text-xs font-medium text-white backdrop-blur-md before:h-1.5 before:w-1.5 before:rounded-full before:bg-[#a78bfa]";

export default function Example() {
  return (
    <header className="relative isolate min-h-[40rem] overflow-hidden bg-black flex flex-col items-center justify-start pt-16 text-center [--portrait-w:min(24rem,60vw)]">
      <div className="absolute inset-0 -z-10">
        <GradientBackground />
      </div>

      <div className="absolute bottom-0 left-1/2 w-[var(--portrait-w)] -translate-x-1/2">
        {/* Swap for your own portrait or product shot. */}
        <Image
          src="/your-photo.png"
          alt=""
          width={628}
          height={713}
          className="block h-auto w-full [mask-image:radial-gradient(65%_60%_at_50%_40%,#000_40%,transparent_85%)]"
        />
        <span className={`${TAG} top-[4%] left-[2%]`}>One file</span>
        <span className={`${TAG} top-[46%] right-[4%]`}>No dependencies</span>
        <span className={`${TAG} bottom-[6%] left-[6%]`}>Reduced-motion safe</span>
      </div>

      <div className="mx-auto max-w-xl px-8 pb-[26rem] text-white">
        <h1 className="text-5xl font-medium tracking-tight">
          Make the claim{" "}
          <span className="rounded-lg bg-[#7c3aed] px-[0.42em] py-[0.05em]">
            felt
          </span>
          , not just{" "}
          <span className="rounded-lg border border-white/20 bg-white/10 px-[0.42em] py-[0.05em] backdrop-blur-sm">
            read
          </span>
          .
        </h1>
        <p className="mt-4 opacity-80">
          A photo anchors it. A few floating labels point at what matters,
          live, instead of waiting in a caption underneath.
        </p>
        <div className="mt-8">
          <a href="#" className="rounded-xl bg-[#7c3aed] px-5 py-3 text-white">
            See it work
          </a>
        </div>
      </div>
    </header>
  );
}

Component

The real source, exactly as it ships — multiple files, kept together.

import Link from 'next/link';
import Image from 'next/image';
import type { CategoryDef, CreationCard } from '@/lib/iris';
import { GradientBackground } from '../visuals/backgrounds';

const TAG =
    "absolute inline-flex items-center gap-[0.35rem] py-[0.4rem] px-3 rounded-full text-[length:0.75rem] font-medium text-[color:var(--field-ink)] bg-[var(--field-glass)] border border-[var(--field-glass-border)] [backdrop-filter:blur(10px)_saturate(1.2)] [-webkit-backdrop-filter:blur(10px)_saturate(1.2)] [text-shadow:0_1px_8px_oklch(0.1_0.02_240_/_0.5)] whitespace-nowrap before:content-[''] before:w-[0.4rem] before:h-[0.4rem] before:rounded-full before:bg-[color:var(--iris-accent-bright)] before:flex-none [@media(prefers-reduced-transparency:reduce)]:[backdrop-filter:none] [@media(prefers-reduced-transparency:reduce)]:[-webkit-backdrop-filter:none] [@media(prefers-reduced-transparency:reduce)]:bg-[var(--field-glass-opaque)]";

const WORD_BASE = "inline py-[0.05em] px-[0.42em] rounded-[0.5em] whitespace-nowrap";

/**
 * The real `label-beacon` section, full scale. Everywhere else in this set
 * the field itself supplies the energy a badge would; here a photograph
 * does that work instead, and a few floating labels ride its edges rather
 * than sitting underneath in a caption — the same three facts
 * `split-beacon`'s glass ledger proves, just carried as live-looking tags
 * next to a face instead of a panel of text.
 *
 * `GradientBackground`'s violet/blue/cyan/mint field is real, composed
 * underneath rather than a stand-in — one calm field to read the photo
 * against, not several bands competing with it — built to prove the
 * anatomy holds outside pure abstraction too: swap the field, keep the
 * photo and its labels.
 */
export function LabelBeacon({
    category,
    creation,
}: {
    category: CategoryDef;
    creation: CreationCard;
}) {
    return (
        <header className="iris-hero iris-hero--field iris-section-hero--label relative isolate [overflow:clip] mt-[calc(-3.5rem_-_1px)] min-h-[min(82vh,760px)] flex flex-col justify-start [padding-block:calc(clamp(3rem,8vh,6rem)+3.5rem)_clamp(3rem,8vh,6rem)] [--portrait-w:min(24rem,60vw)] [--portrait-h:calc(var(--portrait-w)*713/628)]">
            <div className="absolute inset-0 z-[-1] overflow-hidden bg-[color:var(--iris-bg)]" aria-hidden="true">
                <div className="absolute inset-0 motion-reduce:[animation:none] data-[enter=right]:[animation:iris-field-enter-right_560ms_cubic-bezier(0.16,1,0.3,1)_both] data-[enter=left]:[animation:iris-field-enter-left_560ms_cubic-bezier(0.16,1,0.3,1)_both]">
                    <GradientBackground />
                </div>
            </div>

            {/* Pinned to the hero's own bottom edge, out of the text's flow,
                so scaling it up never pushes the headline around — it just
                grows toward the top from feet planted at the hero's floor.
                Sits before `.iris-hero__inner` in the DOM so the text stacks
                above it by default if the two ever overlap. */}
            <div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-[var(--portrait-w,min(24rem,60vw))] z-0">
                <Image
                    src="/hero_char.png"
                    alt="Studio portrait used to demonstrate the layout"
                    width={628}
                    height={713}
                    sizes="(min-width: 48rem) 24rem, 60vw"
                    className="block w-full h-auto [mask-image:radial-gradient(65%_60%_at_50%_40%,#000_40%,transparent_85%)] [-webkit-mask-image:radial-gradient(65%_60%_at_50%_40%,#000_40%,transparent_85%)] [filter:drop-shadow(0_1.5rem_2.5rem_oklch(0.05_0.01_280_/_0.45))]"
                    priority
                />
                <span className={`${TAG} top-[4%] left-[2%]`}>
                    One file
                </span>
                <span className={`${TAG} top-[46%] right-[4%]`}>
                    No dependencies
                </span>
                <span className={`${TAG} bottom-[6%] left-[6%]`}>
                    Reduced-motion safe
                </span>
            </div>

            <div className="iris-shell flex flex-col items-center relative text-center pb-[calc(var(--portrait-h)+2.5rem)]">
                <p className="flex flex-wrap items-center gap-[0.4rem] text-[length:0.8125rem] text-[var(--field-ink-dim)] [text-shadow:0_1px_10px_oklch(0.12_0.02_264_/_0.55)] [&_a]:text-inherit [&_a:hover]:text-[color:var(--field-ink)]">
                    <Link href="/iris">Iris</Link>
                    <span aria-hidden="true">/</span>
                    <Link href={`/iris/${category.slug}`}>
                        {category.label}
                    </Link>
                    <span aria-hidden="true">/</span>
                    <span>{creation.title}</span>
                </p>
                <h1 className="font-[family-name:var(--font-iris-display)] font-normal text-[length:clamp(2.5rem,1.5rem+4.4vw,5rem)] leading-[1.02] tracking-[-0.02em] max-w-[18ch] text-balance text-[color:var(--field-ink)] [text-shadow:0_1px_12px_oklch(0.12_0.02_264_/_0.55)]">
                    Make the claim{' '}
                    <span className={`${WORD_BASE} bg-[color:var(--iris-accent)] text-[color:var(--iris-accent-ink)]`}>
                        felt
                    </span>
                    , not just{' '}
                    <span className={`${WORD_BASE} bg-[var(--field-glass)] text-[color:var(--field-ink)] border border-[var(--field-glass-border)] [backdrop-filter:blur(8px)] [-webkit-backdrop-filter:blur(8px)] [@media(prefers-reduced-transparency:reduce)]:[backdrop-filter:none] [@media(prefers-reduced-transparency:reduce)]:[-webkit-backdrop-filter:none] [@media(prefers-reduced-transparency:reduce)]:bg-[var(--field-glass-opaque)]`}>
                        read
                    </span>
                    .
                </h1>
                <p className="mt-4 max-w-[38ch] text-black [text-shadow:none]">
                    A photo anchors it. A few floating labels point at what
                    matters, live, instead of waiting in a caption underneath.
                </p>
                <div className="mt-9 flex flex-wrap justify-center gap-3">
                    <Link href={`/iris/${category.slug}/${creation.slug}#detail`} className="inline-flex items-center justify-center gap-[0.4rem] py-[0.8rem] px-5 rounded-[var(--iris-radius-sm)] border border-transparent text-[length:0.9375rem] font-medium leading-none whitespace-nowrap cursor-pointer transition-[background-color,border-color,color] duration-[120ms] ease-[ease] disabled:opacity-35 disabled:cursor-default disabled:pointer-events-none bg-[color:var(--iris-accent)] text-[color:var(--iris-accent-ink)] hover:bg-[color:var(--iris-accent-hover)]">
                        See it work
                    </Link>
                </div>
            </div>
        </header>
    );
}

More sections

Start with a background.

The backgrounds are the most finished corner of the catalogue. Take one and drop it under your own copy.

Browse backgrounds