Magic Button

Back to Digital Exploration

From the CloudX website case

How it’s built

Three layers, one angle

The Magic Button was the featured component of the CloudX website: a call to action with a gradient border that keeps turning and a soft glow that breathes behind it. No JavaScript, no images, no SVG. Three stacked layers and a single animated value that drives all of them.

01

The structure

A wrapper holds two elements. Behind, the glow: an absolutely positioned layer that sits a little outside the button. On top, the button itself, which paints two layers on its own: the gradient border and the solid fill. The wrapper is position: relative so the glow is placed against it, and the button gets a higher z-index so it always stays in front.

<div className={styles.wrapper}>             // position: relative
    <div className={styles.glow} />          // layer 1: blurred, behind
    <button className={styles.button}>       // layers 2 + 3: border and fill
        Get Started
    </button>
</div>

The glow is hidden from screen readers with aria-hidden; it is pure decoration.

02

A gradient border without a border image

CSS borders can’t take a gradient and keep rounded corners at the same time (border-image ignores border-radius). The trick is to paint two backgrounds on the same element and clip each one to a different box*: the solid fill to the padding box and the conic gradient** to the border box. The border itself is transparent, so the only place where the gradient shows is that 1.5px ring.

.button {
    background:
        linear-gradient(var(--background), var(--background)) padding-box,
        conic-gradient(from var(--magic-angle), #f97316, #a855f7, #ec4899, #f97316) border-box;
    border: 1.5px solid transparent;
}

The fill is written as linear-gradient(var(--background), var(--background)) because only images can be stacked and clipped per layer, and a gradient of a single color*** is a solid image. Since it reads the site’s --background token, the button follows light mode, dark mode and the seasonal themes without extra code.

03

Animating the angle with @property

The gradient starts from var(--magic-angle). Turning that angle from 0° to 360° spins the colors around the border. The problem is that a normal custom property is just text: the browser can’t work out the values between “0deg” and “360deg”, so the animation would jump straight to the end. @property* registers it as a real angle, and from then on it can be animated like any other CSS value.

@property --magic-angle {
    syntax: "<angle>";
    initial-value: 0deg;
    inherits: false;
}

@keyframes magic-spin {
    to { --magic-angle: 360deg; }
}

.button { animation: magic-spin 3s linear infinite; }

The animation is linear and loops every 3 seconds, and since 360° looks exactly like 0°, the turn never shows a restart.

04

The glow

The glow uses the very same conic gradient and the same spin, but it is 6px larger on every side and blurred by 12px, so the colors bleed softly around the button. A second animation, the pulse, scales it to 108% and raises its opacity every 2 seconds, so it seems to breathe.

.glow {
    position: absolute;
    inset: -6px;                 /* 6px larger than the button on every side */
    border-radius: 9999px;
    background: conic-gradient(from var(--magic-angle), #f97316, #a855f7, #ec4899, #f97316);
    filter: blur(12px);
    opacity: 0.5;
    animation:
        magic-spin 3s linear infinite,
        magic-glow-pulse 2s ease-in-out infinite;
}

@keyframes magic-glow-pulse {
    0%, 100% { opacity: 0.6; transform: scale(1); }
    50%      { opacity: 1;   transform: scale(1.08); }
}

Because the pulse lasts 2 seconds and the spin 3, they only line up again every 6 seconds. That small offset keeps the motion from feeling mechanical.

05

Keeping the layers in sync

This was the tricky part. The border and the glow are separate elements, each animating its own --magic-angle. They stay perfectly aligned because both animations have the same duration and timing function and they start at the same moment, when the button is first rendered. If the glow started even a fraction later, its colors would trail behind the border’s for as long as the page stayed open.

06

Performance

Animating a gradient means repainting it on every frame: the browser can’t hand that work to the GPU the way it does with transform and opacity. Here it is cheap because the area is tiny, a pill of roughly 130 × 40 pixels. The pulse, on the other hand, only touches transform and opacity, which are the two properties browsers can animate without repainting.

07

Accessibility

It is a real <button>, so it works with the keyboard out of the box. For this Lab version I added two things the original didn’t have: a visible focus ring for keyboard users, and support for reduced motion, which stops the spin and the pulse and leaves a still gradient border.

.button:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 4px;
}

@media (prefers-reduced-motion: reduce) {
    .button, .glow { animation: none; }
}

The label uses the site’s foreground color over its background token, so its contrast is the same as the rest of the text on the page.