GodUIGodUI
81

Anatomy of the Stack Badge

A tech-stack wall built from one flex row, a lookup table that normalizes built-in and custom logos to the same shape, and two independent springs — one that enters once, one that only ever plays on hover.

A tech-stack wall usually means dropping in a row of brand SVGs and calling it done. StackBadge treats that row as two problems: normalizing whatever you hand it (a built-in name or a custom { name, icon, color }) into one uniform chip shape, and layering exactly two motions onto that chip — an entrance that fires once, and a hover that fires forever.

One pill, two tokens

Every chip is the same inline-flex items-center rounded-full row — an icon, then a label — regardless of whether it came from the built-in registry or a custom object:

Anatomyone pill · icon token + label token
Icon token
24×24 mono SVG, currentColor
Label token
font-medium text, same row
tsx
 
function resolveItem(item: StackBadgeItem, size: number): ResolvedItem {
  if (typeof item === "string") {
    const entry: TechEntry = TECH[item];
    return {
      key: item,
      label: entry.label,
      color: entry.color,
      glow: entry.mono
        ? "color-mix(in oklch, var(--foreground) 16%, transparent)"
        : `${entry.color}3d`,
      icon: <TechIcon path={entry.path} label={entry.label} size={size} />,
    };
  }
  return {
    key: item.name,
    label: item.name,
    color: item.color,
    glow: item.color.startsWith("#")
      ? `${item.color}3d`
      : "color-mix(in oklch, var(--foreground) 16%, transparent)",
    icon: item.icon,
  };
}

resolveItem is the whole trick: it runs before render and returns the same { key, label, color, glow, icon } shape either way. The chip component below never branches on "is this a built-in tech or a custom one" — it just destructures ResolvedItem. Mono logos (Next.js, Rust, Vercel — brands with no color of their own) get a neutral color-mix glow instead of a hex suffix, so they still warm up on hover without inventing a brand color that doesn't exist.

Enter once, hover forever

The motionenter once · hover loops forever
Stagger in
delay index × 40ms · spring 320/32/0.9
Hover glow
y −3px snappy 520/32 · glow 240ms
tsx
 
<motion.div
  initial={reduce || !animateIn ? false : { opacity: 0, y: 8 }}
  whileInView={reduce || !animateIn ? undefined : { opacity: 1, y: 0 }}
  viewport={{ once: true, margin: "0px 0px -10% 0px" }}
  transition={
    reduce || !animateIn
      ? undefined
      : { delay: index * 0.04, type: "spring", stiffness: 320, damping: 32, mass: 0.9 }
  }
  whileHover={
    reduce
      ? undefined
      : { y: -3, transition: { type: "spring", stiffness: 520, damping: 32 } }
  }
>

viewport={{ once: true }} means the stagger spring is a one-shot: it plays the moment the row is 10% into view and Framer detaches the observer — scrolling past and back never replays it. whileHover is a completely separate animation living on the same node; it has no viewport, no once, so it fires every time the pointer enters, indefinitely, for the lifetime of the chip. Two transition objects, two lifespans, one element.

The glow is a shadow, not a background swap

tsx
 
{glow ? (
  <span
    aria-hidden
    className="pointer-events-none absolute inset-0 rounded-full opacity-0 shadow-[0_10px_24px_-14px_var(--glow)] [transition:opacity_240ms_cubic-bezier(0.22,1,0.36,1)] group-hover/chip:opacity-100"
  />
) : null}

The halo is a box-shadow that's already painted at opacity-0 — hovering only flips its opacity to 1 over 240ms, so the glow itself never repaints or resizes, just fades in. --glow is set once per chip in inline style (the 3d hex suffix or the color-mix fallback from resolveItem), so the shadow's color is fixed per-item and the transition only ever touches opacity.

Reduced motion turns off the trigger, not the transition

tsx
 
initial={reduce || !animateIn ? false : { opacity: 0, y: 8 }}
whileHover={reduce ? undefined : { y: -3, ... }}

Under prefers-reduced-motion, initial becomes false (Framer renders the resting style immediately, no animate-in) and whileHover becomes undefined (no hover animation is registered at all). The chip still renders, still links, still shows its aria-label when showLabel is off — only the two springs are removed.

The result

Resultthe real component — hover a chip
React
TypeScript
Tailwind CSS
Next.js
Node.js
Figma
Rust
PostgreSQL

A lookup table that normalizes every item to one shape, one spring that plays once on scroll-in, and one independent spring that plays forever on hover — scroll past this wall and nothing happens twice, but you can hover the same chip a hundred times.

On this page

Built with GodUI

Beautifully crafted motion components for modern interfaces.

Star on GitHub