Equalizer Wordmark
A header wordmark whose leading letters bounce like equalizer bars with scroll speed, with a spectrum tint on hover.
npx shadcn@latest add @vibebox/equalizer-wordmarkControls
<EqualizerWordmark />About
A header logo for businesses without a drawn mark, or alongside one. The leading letters of the word bounce like equalizer bars in proportion to how fast the page scrolls, and settle when scrolling stops; the rest of the word holds still, so it stays legible. On hover or keyboard focus a soft spectrum tint rises through each letter in turn. An optional mark slot before the word tilts in 3D on hover. Ported from the VibeBox header, where "Vibe" bounces and "Box" holds; the word, the split and the mark are now props.
Use it when
- The header needs a wordmark that feels alive without an animated logo file.
- The business name is short enough to read at header size (up to about 16 characters).
Skip it when
- The client has a strict logo; use their file.
- The header already carries another moving element.
Install
- 1Add the VibeBox registry to
components.json:{ "registries": { "@vibebox": "https://motion.vibeboxph.com/r/{name}.json" } } - 2Add the component with the shadcn CLI:
npx shadcn@latest add @vibebox/equalizer-wordmark - 3Use it:
import { EqualizerWordmark } from "@/components/vibebox/equalizer-wordmark"; <EqualizerWordmark word="Northline" bounce={5} href="/" label="Northline home" className="text-2xl" />
Code
2 filescomponents/vibebox/equalizer-wordmark/equalizer-wordmark.client.tsx
/**
* VibeBox Motion · Equalizer Wordmark
* Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
* https://motion.vibeboxph.com/license
* Use it in unlimited personal, commercial and client projects.
* Do not sell or redistribute it as a component, kit, template or builder.
*/
"use client";
import { LazyMotion, domAnimation, m, motionValue, useAnimationFrame, useMotionValue, useScroll, useSpring, useTransform, useVelocity } from "motion/react";
import { useMemo, useRef, type CSSProperties, type ReactNode } from "react";
import { cn } from "@/lib/utils";
import { useReducedMotion } from "@/lib/vibebox/hooks";
/* The leading letters bounce like equalizer bars in proportion to scroll speed while the rest
hold. On hover or focus a soft spectrum tint rises through each letter in turn. */
const CSS = `
@keyframes vb-ew-rise { from { background-position: 0 0; } to { background-position: 0 100%; } }
@supports ((background-clip: text) or (-webkit-background-clip: text)) {
[data-vb-ew-letter] {
color: transparent;
background-image: linear-gradient(180deg, var(--foreground) 0%, var(--foreground) 34%, var(--ew-tint) 46%, var(--ew-tint-next) 55%, var(--foreground) 66%, var(--foreground) 100%);
background-size: 100% 300%;
background-position: 0 0;
-webkit-background-clip: text;
background-clip: text;
}
@media (prefers-reduced-motion: no-preference) {
[data-vb="equalizer-wordmark"]:hover [data-vb-ew-letter],
[data-vb="equalizer-wordmark"]:focus-visible [data-vb-ew-letter] {
animation: vb-ew-rise 820ms var(--ease-expo-in-out, cubic-bezier(0.65, 0, 0.35, 1)) calc(var(--ew-i) * 60ms) both;
}
}
}
[data-vb-ew-mark] { transition: transform 500ms var(--ease-expo-out, cubic-bezier(0.16, 1, 0.3, 1)); transform-style: preserve-3d; }
@media (prefers-reduced-motion: no-preference) {
[data-vb="equalizer-wordmark"]:hover [data-vb-ew-mark] { transform: rotateY(-22deg) rotateX(12deg) scale(1.06); }
}
`;
/** Bounce heights in px, repeated across the bouncing letters. */
const HEIGHTS = [5, 7.5, 6, 4.5];
/** A spectrum stop for letter `index` of `count`, mixed halfway with white: a tint, not a fill. */
function tint(index: number, count: number) {
const stop = count <= 1 ? 1 : Math.round((index / (count - 1)) * 6) + 1;
return `color-mix(in oklab, var(--spectrum-${Math.min(7, Math.max(1, stop))}) 50%, white)`;
}
export type EqualizerWordmarkProps = {
/** The word, for example the business name. */
word: string;
/** How many leading letters bounce with scroll speed (the rest hold). Defaults to about half. */
bounce?: number;
/** An optional mark before the word (an img or svg); it tilts on hover. */
mark?: ReactNode;
/** Makes the wordmark a link. */
href?: string;
/** Accessible name; defaults to the word (add "home" when it links home). */
label?: string;
onNavigate?: () => void;
className?: string;
};
export function EqualizerWordmark({ word, bounce, mark, href, label, onNavigate, className }: EqualizerWordmarkProps) {
const reduced = useReducedMotion();
const letters = useMemo(() => Array.from(word), [word]);
const bouncing = Math.min(letters.length, bounce ?? Math.ceil(letters.length / 2));
const { scrollY } = useScroll();
const velocity = useVelocity(scrollY);
const energy = useSpring(
useTransform(velocity, (v) => Math.min(1, Math.abs(v) / 2400)),
{ stiffness: 120, damping: 22, mass: 0.5 },
);
const burst = useMotionValue(0);
// One offset per letter; created outside the render path because the letter count varies.
const offsets = useMemo(() => letters.map(() => motionValue(0)), [letters]);
const resting = useRef(true);
useAnimationFrame((time) => {
if (reduced) return;
const amplitude = Math.max(energy.get(), burst.get());
if (amplitude < 0.01) {
if (!resting.current) {
offsets.forEach((offset) => offset.set(0));
resting.current = true;
}
return;
}
resting.current = false;
for (let index = 0; index < bouncing; index++) {
const phase = time / 120 + index * 1.4;
offsets[index]?.set(-amplitude * (HEIGHTS[index % HEIGHTS.length] ?? 5) * Math.abs(Math.sin(phase)));
}
});
const kick = () => {
if (reduced) return;
burst.set(1);
window.setTimeout(() => burst.set(0), 520);
};
const content = (
<>
<style href="vb-equalizer-wordmark" precedence="vb">
{CSS}
</style>
{mark && (
<span data-vb-ew-mark="" aria-hidden="true" className="inline-flex [&>*]:h-auto [&>*]:w-8">
{mark}
</span>
)}
<span aria-hidden="true" className="inline-flex items-baseline">
{letters.map((letter, index) => {
const style = {
"--ew-i": index,
"--ew-tint": tint(index, letters.length),
"--ew-tint-next": tint(Math.min(letters.length - 1, index + 1), letters.length),
} as CSSProperties;
return index < bouncing ? (
<m.span key={`${letter}-${index}`} data-vb-ew-letter="" className="inline-block will-change-transform" style={{ ...style, y: offsets[index] }}>
{letter}
</m.span>
) : (
<span key={`${letter}-${index}`} data-vb-ew-letter="" className="inline-block" style={style}>
{letter}
</span>
);
})}
</span>
</>
);
const classes = cn(
"relative inline-flex items-center gap-2.5 rounded-md font-display text-[1.16rem] leading-[1.1] font-bold tracking-[-0.035em] text-foreground no-underline [perspective:400px]",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-4 focus-visible:ring-offset-background focus-visible:outline-none",
className,
);
return (
<LazyMotion features={domAnimation} strict>
{href ? (
<a data-vb="equalizer-wordmark" href={href} aria-label={label ?? word} onMouseEnter={kick} onFocus={kick} onClick={onNavigate} className={classes}>
{content}
</a>
) : (
<span data-vb="equalizer-wordmark" role="img" aria-label={label ?? word} onMouseEnter={kick} className={classes}>
{content}
</span>
)}
</LazyMotion>
);
}components/vibebox/equalizer-wordmark/index.tsx
/**
* VibeBox Motion · Equalizer Wordmark
* Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
* https://motion.vibeboxph.com/license
* Use it in unlimited personal, commercial and client projects.
* Do not sell or redistribute it as a component, kit, template or builder.
*/
export { EqualizerWordmark, type EqualizerWordmarkProps } from "./equalizer-wordmark.client";Props
| Prop | Type | Default | Description |
|---|---|---|---|
| word * | string | — | The word, for example the business name. |
| bounce | number | half the letters | How many leading letters bounce with scroll speed. |
| mark | ReactNode | — | An optional mark before the word (img or svg); it tilts on hover. |
| href | string | — | Makes the wordmark a link. |
| label | string | — | Accessible name; defaults to the word. |
| onNavigate | () => void | — | Called on click (for example to close a menu). |
| className | string | — | Merged with cn(); set the size with a text class. |
Accessibility and motion
- Keyboard
- With href it is a link (Tab, Enter) and focus plays the tint; without href it is not focusable.
- Screen readers
- Named by
labelor the word; individual letters are hidden. - Touch
- A tap follows the link; the bounce comes from scrolling, which touch does too.
- Reduced motion
- The word at rest in the foreground colour; no bounce, tint or tilt.
Edge cases and limits
- Spaces count as letters; a two-word name bounces through its first word if
bouncestops at the space. - Without
background-clip: textsupport the letters stay solid foreground.
Related

Footer Wordmark
FreeA poster-sized word for the foot of a page whose letters rise toward the cursor like equalizer bars.
Brand and proof · Motion

A menu button whose three spectrum bars idle like a level meter and fold into an X when the menu opens.
Navigation · CSS

Equalizer Ring
FreeA crown of light bars pulsing like a visualizer around a floating glass box; bars rise toward the pointer.
3D · three.js