← alle entries

hero-badge

FC-0144Badges & bannershero-badgeprimitivev1.0.0

De pil boven een hero-kop: icoon vóór, tekst, icoon erna; komt van boven in en het voorste icoon kantelt en kleurt naar --primary als je erop komt. Als link of knop. Drie vormen (default, outline, ghost) en drie maten (typerollen label, tekst-klein, tekst).

zelfde component — andere tokens
Nieuw! Componenten v2

Komt van boven in; kom erop en het voorste icoon kantelt en kleurt naar de merkkleur. Gedrukt: 0×.

primitives/badges/hero-badge.tsx
/**
 * HeroBadge — de pil boven een hero-kop ('Nieuw! Componenten v2 →'): een
 * icoon vóór, tekst, een icoon erna; komt bij verschijnen van boven in en
 * het voorste icoon kantelt licht als je erop komt (en kleurt naar
 * --primary). Als link (href) of knop (onPress). Drie vormen (default,
 * outline, ghost) en drie maten (sm, md, lg) — de maten zijn typerollen.
 *
 * Geoogst uit 21st.dev Hero Badge (Codehagen): next/link → linkComponent,
 * losse cubic-bezier [0.16, 1, 0.3, 1] + 0.8 s → smooth-token, spring
 * stiffness 300/damping 10 → pop-token, useAnimation → whileHover-varianten,
 * tekst-xs/sm/base → --text-label/--text-body-sm/--text-body, onClick →
 * onPress (usePress), PrismUI-iconen uit de demo → slots.
 */

"use client";

import { useMemo } from "react";
import type * as React from "react";
import { motion, useReducedMotion } from "framer-motion";
import { useSpringTransition } from "../motion";
import { usePress } from "../ui/press";

export interface HeroBadgeProps extends Omit<React.HTMLAttributes<HTMLElement>, "onClick" | "children"> {
  children: React.ReactNode;
  /** Icoon vóór de tekst (kantelt bij hover). */
  icon?: React.ReactNode;
  /** Icoon na de tekst (bv. een chevron). */
  endIcon?: React.ReactNode;
  variant?: "default" | "outline" | "ghost";
  size?: "sm" | "md" | "lg";
  href?: string;
  onPress?: () => void;
  linkComponent?: React.ElementType;
}

export function HeroBadge({ children, icon, endIcon, variant = "default", size = "md", href, onPress, linkComponent: Link = "a", className, ...rest }: HeroBadgeProps) {
  const reduced = useReducedMotion() ?? false;
  const entree = useSpringTransition("smooth", { mass: "content" });
  const kantel = useSpringTransition("pop");
  const { pressed, pressProps } = usePress({ onPress, disabled: !onPress });
  const MotionLink = useMemo(() => motion.create(Link as React.ElementType), [Link]);
  const klassen = ["fp-hbadge", `fp-hbadge--${variant}`, `fp-hbadge--${size}`, className].filter(Boolean).join(" ");
  const pil = (
    <motion.span className="fp-hbadge__pil" initial={reduced ? false : { opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} transition={entree}>
      {icon && (
        <motion.span className="fp-hbadge__icoon fp-hbadge__icoon--voor" variants={{ rest: { rotate: 0 }, hover: { rotate: -10 } }} transition={kantel}>
          {icon}
        </motion.span>
      )}
      <span>{children}</span>
      {endIcon && <span className="fp-hbadge__icoon">{endIcon}</span>}
    </motion.span>
  );
  if (href) {
    return (
      <MotionLink {...rest} href={href} className={klassen} initial="rest" animate="rest" whileHover="hover" whileFocus="hover">
        {pil}
      </MotionLink>
    );
  }
  return (
    <motion.button
      {...(rest as React.ComponentProps<typeof motion.button>)}
      {...pressProps}
      type="button"
      className={klassen}
      data-pressed={pressed ? "" : undefined}
      initial="rest"
      animate="rest"
      whileHover="hover"
      whileFocus="hover"
    >
      {pil}
    </motion.button>
  );
}
primitives/badges/badges.css
/**
 * Badges & banners — importeren in de app-root (registry-site: app/globals.css).
 * Token-contract 2. Toon-varianten lopen via één CSS-variabele (--fp-toon) die
 * uit de status-tokens komt; de entries zelf bevatten nul kleuren.
 */

/* ── announcement: pil met een tag links en een titel ─────────────────────── */
.fp-ann {
  --fp-toon: var(--foreground);
  display: inline-flex; align-items: center; gap: var(--space-xs); max-width: 100%;
  padding: 0.125rem var(--space-sm) 0.125rem 0.125rem; border-radius: 999px;
  border: var(--border-width) solid var(--border); background: var(--background); color: var(--foreground);
  font-size: var(--text-body-sm); font-weight: 500; line-height: 1.4; text-decoration: none;
  box-shadow: 0 1px 2px color-mix(in srgb, var(--foreground) 6%, transparent);
  transition: box-shadow var(--motion-instant-duration) var(--motion-instant), border-color var(--motion-instant-duration) var(--motion-instant);
}
a.fp-ann:hover, button.fp-ann:hover { box-shadow: 0 2px 6px color-mix(in srgb, var(--foreground) 10%, transparent); }
.fp-ann:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
button.fp-ann { cursor: pointer; font: inherit; font-size: var(--text-body-sm); font-weight: 500; }
button.fp-ann[data-pressed] { transform: scale(0.98); }
/* Getinte tonen: vlak en rand uit de status-token, tekst in de token zelf. */
.fp-ann--info { --fp-toon: var(--info); }
.fp-ann--success { --fp-toon: var(--success); }
.fp-ann--warning { --fp-toon: var(--warning); }
.fp-ann--danger { --fp-toon: var(--danger); }
.fp-ann--info, .fp-ann--success, .fp-ann--warning, .fp-ann--danger {
  color: var(--fp-toon);
  background: color-mix(in srgb, var(--fp-toon) 10%, var(--background));
  border-color: color-mix(in srgb, var(--fp-toon) 20%, transparent);
}
.fp-ann__tag {
  flex: 0 0 auto; padding: 0.125rem var(--space-xs); border-radius: 999px;
  font-size: var(--text-label); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  background: color-mix(in srgb, var(--fp-toon) 8%, transparent);
}
.fp-ann--info .fp-ann__tag, .fp-ann--success .fp-ann__tag, .fp-ann--warning .fp-ann__tag, .fp-ann--danger .fp-ann__tag { background: color-mix(in srgb, var(--background) 60%, transparent); }
/* 'raised': de tag als los kaartje met rand (de Badge Tag-vorm). */
.fp-ann--raised .fp-ann__tag { background: var(--card); border: var(--border-width) solid var(--border); }
.fp-ann--raised { background: var(--muted); }
.fp-ann__titel { display: inline-flex; align-items: center; gap: var(--space-2xs); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fp-ann__titel > svg { flex: 0 0 auto; width: 1em; height: 1em; opacity: 0.7; }

/* ── promo-banner: één regel met onderstreepte actie, toelichting en sluiten ── */
.fp-promo { position: relative; display: inline-flex; }
.fp-promo__vlak {
  position: relative; display: flex; align-items: center; gap: var(--space-2xs); height: 2.25rem;
  padding: 0 var(--space-2xs) 0 var(--space-xs); border-radius: var(--radius-sm);
  border: var(--border-width) solid color-mix(in srgb, var(--info) 30%, transparent);
  background: color-mix(in srgb, var(--info) 8%, var(--background));
  font-size: var(--text-body-sm); color: var(--info);
}
.fp-promo__actie {
  margin: 0; padding: var(--space-2xs) 0; border: 0; background: transparent; cursor: pointer;
  font: inherit; font-size: var(--text-body-sm); font-weight: 500; color: var(--foreground);
  text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--info) 35%, transparent); text-underline-offset: 0.35em;
  border-radius: var(--radius-sm);
  transition: color var(--motion-instant-duration) var(--motion-instant), text-decoration-color var(--motion-instant-duration) var(--motion-instant);
}
.fp-promo__actie:hover { color: var(--info); text-decoration-color: color-mix(in srgb, var(--info) 60%, transparent); }
.fp-promo__actie:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.fp-promo__tekst { margin: 0; white-space: nowrap; }
.fp-promo__sluit {
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
  width: 1.5rem; height: 1.5rem; margin: 0; padding: 0; border: 0; border-radius: var(--radius-sm);
  background: transparent; color: var(--info); cursor: pointer;
  transition: background var(--motion-instant-duration) var(--motion-instant);
}
.fp-promo__sluit:hover { background: color-mix(in srgb, var(--info) 15%, transparent); }
.fp-promo__sluit:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
.fp-promo__sluit > svg { width: 1rem; height: 1rem; }
/* De twee tandwielen die bij hover uit de hoeken zweven — verf, geen layout. */
.fp-promo__wiel { position: absolute; z-index: 1; width: 1rem; height: 1rem; color: var(--info); pointer-events: none; }
.fp-promo__wiel--lb { left: 0.25rem; top: 0.125rem; }
.fp-promo__wiel--ro { left: 6rem; bottom: 0.125rem; }

/* ── hero-badge: pil boven een hero, met icoon dat kantelt bij hover ──────── */
.fp-hbadge { display: inline-flex; cursor: pointer; text-decoration: none; color: inherit; border: 0; background: transparent; padding: 0; margin: 0; font: inherit; border-radius: 999px; }
.fp-hbadge:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.fp-hbadge__pil {
  display: inline-flex; align-items: center; gap: var(--space-xs);
  padding: 0.375rem var(--space-md); border-radius: 999px;
  border: var(--border-width) solid var(--border); background: var(--background); color: var(--foreground);
  font-size: var(--text-body-sm); font-weight: 500; line-height: 1.2;
  transition: background var(--motion-instant-duration) var(--motion-instant), border-color var(--motion-instant-duration) var(--motion-instant);
}
.fp-hbadge:hover .fp-hbadge__pil { background: var(--muted); }
.fp-hbadge--outline .fp-hbadge__pil { border-width: calc(var(--border-width) * 2); }
.fp-hbadge--ghost .fp-hbadge__pil { border-color: transparent; background: transparent; }
.fp-hbadge--ghost:hover .fp-hbadge__pil { background: color-mix(in srgb, var(--muted) 50%, transparent); }
.fp-hbadge--sm .fp-hbadge__pil { gap: var(--space-2xs); padding: var(--space-2xs) var(--space-sm); font-size: var(--text-label); }
.fp-hbadge--lg .fp-hbadge__pil { gap: var(--space-xs); padding: var(--space-xs) var(--space-lg); font-size: var(--text-body); }
.fp-hbadge__icoon { display: inline-flex; color: color-mix(in srgb, var(--foreground) 60%, transparent); transition: color var(--motion-instant-duration) var(--motion-instant); }
.fp-hbadge:hover .fp-hbadge__icoon--voor { color: var(--primary); }
.fp-hbadge__icoon > svg { width: 1em; height: 1em; }