Zum Inhalt springen
UI Kit
Accent

Link in Bio

Eine Seite für alles, worauf eine Person verlinkt. Links, Produkte, Termine, Newsletter und Teilen, als Spalte, Bento, Tabs, Countdown oder geteilt mit Porträt.

Eine von 3 Components in Personal Brand

5 Variants

Variants

Details

Was Link in Bio mitbringt, aus Registry und Quellcode gelesen.

Bereich
Industries
Variants
5
Version
1.0.0 · seit 1.1.0
Datei
components/ui/link-in-bio.tsx
Bringt mit
utilskitbuttoniconmotionsection
npm-Pakete
Keine
Sprachen
Englisch und Deutsch, 26 eigene Texte
In Sammlungen
Creator-Seite

Props

Die Schnittstelle LinkInBioProps aus components/ui/link-in-bio.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • variantoptional
    LinkInBioVariant

    stacktilestabsdatessplit

  • profilePflicht
    BioProfile
  • linksoptional
    BioLink[]
  • socialsoptional
    BioSocial[]
  • productsoptional
    BioProduct[]
  • eventsoptional
    BioEvent[]
  • groupsoptional
    BioGroup[]

    split: links in groups

  • videooptional
    { title: string; length?: string; href: string; media?: React.ReactNode }

    tiles: the video tile

  • newsletteroptional
    { title: string; text?: string; count?: string }

    tiles: the newsletter tile

  • shareUrloptional
    string

    The address of this page, copied by the share button

  • currencyoptional
    string
  • onSubscribeoptional
    (email: string) => void

Eingebaute Texte

Diese Texte schreibt Link in Bio selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.

Alle 26 Texte anzeigen
shareEnglisch Copy link to this pageDeutsch Link zu dieser Seite kopieren
copiedEnglisch Link copiedDeutsch Link kopiert
linksEnglisch LinksDeutsch Links
shopEnglisch ShopDeutsch Shop
datesEnglisch DatesDeutsch Termine
sectionsEnglisch SectionsDeutsch Bereiche
nextEnglisch Next dateDeutsch Nächster Termin
inEnglisch Starts inDeutsch Beginnt in
daysEnglisch daysDeutsch Tage
hoursEnglisch hrsDeutsch Std.
minutesEnglisch minDeutsch Min.
secondsEnglisch secDeutsch Sek.
remindEnglisch Remind meDeutsch Erinnere mich
remindedEnglisch Reminder setDeutsch Erinnerung gesetzt
seatsEnglisch {n} seats leftDeutsch Noch {n} Plätze
joinEnglisch Save my seatDeutsch Platz sichern
getEnglisch Get itDeutsch Holen
watchEnglisch Watch nowDeutsch Jetzt ansehen
emailEnglisch Email addressDeutsch E-Mail-Adresse
emailPhEnglisch you@example.comDeutsch du@beispiel.de
subscribeEnglisch SubscribeDeutsch Abonnieren
subscribedEnglisch You are in. The next letter comes on Sunday.Deutsch Du bist dabei. Der nächste Brief kommt am Sonntag.
invalidEnglisch Please enter an email address like name@example.com.Deutsch Bitte gib eine E-Mail-Adresse wie name@beispiel.de ein.
socialsEnglisch ElsewhereDeutsch Woanders
laterEnglisch Later datesDeutsch Weitere Termine
liveEnglisch Happening nowDeutsch Läuft gerade

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • Link in Bio reagiert selbst auf die Tasten Pfeil links und Pfeil rechts.
  • Setzt aria-controls, aria-describedby, aria-invalid, aria-label, aria-labelledby, aria-live, aria-pressed und aria-selected, damit Screenreader Zweck und Zustand ansagen.
  • Nutzt die Rollen status, tab, tablist, tabpanel und timer.
  • Meldet Änderungen über eine Live-Region, ohne den Fokus zu verschieben.
  • Jedes Feld hat ein Label, sichtbar oder für Screenreader.
  • Ergänzt Text für Screenreader, wo Icons und Grafiken allein stehen.
  • Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
  • Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.

Bewegung

  • Erscheint beim ersten Sichtkontakt mit einem kurzen Auftritt (kit-rise, useInView).
  • Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
  • Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 9 Stellen im Code schalten Bewegung dann ab.

Quellcode

Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.

Datei anzeigen, 885 Zeilen
components/ui/link-in-bio.tsx
'use client';

import { useEffect, useRef, useState } from 'react';
import { cn, round, useInView, useLocale, useSpotlight, useTexts, type Locale } from '@/lib/kit';
import { Button } from '@/components/ui/button';
import { Icon, type IconName } from '@/components/ui/icon';
import { Parallax, Stagger, Magnetic } from '@/components/ui/motion';
import { Section } from '@/components/ui/section';

/*
 * One page that holds everything a person links to: the latest work, the
 * products, the next dates, a way to subscribe. Built for creators,
 * coaches, artists and anyone with one link in a profile.
 *
 *   stack  the classic column, a featured link with a picture, the rest as rows, share with a copied check
 *   tiles  a bento of tiles, video, newsletter with its own form, products and the next date
 *   tabs   links, shop and dates behind one switch with a gliding marker
 *   dates  the next date with a live countdown, the others as a list with a reminder each
 *   split  a large portrait that stays while the grouped links scroll beside it
 */

export type LinkInBioVariant = 'stack' | 'tiles' | 'tabs' | 'dates' | 'split';

export interface BioProfile {
  name: string;
  /** e.g. @mira.draws or a short role */
  handle?: string;
  bio?: string;
  location?: string;
  avatar?: React.ReactNode;
  /** A wide picture behind the head, used by split and tiles */
  cover?: React.ReactNode;
  /** A short status line with a live dot, e.g. "Booking for March" */
  status?: string;
}

export interface BioLink {
  id: string;
  label: string;
  description?: string;
  href: string;
  icon?: IconName;
  /** A short marker like "New" */
  badge?: string;
  /** A picture for the featured link (the first one in stack) */
  image?: React.ReactNode;
}

export interface BioSocial {
  label: string;
  icon: IconName;
  href: string;
}

export interface BioProduct {
  id: string;
  title: string;
  /** e.g. "Preset pack" or "Ebook, 64 pages" */
  kind?: string;
  price: number;
  /** A struck through price before */
  was?: number;
  href: string;
  image?: React.ReactNode;
}

export interface BioEvent {
  id: string;
  title: string;
  /** ISO date and time, e.g. 2026-11-14T19:00 (local time of the event) */
  start: string;
  place?: string;
  /** e.g. "Live session" or "Workshop" */
  kind?: string;
  href: string;
  /** Seats left, shown when low */
  seats?: number;
}

export interface BioGroup {
  title: string;
  links: BioLink[];
}

export interface LinkInBioProps {
  variant?: LinkInBioVariant;
  profile: BioProfile;
  links?: BioLink[];
  socials?: BioSocial[];
  products?: BioProduct[];
  events?: BioEvent[];
  /** split: links in groups */
  groups?: BioGroup[];
  /** tiles: the video tile */
  video?: { title: string; length?: string; href: string; media?: React.ReactNode };
  /** tiles: the newsletter tile */
  newsletter?: { title: string; text?: string; count?: string };
  /** The address of this page, copied by the share button */
  shareUrl?: string;
  currency?: string;
  onSubscribe?: (email: string) => void;
  labels?: Partial<Texts>;
  className?: string;
}

const TEXTS = {
  en: {
    share: 'Copy link to this page',
    copied: 'Link copied',
    links: 'Links',
    shop: 'Shop',
    dates: 'Dates',
    sections: 'Sections',
    next: 'Next date',
    in: 'Starts in',
    days: 'days',
    hours: 'hrs',
    minutes: 'min',
    seconds: 'sec',
    remind: 'Remind me',
    reminded: 'Reminder set',
    seats: '{n} seats left',
    join: 'Save my seat',
    get: 'Get it',
    watch: 'Watch now',
    email: 'Email address',
    emailPh: 'you@example.com',
    subscribe: 'Subscribe',
    subscribed: 'You are in. The next letter comes on Sunday.',
    invalid: 'Please enter an email address like name@example.com.',
    socials: 'Elsewhere',
    later: 'Later dates',
    live: 'Happening now',
  },
  de: {
    share: 'Link zu dieser Seite kopieren',
    copied: 'Link kopiert',
    links: 'Links',
    shop: 'Shop',
    dates: 'Termine',
    sections: 'Bereiche',
    next: 'Nächster Termin',
    in: 'Beginnt in',
    days: 'Tage',
    hours: 'Std.',
    minutes: 'Min.',
    seconds: 'Sek.',
    remind: 'Erinnere mich',
    reminded: 'Erinnerung gesetzt',
    seats: 'Noch {n} Plätze',
    join: 'Platz sichern',
    get: 'Holen',
    watch: 'Jetzt ansehen',
    email: 'E-Mail-Adresse',
    emailPh: 'du@beispiel.de',
    subscribe: 'Abonnieren',
    subscribed: 'Du bist dabei. Der nächste Brief kommt am Sonntag.',
    invalid: 'Bitte gib eine E-Mail-Adresse wie name@beispiel.de ein.',
    socials: 'Woanders',
    later: 'Weitere Termine',
    live: 'Läuft gerade',
  },
};
type Texts = typeof TEXTS.en;

/* ------------------------------------------------------------------ */
/* Helpers                                                             */
/* ------------------------------------------------------------------ */

const TAG: Record<Locale, string> = { en: 'en-US', de: 'de-DE' };
const fill = (s: string, v: Record<string, string | number>) => s.replace(/\{(\w+)\}/g, (_, k) => String(v[k] ?? ''));
const focusRing = 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background';
const fit = '[&>*]:size-full [&_img]:object-cover';

function money(value: number, currency: string, locale: Locale) {
  const whole = Number.isInteger(value);
  return new Intl.NumberFormat(TAG[locale], { style: 'currency', currency, minimumFractionDigits: whole ? 0 : 2, maximumFractionDigits: whole ? 0 : 2 }).format(value);
}
/** Dates are read as given, without the time zone of the visitor, so server and browser agree */
function parts(iso: string) {
  const [d, t = '00:00'] = iso.split('T');
  const [y, m, day] = d.split('-').map(Number);
  const [h, min] = t.split(':').map(Number);
  return new Date(Date.UTC(y, m - 1, day, h, min));
}
const fmt = (iso: string, locale: Locale, o: Intl.DateTimeFormatOptions) => new Intl.DateTimeFormat(TAG[locale], { timeZone: 'UTC', ...o }).format(parts(iso));

const MOTION_CSS = `
@keyframes kitbio-in{from{opacity:0;transform:translateY(8px)}}
@keyframes kitbio-swap{from{opacity:0;filter:blur(4px);transform:translateY(6px)}}
@keyframes kitbio-pop{0%{transform:scale(.4);opacity:0}60%{transform:scale(1.15);opacity:1}100%{transform:scale(1)}}
@keyframes kitbio-ring{to{transform:rotate(360deg)}}
@keyframes kitbio-tick{from{transform:translateY(-30%);opacity:.35}}
@media (prefers-reduced-motion:reduce){[class*="kitbio-"],.kitbio-anim{animation:none!important}}`;

function BioMotion() {
  return (
    <style href="kitbio-motion" precedence="default">
      {MOTION_CSS}
    </style>
  );
}

/** Copies the page address and shows a check for a moment */
function ShareButton({ url, t, className }: { url?: string; t: Texts; className?: string }) {
  const [done, setDone] = useState(false);
  const timer = useRef(0);
  useEffect(() => () => window.clearTimeout(timer.current), []);
  const copy = async () => {
    const href = url ?? (typeof window !== 'undefined' ? window.location.href : '');
    try {
      await navigator.clipboard?.writeText(href);
    } catch {
      /* the check still confirms the press, the address is shown in the tooltip */
    }
    setDone(true);
    window.clearTimeout(timer.current);
    timer.current = window.setTimeout(() => setDone(false), 1800);
  };
  return (
    <button type="button" onClick={copy} aria-label={done ? t.copied : t.share} title={url} className={cn('relative inline-flex size-11 shrink-0 items-center justify-center rounded-full border border-border bg-card/80 text-foreground shadow-[var(--highlight),var(--shadow-sm)] backdrop-blur transition-[transform,background-color] duration-300 ease-[var(--ease-kit)] hover:bg-accent active:scale-95', focusRing, className)}>
      <span className={cn('absolute inset-0 flex items-center justify-center transition-[opacity,transform] duration-300', done ? 'scale-50 opacity-0' : 'opacity-100')}>
        <Icon name="share" size={18} />
      </span>
      <span className={cn('absolute inset-0 flex items-center justify-center text-success transition-[opacity,transform] duration-300', done ? 'opacity-100' : 'scale-50 opacity-0')}>
        <Icon name="check" size={18} />
      </span>
      <span className="sr-only" aria-live="polite">
        {done ? t.copied : ''}
      </span>
    </button>
  );
}

/** Portrait in a ring that turns slowly once, with an optional live status */
function Head({ profile, size = 'md', align = 'center' }: { profile: BioProfile; size?: 'md' | 'lg'; align?: 'center' | 'left' }) {
  const center = align === 'center';
  return (
    <div className={cn('flex flex-col', center ? 'items-center text-center' : 'items-start text-left')}>
      <div className="relative">
        <span aria-hidden className="kitbio-anim absolute -inset-[3px] rounded-full bg-[conic-gradient(from_0deg,var(--primary),color-mix(in_oklab,var(--primary)_20%,transparent),var(--primary))] opacity-80 [animation:kitbio-ring_14s_linear_infinite]" />
        <div data-clip-ok className={cn('relative overflow-hidden rounded-full bg-muted ring-[3px] ring-background', size === 'lg' ? 'size-24' : 'size-[84px]', fit)}>{profile.avatar}</div>
      </div>
      <h3 className={cn('mt-4 font-semibold tracking-[-0.02em] text-foreground', size === 'lg' ? 'text-[26px]' : 'text-[21px]')}>{profile.name}</h3>
      {profile.handle && <p className="mt-0.5 text-[14px] text-muted-foreground">{profile.handle}</p>}
      {profile.bio && <p className={cn('mt-3 max-w-[30rem] text-pretty text-[15px] leading-relaxed text-foreground/85', center && 'text-balance')}>{profile.bio}</p>}
      {(profile.status || profile.location) && (
        <div className={cn('mt-4 flex flex-wrap items-center gap-2', center && 'justify-center')}>
          {profile.status && (
            <span className="inline-flex min-h-8 items-center gap-2 rounded-full bg-[color-mix(in_oklab,var(--success)_14%,transparent)] px-3 text-[13px] font-medium text-success-foreground">
              <span className="relative flex size-2">
                <span aria-hidden className="absolute inset-0 animate-ping rounded-full bg-success opacity-60 motion-reduce:animate-none" />
                <span className="relative size-2 rounded-full bg-success" />
              </span>
              {profile.status}
            </span>
          )}
          {profile.location && (
            <span className="inline-flex min-h-8 items-center gap-1.5 rounded-full border border-border px-3 text-[13px] text-muted-foreground">
              <Icon name="map-pin" size={14} />
              {profile.location}
            </span>
          )}
        </div>
      )}
    </div>
  );
}

function Socials({ socials = [], t, align = 'center' }: { socials?: BioSocial[]; t: Texts; align?: 'center' | 'left' }) {
  if (!socials.length) return null;
  return (
    <ul aria-label={t.socials} className={cn('flex flex-wrap gap-2', align === 'center' && 'justify-center')}>
      {socials.map((s) => (
        <li key={s.label}>
          <a href={s.href} aria-label={s.label} title={s.label} className={cn('inline-flex size-11 items-center justify-center rounded-full border border-border text-muted-foreground transition-[color,background-color,transform] duration-300 ease-[var(--ease-kit)] hover:-translate-y-0.5 hover:bg-accent hover:text-foreground', focusRing)}>
            <Icon name={s.icon} size={18} />
          </a>
        </li>
      ))}
    </ul>
  );
}

/** One link as a full width row. Arrow slides, light follows the mouse. */
function LinkRow({ link, className }: { link: BioLink; className?: string }) {
  const spot = useSpotlight<HTMLAnchorElement>();
  return (
    <a href={link.href} onPointerMove={spot} className={cn('kit-spotlight group relative flex min-h-[60px] items-center gap-3.5 rounded-xl border border-border bg-card px-4 py-3 shadow-[var(--highlight),var(--shadow-sm)] transition-[transform,box-shadow,border-color] duration-300 ease-[var(--ease-kit)] hover:-translate-y-0.5 hover:border-border-strong hover:shadow-[var(--highlight),var(--shadow-md)]', focusRing, className)}>
      {link.icon && (
        <span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground transition-colors group-hover:bg-[color-mix(in_oklab,var(--primary)_14%,var(--muted))] group-hover:text-primary">
          <Icon name={link.icon} size={18} />
        </span>
      )}
      <span className="min-w-0 flex-1">
        <span className="flex items-start gap-2">
          <span className="line-clamp-2 text-[15px] font-medium leading-snug text-foreground">{link.label}</span>
          {link.badge && <span className="mt-px shrink-0 rounded-full bg-primary px-2 py-0.5 text-[11px] font-semibold text-primary-foreground">{link.badge}</span>}
        </span>
        {link.description && <span className="mt-0.5 block truncate text-[13px] text-muted-foreground">{link.description}</span>}
      </span>
      <Icon name="arrow-up-right" size={17} className="text-muted-foreground transition-transform duration-300 ease-[var(--ease-kit)] group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-foreground" />
    </a>
  );
}

/** The featured link, a picture that zooms a little on hover */
function FeaturedLink({ link }: { link: BioLink }) {
  return (
    <a href={link.href} className={cn('group block overflow-hidden rounded-xl border border-border bg-card shadow-[var(--highlight),var(--shadow-sm)] transition-[transform,box-shadow] duration-300 ease-[var(--ease-kit)] hover:-translate-y-0.5 hover:shadow-[var(--highlight),var(--shadow-md)]', focusRing)}>
      {link.image && (
        <div data-clip-ok className="relative aspect-[16/8] overflow-hidden bg-muted">
          <div className={cn('absolute inset-0 transition-transform duration-700 ease-[var(--ease-kit)] group-hover:scale-[1.04] motion-reduce:transition-none', fit)}>{link.image}</div>
          {link.badge && <span className="absolute left-3 top-3 rounded-full bg-background/85 px-2.5 py-1 text-[12px] font-semibold text-foreground backdrop-blur">{link.badge}</span>}
        </div>
      )}
      <div className="flex items-center gap-3 px-4 py-3.5">
        <span className="min-w-0 flex-1">
          <span className="block text-[15.5px] font-semibold text-foreground">{link.label}</span>
          {link.description && <span className="mt-0.5 block text-[13px] text-muted-foreground">{link.description}</span>}
        </span>
        <span className="flex size-9 items-center justify-center rounded-full bg-primary text-primary-foreground transition-transform duration-300 ease-[var(--ease-kit)] group-hover:translate-x-0.5">
          <Icon name="arrow-right" size={16} />
        </span>
      </div>
    </a>
  );
}

/** Live countdown, computed only in the browser. The server shows the date. */
function useCountdown(iso?: string) {
  const [left, setLeft] = useState<number | null>(null);
  useEffect(() => {
    if (!iso) return;
    // The event time is read as local time of the visitor
    const [d, tm = '00:00'] = iso.split('T');
    const [y, m, day] = d.split('-').map(Number);
    const [h, min] = tm.split(':').map(Number);
    const target = new Date(y, m - 1, day, h, min).getTime();
    const tick = () => setLeft(Math.max(0, target - Date.now()));
    tick();
    const id = window.setInterval(tick, 1000);
    return () => window.clearInterval(id);
  }, [iso]);
  return left;
}

function Countdown({ iso, t }: { iso: string; t: Texts }) {
  const left = useCountdown(iso);
  const locale = useLocale();
  const units = left === null ? null : [Math.floor(left / 864e5), Math.floor(left / 36e5) % 24, Math.floor(left / 6e4) % 60, Math.floor(left / 1e3) % 60];
  const names = [t.days, t.hours, t.minutes, t.seconds];
  if (left === 0)
    return (
      <p role="status" className="flex min-h-[76px] items-center justify-center gap-2 rounded-xl border border-border bg-card text-[15px] font-semibold text-foreground">
        <span className="size-2 rounded-full bg-destructive" />
        {t.live}
      </p>
    );
  return (
    <div className="grid grid-cols-4 gap-px overflow-hidden rounded-xl border border-border bg-border" role="timer" aria-label={`${t.in} ${fmt(iso, locale, { dateStyle: 'long' })}`}>
      {names.map((n, i) => (
        <div key={n} className="bg-card px-2 py-3 text-center">
          <span className="block overflow-hidden text-[26px] font-semibold leading-none tracking-[-0.02em] tabular-nums text-foreground sm:text-[30px]">
            {units ? (
              <span key={units[i]} className={cn('inline-block', i === 3 && '[animation:kitbio-tick_.35s_var(--ease-kit)]')}>
                {String(units[i]).padStart(2, '0')}
              </span>
            ) : (
              <span className="text-muted-foreground">··</span>
            )}
          </span>
          <span className="mt-1.5 block text-[12px] text-muted-foreground">{n}</span>
        </div>
      ))}
    </div>
  );
}

function DateBadge({ iso, className }: { iso: string; className?: string }) {
  const locale = useLocale();
  return (
    <span className={cn('flex w-14 shrink-0 flex-col items-center justify-center rounded-lg border border-border bg-surface py-1.5 leading-none', className)}>
      <span className="text-[11px] font-semibold uppercase tracking-[0.06em] text-primary">{fmt(iso, locale, { month: 'short' }).replace('.', '')}</span>
      <span className="mt-1 text-[20px] font-semibold tabular-nums text-foreground">{fmt(iso, locale, { day: 'numeric' }).replace('.', '')}</span>
    </span>
  );
}

function RemindButton({ t, title }: { t: Texts; title: string }) {
  const [on, setOn] = useState(false);
  return (
    <button type="button" aria-pressed={on} aria-label={`${on ? t.reminded : t.remind}, ${title}`} onClick={() => setOn((v) => !v)} className={cn('inline-flex min-h-11 min-w-11 shrink-0 items-center justify-center gap-1.5 rounded-lg border px-3 text-[13px] font-medium transition-[background-color,border-color,color] duration-300', focusRing, on ? 'border-transparent bg-[color-mix(in_oklab,var(--primary)_14%,transparent)] text-foreground' : 'border-border text-muted-foreground hover:bg-accent hover:text-foreground')}>
      <span key={String(on)} className={cn('inline-flex', on && '[animation:kitbio-pop_.45s_var(--ease-kit)]')}>
        <Icon name={on ? 'check' : 'bell'} size={15} className={on ? 'text-primary' : undefined} />
      </span>
      <span className="max-sm:sr-only">{on ? t.reminded : t.remind}</span>
    </button>
  );
}

function EventRow({ e, t }: { e: BioEvent; t: Texts }) {
  const locale = useLocale();
  return (
    <li className="flex items-center gap-3.5 py-3.5">
      <DateBadge iso={e.start} />
      <div className="min-w-0 flex-1">
        <a href={e.href} className={cn('-my-3 block rounded-sm py-3 text-[15px] font-medium text-foreground hover:underline hover:underline-offset-4', focusRing)}>
          {e.title}
        </a>
        <p className="mt-0.5 text-pretty text-[13px] text-muted-foreground">
          {[e.kind, fmt(e.start, locale, { weekday: 'short', hour: 'numeric', minute: '2-digit' }), e.place].filter(Boolean).join(' · ')}
        </p>
        {e.seats !== undefined && e.seats <= 10 && <p className="mt-1 text-[12.5px] font-medium text-warning-foreground">{fill(t.seats, { n: e.seats })}</p>}
      </div>
      <RemindButton t={t} title={e.title} />
    </li>
  );
}

function ProductCard({ p, currency, t, className }: { p: BioProduct; currency: string; t: Texts; className?: string }) {
  const locale = useLocale();
  return (
    <a href={p.href} className={cn('group flex flex-col overflow-hidden rounded-xl border border-border bg-card shadow-[var(--highlight),var(--shadow-sm)] transition-[transform,box-shadow] duration-300 ease-[var(--ease-kit)] hover:-translate-y-1 hover:shadow-[var(--highlight),var(--shadow-md)]', focusRing, className)}>
      <div data-clip-ok className="relative aspect-square overflow-hidden bg-muted">
        <div className={cn('absolute inset-0 transition-transform duration-700 ease-[var(--ease-kit)] group-hover:scale-105 motion-reduce:transition-none', fit)}>{p.image}</div>
      </div>
      <div className="flex flex-1 flex-col p-3.5">
        {p.kind && <span className="text-[12px] text-muted-foreground">{p.kind}</span>}
        <span className="mt-0.5 line-clamp-2 text-[14.5px] font-medium leading-snug text-foreground">{p.title}</span>
        <span className="mt-auto flex items-baseline gap-2 pt-2.5">
          <span className="text-[15px] font-semibold tabular-nums text-foreground">{money(p.price, currency, locale)}</span>
          {p.was && <s className="text-[13px] tabular-nums text-muted-foreground">{money(p.was, currency, locale)}</s>}
          <span className="ml-auto inline-flex items-center gap-1 text-[13px] font-medium text-primary">
            {t.get}
            <Icon name="arrow-right" size={14} className="transition-transform duration-300 group-hover:translate-x-0.5" />
          </span>
        </span>
      </div>
    </a>
  );
}

/** A row of choices with a marker that glides to the active one */
function Switch({ options, value, onChange, label }: { options: { id: string; label: string; count?: number }[]; value: string; onChange: (id: string) => void; label: string }) {
  const wrap = useRef<HTMLDivElement>(null);
  const [box, setBox] = useState<{ x: number; w: number } | null>(null);
  useEffect(() => {
    const el = wrap.current;
    if (!el) return;
    const measure = () => {
      const b = el.querySelector<HTMLElement>('[aria-selected="true"]');
      if (b) setBox({ x: b.offsetLeft, w: b.offsetWidth });
    };
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(el);
    return () => ro.disconnect();
  }, [value]);
  const key = (e: React.KeyboardEvent) => {
    const d = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;
    if (!d) return;
    e.preventDefault();
    const i = options.findIndex((o) => o.id === value);
    const n = options[(i + d + options.length) % options.length];
    onChange(n.id);
    wrap.current?.querySelector<HTMLElement>(`[data-v="${n.id}"]`)?.focus();
  };
  return (
    <div ref={wrap} role="tablist" aria-label={label} onKeyDown={key} className="relative flex w-full rounded-xl border border-border bg-muted p-1">
      {box && <span aria-hidden className="absolute bottom-1 left-0 top-1 rounded-lg bg-card shadow-[var(--highlight),var(--shadow-sm)] ring-1 ring-border-strong transition-[transform,width] duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none" style={{ width: box.w, transform: `translateX(${box.x}px)` }} />}
      {options.map((o) => {
        const on = o.id === value;
        return (
          <button key={o.id} id={`bio-tab-${o.id}`} type="button" role="tab" data-v={o.id} aria-selected={on} aria-controls={`bio-panel-${o.id}`} tabIndex={on ? 0 : -1} onClick={() => onChange(o.id)} className={cn('relative z-[1] inline-flex min-h-11 flex-1 items-center justify-center gap-1.5 rounded-lg px-3 text-[14px] font-medium transition-colors', focusRing, on ? 'text-foreground' : 'text-muted-foreground hover:text-foreground', on && !box && 'bg-card shadow-[var(--shadow-sm)]')}>
            {o.label}
            {o.count !== undefined && <span className={cn('rounded-full px-1.5 text-[11.5px] tabular-nums transition-colors', on ? 'bg-[color-mix(in_oklab,var(--primary)_16%,transparent)] text-foreground' : 'bg-background/60 text-muted-foreground')}>{o.count}</span>}
          </button>
        );
      })}
    </div>
  );
}

/** A soft cover behind the column, it drifts slower than the page */
function Backdrop({ cover }: { cover?: React.ReactNode }) {
  if (!cover) return null;
  return (
    <div aria-hidden data-clip-ok className="pointer-events-none absolute left-1/2 top-[-80px] h-[540px] w-screen -translate-x-1/2 overflow-hidden [mask-image:linear-gradient(to_bottom,black_35%,transparent)]">
      <Parallax speed={60} className={cn('absolute -inset-y-10 inset-x-0 opacity-40 blur-3xl saturate-150 dark:opacity-30', fit)}>
        {cover}
      </Parallax>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* stack                                                               */
/* ------------------------------------------------------------------ */

function StackView({ profile, links = [], socials, shareUrl, t }: LinkInBioProps & { t: Texts }) {
  const [first, ...rest] = links;
  const featured = first?.image ? first : undefined;
  const rows = featured ? rest : links;
  return (
    <div className="relative mx-auto w-full max-w-[460px]">
      <div className="absolute right-0 top-0">
        <ShareButton url={shareUrl} t={t} />
      </div>
      <div className="kit-rise">
        <Head profile={profile} />
      </div>
      <Stagger step={70} className="mt-8 grid grid-cols-[minmax(0,1fr)] gap-3">
        {featured && <FeaturedLink link={featured} />}
        {rows.map((l) => (
          <LinkRow key={l.id} link={l} />
        ))}
      </Stagger>
      <div className="mt-8">
        <Socials socials={socials} t={t} />
      </div>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* tiles                                                               */
/* ------------------------------------------------------------------ */

function NewsletterTile({ data, t, onSubscribe }: { data: NonNullable<LinkInBioProps['newsletter']>; t: Texts; onSubscribe?: (e: string) => void }) {
  const [email, setEmail] = useState('');
  const [state, setState] = useState<'idle' | 'error' | 'done'>('idle');
  const submit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return setState('error');
    onSubscribe?.(email);
    setState('done');
  };
  return (
    <div className="flex h-full flex-col">
      <span className="flex size-10 items-center justify-center rounded-xl bg-[color-mix(in_oklab,var(--primary)_14%,transparent)] text-primary">
        <Icon name="mail" size={19} />
      </span>
      <h4 className="mt-4 text-[17px] font-semibold tracking-[-0.01em] text-foreground">{data.title}</h4>
      {data.text && <p className="mt-1.5 text-pretty text-[14px] leading-relaxed text-muted-foreground">{data.text}</p>}
      {state === 'done' ? (
        <p role="status" className="mt-auto flex items-start gap-2 pt-5 text-[14px] font-medium text-foreground [animation:kitbio-swap_.5s_var(--ease-kit)]">
          <Icon name="check-circle" size={18} className="mt-px text-success" />
          {t.subscribed}
        </p>
      ) : (
        <form onSubmit={submit} noValidate className="mt-auto pt-5">
          <label htmlFor="bio-nl" className="sr-only">
            {t.email}
          </label>
          <div className="flex gap-2">
            <input id="bio-nl" type="email" value={email} onChange={(e) => { setEmail(e.target.value); if (state === 'error') setState('idle'); }} placeholder={t.emailPh} aria-invalid={state === 'error'} aria-describedby={state === 'error' ? 'bio-nl-err' : undefined} className={cn('h-11 w-0 min-w-0 flex-1 rounded-lg border bg-background px-3 text-[14px] text-foreground placeholder:text-subtle-foreground', focusRing, state === 'error' ? 'border-destructive' : 'border-border-strong')} />
            <Button type="submit" size="md" className="px-4">
              {t.subscribe}
            </Button>
          </div>
          {state === 'error' && (
            <p id="bio-nl-err" className="mt-2 text-[13px] text-destructive">
              {t.invalid}
            </p>
          )}
          {data.count && <p className="mt-2.5 text-[12.5px] text-muted-foreground">{data.count}</p>}
        </form>
      )}
    </div>
  );
}

function TilesView({ profile, links = [], socials, products = [], events = [], video, newsletter, shareUrl, currency = 'EUR', onSubscribe, t }: LinkInBioProps & { t: Texts }) {
  const locale = useLocale();
  const spot = useSpotlight<HTMLDivElement>();
  const next = events[0];
  const tile = 'kit-card relative overflow-hidden';
  return (
    <Stagger step={60} className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4 lg:gap-4">
      <div className={cn(tile, 'flex flex-col p-6 sm:col-span-2 lg:row-span-2 lg:p-8')}>
        <div className="flex items-start justify-between gap-4">
          <Head profile={profile} size="lg" align="left" />
          <ShareButton url={shareUrl} t={t} />
        </div>
        <div className="mt-5">
          <Socials socials={socials} t={t} align="left" />
        </div>
        {links.length > 0 && (
          <div className="mt-6 border-t border-border pt-3 lg:mt-auto lg:pt-4"><ul className="-mx-3 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-1">
            {links.map((l) => (
              <li key={l.id}>
                <a href={l.href} className={cn('group flex min-h-12 items-center gap-3 rounded-lg px-3 py-2.5 transition-colors hover:bg-accent', focusRing)}>
                  {l.icon && <Icon name={l.icon} size={17} className="text-muted-foreground transition-colors group-hover:text-primary" />}
                  <span className="line-clamp-2 min-w-0 flex-1 text-[14.5px] font-medium leading-snug text-foreground">{l.label}</span>
                  <Icon name="arrow-up-right" size={15} className="text-muted-foreground transition-transform duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5" />
                </a>
              </li>
            ))}
          </ul></div>
        )}
      </div>
      {video && (
        <a href={video.href} className={cn(tile, 'group min-h-[240px] sm:col-span-2', focusRing)}>
          <div data-clip-ok className={cn('absolute inset-0 bg-muted transition-transform duration-700 ease-[var(--ease-kit)] group-hover:scale-[1.03] motion-reduce:transition-none', fit)}>{video.media}</div>
          <div className="absolute inset-0 bg-[linear-gradient(to_top,rgb(0_0_0/0.82),rgb(0_0_0/0.4)_45%,transparent_80%)]" />
          <span className="absolute left-1/2 top-1/2 flex size-16 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-white/90 text-black shadow-[var(--shadow-lg)] transition-transform duration-500 ease-[var(--ease-kit)] group-hover:scale-110">
            <span aria-hidden className="absolute inset-0 animate-ping rounded-full bg-white/40 [animation-duration:2.4s] motion-reduce:animate-none" />
            <Icon name="play" size={24} className="relative ml-1" />
          </span>
          <div className="absolute inset-x-0 bottom-0 flex items-end justify-between gap-3 p-5 text-white">
            <span>
              <span className="block text-[12.5px] font-medium text-white/80">{t.watch}</span>
              <span className="mt-0.5 block text-[17px] font-semibold leading-snug">{video.title}</span>
            </span>
            {video.length && <span className="shrink-0 rounded-md bg-black/50 px-2 py-1 text-[12.5px] tabular-nums">{video.length}</span>}
          </div>
        </a>
      )}
      {newsletter && (
        <div className={cn(tile, 'p-6')}>
          <NewsletterTile data={newsletter} t={t} onSubscribe={onSubscribe} />
        </div>
      )}
      {next && (
        <div onPointerMove={spot} className={cn(tile, 'kit-spotlight flex flex-col p-6')}>
          <p className="text-[12.5px] font-medium text-muted-foreground">{t.next}</p>
          <div className="mt-3 flex items-center gap-3">
            <DateBadge iso={next.start} />
            <span className="min-w-0">
              <span className="block text-[15.5px] font-semibold leading-snug text-foreground">{next.title}</span>
              <span className="mt-0.5 block text-[13px] text-muted-foreground">{[next.kind, fmt(next.start, locale, { hour: 'numeric', minute: '2-digit' })].filter(Boolean).join(' · ')}</span>
            </span>
          </div>
          {next.seats !== undefined && (
            <div className="mt-5">
              <div className="flex justify-between text-[12.5px]">
                <span className="font-medium text-warning-foreground">{fill(t.seats, { n: next.seats })}</span>
              </div>
              <SeatBar left={next.seats} />
            </div>
          )}
          <span className="mt-auto pt-5" /><Button href={next.href} variant="outline" size="sm" icon="arrow-right" className="self-start max-sm:w-full max-sm:self-stretch">
            {t.join}
          </Button>
        </div>
      )}
      {products.slice(0, 4).map((p) => (
        <ProductCard key={p.id} p={p} currency={currency} t={t} className="max-sm:flex-row [&>div:first-child]:max-sm:aspect-auto [&>div:first-child]:max-sm:w-28" />
      ))}
    </Stagger>
  );
}

/** How full a date is, the bar grows when it scrolls in */
function SeatBar({ left, total = 30 }: { left: number; total?: number }) {
  const [ref, inView] = useInView<HTMLDivElement>();
  const taken = round(Math.min(1, Math.max(0, (total - left) / total)), 3);
  return (
    <div ref={ref} className="mt-2 h-1.5 overflow-hidden rounded-full bg-[color-mix(in_oklab,var(--foreground)_12%,transparent)]" aria-hidden>
      <div className="h-full origin-left rounded-full bg-warning transition-transform duration-1000 ease-[var(--ease-kit)] motion-reduce:transition-none" style={{ transform: `scaleX(${inView ? taken : 0})` }} />
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* tabs                                                                */
/* ------------------------------------------------------------------ */

function TabsView({ profile, links = [], socials, products = [], events = [], shareUrl, currency = 'EUR', t }: LinkInBioProps & { t: Texts }) {
  const opts = [
    { id: 'links', label: t.links, count: links.length },
    ...(products.length ? [{ id: 'shop', label: t.shop, count: products.length }] : []),
    ...(events.length ? [{ id: 'dates', label: t.dates, count: events.length }] : []),
  ];
  const [tab, setTab] = useState('links');
  return (
    <div className="relative mx-auto w-full max-w-[520px]">
      <div className="absolute right-0 top-0">
        <ShareButton url={shareUrl} t={t} />
      </div>
      <div className="kit-rise">
        <Head profile={profile} />
        <div className="mt-6">
          <Socials socials={socials} t={t} />
        </div>
      </div>
      <div className="mt-8">
        <Switch label={t.sections} options={opts} value={tab} onChange={setTab} />
      </div>
      <div key={tab} id={`bio-panel-${tab}`} role="tabpanel" aria-labelledby={`bio-tab-${tab}`} className="mt-5 [animation:kitbio-swap_.45s_var(--ease-kit)]">
        {tab === 'links' && (
          <div className="grid grid-cols-[minmax(0,1fr)] gap-3">
            {links.map((l, i) => (
              <div key={l.id} style={{ animationDelay: `${i * 50}ms` }} className="[animation:kitbio-in_.45s_var(--ease-kit)_both]">
                <LinkRow link={l} />
              </div>
            ))}
          </div>
        )}
        {tab === 'shop' && (
          <div className="grid grid-cols-2 gap-3">
            {products.map((p, i) => (
              <div key={p.id} className="flex [animation:kitbio-in_.45s_var(--ease-kit)_both] [&>a]:w-full" style={{ animationDelay: `${i * 60}ms` }}>
                <ProductCard p={p} currency={currency} t={t} />
              </div>
            ))}
          </div>
        )}
        {tab === 'dates' && (
          <ul className="divide-y divide-border rounded-xl border border-border bg-card px-4">
            {events.map((e) => (
              <EventRow key={e.id} e={e} t={t} />
            ))}
          </ul>
        )}
      </div>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* dates                                                               */
/* ------------------------------------------------------------------ */

function DatesView({ profile, links = [], events = [], socials, shareUrl, t }: LinkInBioProps & { t: Texts }) {
  const locale = useLocale();
  const [next, ...later] = events;
  return (
    <div className="relative mx-auto w-full max-w-[520px]">
      <div className="absolute right-0 top-0">
        <ShareButton url={shareUrl} t={t} />
      </div>
      <div className="kit-rise">
        <Head profile={profile} />
      </div>
      {next && (
        <div className="kit-card mt-8 overflow-hidden kit-rise [--kit-delay:120ms]">
          <div className="flex items-center justify-between gap-3 border-b border-border px-5 py-3">
            <span className="inline-flex items-center gap-2 text-[13px] font-medium text-foreground">
              <span className="relative flex size-2">
                <span aria-hidden className="absolute inset-0 animate-ping rounded-full bg-primary opacity-60 motion-reduce:animate-none" />
                <span className="relative size-2 rounded-full bg-primary" />
              </span>
              {t.next}
            </span>
            {next.kind && <span className="text-[12.5px] text-muted-foreground">{next.kind}</span>}
          </div>
          <div className="p-5">
            <h4 className="text-[19px] font-semibold leading-snug tracking-[-0.01em] text-foreground">{next.title}</h4>
            <p className="mt-1 text-[14px] text-muted-foreground">
              {fmt(next.start, locale, { weekday: 'long', day: 'numeric', month: 'long', hour: 'numeric', minute: '2-digit' })}
              {next.place && ` · ${next.place}`}
            </p>
            <p className="mb-2.5 mt-5 text-[12.5px] font-medium text-muted-foreground">{t.in}</p>
            <Countdown iso={next.start} t={t} />
            <div className="mt-5 flex flex-wrap items-center gap-3">
              <Magnetic className="max-sm:flex max-sm:w-full"><Button href={next.href} icon="arrow-right" className="max-sm:w-full">
                {t.join}
              </Button></Magnetic>
              {next.seats !== undefined && <span className="text-[13px] font-medium text-warning-foreground">{fill(t.seats, { n: next.seats })}</span>}
            </div>
          </div>
        </div>
      )}
      {later.length > 0 && (
        <>
          <p className="mb-1 mt-8 text-[13px] font-medium text-muted-foreground">{t.later}</p>
          <Stagger as="ul" step={70} className="divide-y divide-border border-y border-border">
            {later.map((e) => (
              <EventRow key={e.id} e={e} t={t} />
            ))}
          </Stagger>
        </>
      )}
      {links.length > 0 && (
        <Stagger step={60} className="mt-8 grid grid-cols-[minmax(0,1fr)] gap-3">
          {links.map((l) => (
            <LinkRow key={l.id} link={l} />
          ))}
        </Stagger>
      )}
      <div className="mt-8">
        <Socials socials={socials} t={t} />
      </div>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* split                                                               */
/* ------------------------------------------------------------------ */

function SplitView({ profile, groups = [], socials, shareUrl, t }: LinkInBioProps & { t: Texts }) {
  return (
    <div className="grid grid-cols-1 gap-10 lg:grid-cols-[minmax(0,5fr)_minmax(0,6fr)] lg:gap-16">
      <div className="lg:sticky lg:top-8 lg:self-start">
        <div className="kit-card relative overflow-hidden">
          <div data-clip-ok className="relative aspect-[4/3] overflow-hidden bg-muted sm:aspect-[16/9] lg:aspect-[5/4]">
            <Parallax speed={50} className={cn('absolute -inset-y-8 inset-x-0', fit)}>
              {profile.cover ?? profile.avatar}
            </Parallax>
            <div className="absolute inset-x-0 bottom-0 h-1/2 bg-[linear-gradient(to_top,rgb(0_0_0/0.6),transparent)]" />
            <div className="absolute right-4 top-4">
              <ShareButton url={shareUrl} t={t} />
            </div>
            <div className="absolute inset-x-0 bottom-0 p-5 text-white sm:p-6">
              <h3 className="text-[26px] font-semibold leading-tight tracking-[-0.02em] sm:text-[30px]">{profile.name}</h3>
              {profile.handle && <p className="mt-1 text-[14.5px] text-white/80">{profile.handle}</p>}
            </div>
          </div>
          <div className="p-5 sm:p-6">
            {profile.bio && <p className="text-pretty text-[15px] leading-relaxed text-foreground/85">{profile.bio}</p>}
            {profile.status && (
              <p className="mt-4 inline-flex min-h-8 items-center gap-2 rounded-full bg-[color-mix(in_oklab,var(--success)_14%,transparent)] px-3 text-[13px] font-medium text-success-foreground">
                <span className="relative flex size-2">
                  <span aria-hidden className="absolute inset-0 animate-ping rounded-full bg-success opacity-60 motion-reduce:animate-none" />
                  <span className="relative size-2 rounded-full bg-success" />
                </span>
                {profile.status}
              </p>
            )}
            <div className="mt-5 border-t border-border pt-5">
              <Socials socials={socials} t={t} align="left" />
            </div>
          </div>
        </div>
      </div>
      <div className="grid grid-cols-1 gap-10">
        {groups.map((g, gi) => (
          <section key={g.title} aria-labelledby={`bio-g-${gi}`}>
            <div className="mb-4 flex items-baseline justify-between gap-3 border-b border-border pb-3">
              <h3 id={`bio-g-${gi}`} className="text-[17px] font-semibold tracking-[-0.01em] text-foreground">
                {g.title}
              </h3>
              <span className="text-[13px] tabular-nums text-muted-foreground">{String(g.links.length).padStart(2, '0')}</span>
            </div>
            <Stagger step={60} className="grid grid-cols-[minmax(0,1fr)] gap-3">
              {g.links.map((l) => (l.image ? <FeaturedLink key={l.id} link={l} /> : <LinkRow key={l.id} link={l} />))}
            </Stagger>
          </section>
        ))}
      </div>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* LinkInBio                                                           */
/* ------------------------------------------------------------------ */

/**
 * A link-in-bio page for a person: profile, links, products and dates.
 *   stack  the classic column with a featured link and share
 *   tiles  a bento of profile, video, newsletter, next date, products and links
 *   tabs   links, shop and dates behind a gliding switch
 *   dates  a live countdown to the next date, reminders for the others
 *   split  a portrait that stays while grouped links scroll beside it
 */
export function LinkInBio(props: LinkInBioProps) {
  const { variant = 'stack', labels, className, profile } = props;
  const t = useTexts(TEXTS, labels);
  const narrow = variant === 'stack' || variant === 'tabs' || variant === 'dates';
  const body = (() => {
    switch (variant) {
      case 'tiles':
        return <TilesView {...props} t={t} />;
      case 'tabs':
        return <TabsView {...props} t={t} />;
      case 'dates':
        return <DatesView {...props} t={t} />;
      case 'split':
        return <SplitView {...props} t={t} />;
      default:
        return <StackView {...props} t={t} />;
    }
  })();
  return (
    <Section spacing="sm" className={cn('overflow-clip', className)} background={variant === 'tiles' ? 'surface' : 'default'}>
      <BioMotion />
      {narrow && <Backdrop cover={profile.cover} />}
      <div className="relative">{body}</div>
    </Section>
  );
}

Häufige Fragen

Wie installiere ich Link in Bio?

Mit aiw add @ai-website/link-in-bio. Der Befehl legt die Datei components/ui/link-in-bio.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind button, icon, motion und section. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat Link in Bio?

5 Varianten, Spalte, Kacheln, Tabs, Termine und Geteilt mit Porträt. Du wählst sie mit der Prop variant, zum Beispiel variant="tiles".

Kann ich die Texte von Link in Bio übersetzen?

Ja. Link in Bio bringt 26 eigene Texte auf Deutsch und Englisch mit, etwa „Link zu dieser Seite kopieren“, „Link kopiert“ und „Links“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.

Kann ein KI-Assistent Link in Bio einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Link in Bio mit aiw_components und installieren sie mit aiw_add @ai-website/link-in-bio. Nenn im Prompt die Variante, etwa variant="stack", dann schreibt der Assistent deine Inhalte direkt in die Props.

Kann ich Link in Bio ohne Code anpassen?

Ja, im Studio. Alle 5 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden LinkInBio-Code zum Kopieren.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert