Marquee
React + Tailwind
const W = 200;
const H = 60;
const R = 12;
const GAP = 12;
// Bulb centres evenly spaced round the rounded rectangle, so the chase reads as one loop.
function bulbs() {
const straight = 2 * (W - 2 * R) + 2 * (H - 2 * R);
const total = straight + 2 * Math.PI * R;
const n = Math.round(total / GAP);
const out: { x: number; y: number }[] = [];
for (let k = 0; k < n; k++) {
let d = (k / n) * total;
const seg = (len: number) => {
const hit = d < len;
if (!hit) d -= len;
return hit;
};
const arc = (cx: number, cy: number, a0: number) => {
const a = a0 + d / R;
return { x: cx + Math.cos(a) * R, y: cy + Math.sin(a) * R };
};
if (seg(W - 2 * R)) out.push({ x: R + d, y: 0 });
else if (seg((Math.PI / 2) * R)) out.push(arc(W - R, R, -Math.PI / 2));
else if (seg(H - 2 * R)) out.push({ x: W, y: R + d });
else if (seg((Math.PI / 2) * R)) out.push(arc(W - R, H - R, 0));
else if (seg(W - 2 * R)) out.push({ x: W - R - d, y: H });
else if (seg((Math.PI / 2) * R)) out.push(arc(R, H - R, Math.PI / 2));
else if (seg(H - 2 * R)) out.push({ x: 0, y: H - R - d });
else out.push(arc(R, R, Math.PI));
}
return out;
}
const BULBS = bulbs();
const css = `
@keyframes btn-marquee-chase { 0%, 100% { background: light-dark(#cbced3, #393c41); box-shadow: none; } 12% { background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 6px 1px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 60%, transparent); } 30% { background: light-dark(#cbced3, #393c41); box-shadow: none; } }
@keyframes btn-marquee-flash { 0%, 40%, 80% { background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 8px 2px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 60%, transparent); } 20%, 60%, 100% { background: light-dark(#cbced3, #393c41); box-shadow: none; } }
.btn-marquee {
position: relative;
width: ${W}px;
height: ${H}px;
padding: 0;
border: none;
border-radius: ${R}px;
background: light-dark(#f3f4f7, #1a1c1f);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035), 0 12px 24px -14px light-dark(rgba(0, 0, 0, 0.21), rgba(0, 0, 0, 0.7));
color: light-dark(#373a40, #f1f2f3);
font: inherit;
cursor: pointer;
}
.btn-marquee:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 6px; }
.btn-marquee__bulb {
position: absolute;
width: 5px;
height: 5px;
margin: -2.5px;
border-radius: 50%;
background: light-dark(#cbced3, #393c41);
transform: translate(var(--x), var(--y)) scale(0.86);
left: 0;
top: 0;
}
.btn-marquee__panel { position: absolute; inset: 9px; display: grid; place-items: center; border-radius: 6px; background: light-dark(#fbfcfd, #121417); box-shadow: inset 0 1px 6px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
.btn-marquee__small { display: block; font-size: 9px; letter-spacing: 0.3em; margin-right: -0.3em; color: light-dark(#5f626a, #a7abb2); text-transform: uppercase; }
.btn-marquee__big { display: block; font-size: 15px; font-weight: 600; letter-spacing: 0.14em; margin-right: -0.14em; text-transform: uppercase; }
.btn-marquee:is(:hover, :focus-visible) .btn-marquee__bulb { animation: btn-marquee-chase 1.2s linear infinite calc(var(--k) * -0.05s); }
.btn-marquee:active .btn-marquee__bulb { animation: btn-marquee-flash 0.5s steps(1) infinite; }
@media (prefers-reduced-motion: reduce) {
.btn-marquee__bulb { animation: none !important; }
.btn-marquee:is(:hover, :focus-visible) .btn-marquee__bulb { background: light-dark(#42454c, #e5e6e9); }
}
`;
export function MarqueeButton() {
return (
<>
<style>
{css}
</style>
<button type="button" className="btn-marquee">
{BULBS.map((b, k) => (
<span
key={k}
className="btn-marquee__bulb"
aria-hidden="true"
style={{ '--x': `${b.x.toFixed(1)}px`, '--y': `${b.y.toFixed(1)}px`, '--k': BULBS.length - k } as React.CSSProperties}
/>
))}
<span className="btn-marquee__panel">
<span>
<span className="btn-marquee__small">Now showing</span>
<span className="btn-marquee__big">Get tickets</span>
</span>
</span>
</button>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<button type="button" class="btn-marquee">
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:12.0px;--y:0.0px;--k:42"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:23.9px;--y:0.0px;--k:41"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:35.8px;--y:0.0px;--k:40"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:47.7px;--y:0.0px;--k:39"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:59.6px;--y:0.0px;--k:38"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:71.5px;--y:0.0px;--k:37"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:83.3px;--y:0.0px;--k:36"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:95.2px;--y:0.0px;--k:35"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:107.1px;--y:0.0px;--k:34"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:119.0px;--y:0.0px;--k:33"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:130.9px;--y:0.0px;--k:32"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:142.8px;--y:0.0px;--k:31"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:154.7px;--y:0.0px;--k:30"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:166.6px;--y:0.0px;--k:29"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:178.5px;--y:0.0px;--k:28"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:190.3px;--y:0.2px;--k:27"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:199.1px;--y:7.5px;--k:26"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:200.0px;--y:19.3px;--k:25"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:200.0px;--y:31.2px;--k:24"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:200.0px;--y:43.1px;--k:23"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:198.0px;--y:54.6px;--k:22"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:188.0px;--y:60.0px;--k:21"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:176.1px;--y:60.0px;--k:20"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:164.2px;--y:60.0px;--k:19"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:152.3px;--y:60.0px;--k:18"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:140.4px;--y:60.0px;--k:17"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:128.5px;--y:60.0px;--k:16"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:116.7px;--y:60.0px;--k:15"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:104.8px;--y:60.0px;--k:14"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:92.9px;--y:60.0px;--k:13"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:81.0px;--y:60.0px;--k:12"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:69.1px;--y:60.0px;--k:11"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:57.2px;--y:60.0px;--k:10"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:45.3px;--y:60.0px;--k:9"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:33.4px;--y:60.0px;--k:8"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:21.5px;--y:60.0px;--k:7"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:9.7px;--y:59.8px;--k:6"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:0.9px;--y:52.5px;--k:5"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:0.0px;--y:40.7px;--k:4"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:0.0px;--y:28.8px;--k:3"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:0.0px;--y:16.9px;--k:2"></span>
<span class="btn-marquee__bulb" aria-hidden="true" style="--x:2.0px;--y:5.4px;--k:1"></span>
<span class="btn-marquee__panel">
<span>
<span class="btn-marquee__small">Now showing</span>
<span class="btn-marquee__big">Get tickets</span>
</span>
</span>
</button>CSS
@keyframes btn-marquee-chase { 0%, 100% { background: light-dark(#cbced3, #393c41); box-shadow: none; } 12% { background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 6px 1px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 60%, transparent); } 30% { background: light-dark(#cbced3, #393c41); box-shadow: none; } }
@keyframes btn-marquee-flash { 0%, 40%, 80% { background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 8px 2px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 60%, transparent); } 20%, 60%, 100% { background: light-dark(#cbced3, #393c41); box-shadow: none; } }
.btn-marquee {
position: relative;
width: 200px;
height: 60px;
padding: 0;
border: none;
border-radius: 12px;
background: light-dark(#f3f4f7, #1a1c1f);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035), 0 12px 24px -14px light-dark(rgba(0, 0, 0, 0.21), rgba(0, 0, 0, 0.7));
color: light-dark(#373a40, #f1f2f3);
font: inherit;
cursor: pointer;
}
.btn-marquee:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 6px; }
.btn-marquee__bulb {
position: absolute;
width: 5px;
height: 5px;
margin: -2.5px;
border-radius: 50%;
background: light-dark(#cbced3, #393c41);
transform: translate(var(--x), var(--y)) scale(0.86);
left: 0;
top: 0;
}
.btn-marquee__panel { position: absolute; inset: 9px; display: grid; place-items: center; border-radius: 6px; background: light-dark(#fbfcfd, #121417); box-shadow: inset 0 1px 6px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
.btn-marquee__small { display: block; font-size: 9px; letter-spacing: 0.3em; margin-right: -0.3em; color: light-dark(#5f626a, #a7abb2); text-transform: uppercase; }
.btn-marquee__big { display: block; font-size: 15px; font-weight: 600; letter-spacing: 0.14em; margin-right: -0.14em; text-transform: uppercase; }
.btn-marquee:is(:hover, :focus-visible) .btn-marquee__bulb { animation: btn-marquee-chase 1.2s linear infinite calc(var(--k) * -0.05s); }
.btn-marquee:active .btn-marquee__bulb { animation: btn-marquee-flash 0.5s steps(1) infinite; }
@media (prefers-reduced-motion: reduce) {
.btn-marquee__bulb { animation: none !important; }
.btn-marquee:is(:hover, :focus-visible) .btn-marquee__bulb { background: light-dark(#42454c, #e5e6e9); }
}Goal: Create a "Marquee" button.
Visual behavior: A ticket button framed in theatre marquee bulbs: on hover the lights chase round the border, and when you press they all flash together.
Technique:
- The bulbs are placed round a rounded rectangle at build time and given --x, --y and an index (--k).
- On hover and focus they share one chase keyframe with a delay from the index, so a bright spot runs round the frame; :active swaps it for a steps(1) flash that lights them all at once.
Accessibility: A real <button> with a visible label; decoration is aria-hidden. Keyboard focus starts the chase, plus an outline. Under prefers-reduced-motion the bulbs light steadily instead of chasing.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Marquee" button.
Visual behavior: A ticket button framed in theatre marquee bulbs: on hover the lights chase round the border, and when you press they all flash together.
Technique:
- The bulbs are placed round a rounded rectangle at build time and given --x, --y and an index (--k).
- On hover and focus they share one chase keyframe with a delay from the index, so a bright spot runs round the frame; :active swaps it for a steps(1) flash that lights them all at once.
Accessibility: A real <button> with a visible label; decoration is aria-hidden. Keyboard focus starts the chase, plus an outline. Under prefers-reduced-motion the bulbs light steadily instead of chasing.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A ticket button framed in theatre marquee bulbs: on hover the lights chase round the border, and when you press they all flash together.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- hover, click, glow, loop
- Techniques
- keyframes, css variables, stagger
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A real <button> with a visible label; decoration is aria-hidden. Keyboard focus starts the chase, plus an outline. Under prefers-reduced-motion the bulbs light steadily instead of chasing.