---
name: button-shine-sweep
description: Use when you want "Premium, commercial, polished" - Adds a moving highlight across a button surface.
---

# Button Shine Sweep

> **Category:** Motion / Interaction  -  **Personality:** Premium, commercial, polished
>
> **Best use:** Primary CTAs, pricing pages
>
> **Ratings:** Professional 5/5 - Casual 3/5 - Exotic 3/5 - Visual impact 4/5 - Difficulty 2/5 - Performance cost 2/5

## What it is

A glossy highlight band that glides diagonally across a button's surface, like light catching polished metal or glass. It signals "this is the button to click", which is why premium SaaS and pricing pages put it on their primary CTA. Two triggers are common and can be combined: a **hover** sweep (fires once when the pointer enters) and a slow **auto-shine** that glints every few seconds to draw the eye. It is a pure-CSS effect — a single skewed gradient pseudo-element, clipped to the button and translated across with `transform`.

(Note: this is the *button* version. The card "Card Shine Effect" is a different recipe — that one tracks the pointer with a radial specular gradient on a large surface.)

## Dependencies / CDN

None - pure CSS (the sweep needs no JavaScript). The demo loads two display/UI fonts and uses a tiny optional script only for the monthly/annual price toggle — neither is required for the effect:

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Archivo+Expanded:wght@600;700;800&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
```

## HTML

Any button/link works. Add `shine` for the hover sweep; add `auto` to also get the periodic glint. Colour/size are just extra modifier classes.

```html
<!-- hover-only sweep -->
<button class="shine indigo">Choose Scale</button>

<!-- hover sweep + periodic auto-glint (use sparingly: the one primary CTA) -->
<button class="shine auto gold">Start 14-day trial <span class="arw">&rarr;</span></button>
```

## CSS

```css
/* --- the button shell: the only two lines the effect strictly needs --- */
.shine{
  position: relative;     /* anchor the band            */
  overflow: hidden;       /* clip the band to the button */
  isolation: isolate;     /* keep mix-blend-mode local   */
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  border: 0; border-radius: 13px; padding: 14px 24px; cursor: pointer;
  font: 700 15px/1 'Plus Jakarta Sans', sans-serif;
  transition: transform .25s cubic-bezier(.2,.7,.3,1), box-shadow .25s;
}
.shine:hover  { transform: translateY(-2px); }
.shine:active { transform: translateY(0) scale(.99); }
.shine:focus-visible { outline: 2px solid #f5d188; outline-offset: 3px; }

/* --- THE SWEEP --- a skewed white gradient parked off-screen left.
   ::after = hover glint (in front)   ::before = periodic auto-glint (behind) --- */
.shine::after,
.auto::before{
  content: ""; position: absolute; top: -25%; bottom: -25%; left: 0; width: 55%;
  pointer-events: none;
  mix-blend-mode: screen;                 /* white band brightens the face -> gloss */
  background: linear-gradient(100deg,
    transparent 0, rgba(255,255,255,.55) 44%,
    rgba(255,255,255,.85) 50%,
    rgba(255,255,255,.55) 56%, transparent 100%);
  transform: translateX(-200%) skewX(-22deg);   /* fully off the left edge */
}
.shine::after  { z-index: 2; }
.auto::before  { z-index: 1; animation: bss-auto 4.8s ease-in-out infinite; }

.shine:hover::after { animation: bss-sweep .9s cubic-bezier(.25,.6,.3,1); }

@keyframes bss-sweep{ to { transform: translateX(320%) skewX(-22deg); } }
@keyframes bss-auto{                       /* glint in the first ~0.8s, then rest */
  0%        { transform: translateX(-200%) skewX(-22deg); }
  16%, 100% { transform: translateX(320%)  skewX(-22deg); }
}

/* --- a couple of surfaces so the band has something to glint on --- */
.gold{ color:#3a2807;
  background: linear-gradient(135deg,#fbe3a1,#e4b65a 46%,#caa044);
  box-shadow: 0 12px 28px -12px rgba(228,182,90,.8), inset 0 1px 0 rgba(255,255,255,.55); }
.indigo{ color:#fff;
  background: linear-gradient(135deg,#8e7dff,#5a47e0);
  box-shadow: 0 12px 30px -12px rgba(99,77,224,.85), inset 0 1px 0 rgba(255,255,255,.22); }

/* light buttons: a white screen-band is invisible, so use a normal-blend streak */
.light{ color:#23252f; background: linear-gradient(135deg,#eceef4,#cdd2de); }
.light::after{ mix-blend-mode: normal;
  background: linear-gradient(100deg, transparent 0, rgba(255,255,255,.95) 50%, transparent 100%); }

/* --- accessibility: NO sweep when the user asks for reduced motion --- */
@media (prefers-reduced-motion: reduce){
  .shine::after, .auto::before { display: none; }
  .shine:hover { transform: none; }
}
```

## JavaScript

Not required — the sweep is 100% CSS. The demo's only script is an optional pricing toggle that has nothing to do with the effect:

```js
// Optional: swap monthly <-> annual prices. Not part of the shine effect.
var sw = document.getElementById('bssBilling');
sw && sw.addEventListener('click', function(){
  var annual = sw.getAttribute('aria-checked') !== 'true';
  sw.setAttribute('aria-checked', annual ? 'true' : 'false');
  document.querySelectorAll('.bss-amount').forEach(function(el){
    el.textContent = el.getAttribute(annual ? 'data-a' : 'data-m');
  });
});
```

## How it works

- **One skewed gradient, parked off-screen.** A pseudo-element holds a narrow white-to-transparent `linear-gradient(100deg, ...)`, rotated with `skewX(-22deg)` so it reads as an angled light streak. It starts at `translateX(-200%)` — completely past the left edge.
- **Translate, don't repaint.** The animation only changes `transform: translateX()` from off-left (`-200%`) to off-right (`320%`). Transform is GPU-composited, so the sweep stays at 60fps and never triggers layout or paint.
- **`overflow: hidden` is the mask.** The band is wider/taller than the button; clipping it to the rounded button is what makes it look like a reflection *on the surface* rather than a floating bar.
- **`mix-blend-mode: screen`** makes the white band *brighten* whatever colour it crosses instead of painting flat white — that is what sells the "gloss". `isolation: isolate` keeps that blend contained to the button so it never bleeds onto the page.
- **Two pseudo-elements, two jobs.** `::after` runs a one-shot keyframe on `:hover`; `::before` (only on `.auto`) loops a long keyframe that sweeps early in the cycle then holds off-screen, producing a glint every ~5s. Splitting them means a hover always glints instantly, regardless of where the auto cycle is.

## Customization

- **Glint speed / softness:** hover duration `.9s` → snappier at `.5s`; widen the gradient's bright stops (`44%–56%`) for a softer, broader sheen.
- **Auto cadence:** the `4.8s` duration and the `16%` keyframe set how often and how fast the idle glint fires. Bigger duration = rarer; smaller percentage = faster swipe.
- **Angle:** `skewX(-22deg)` plus the `100deg` gradient angle control the diagonal — keep them in sync. Use `skewX(0)` for a straight vertical wipe.
- **Intensity:** raise the band alpha (`.85`) for chrome/metal; lower it for subtle premium-matte. Drop `mix-blend-mode` to `normal` for a plain light wipe (and required on light buttons).
- **Restraint:** reserve `.auto` for the single primary CTA. Auto-shining every button turns "premium" into "noisy".

## Accessibility & performance

- **Reduced motion:** `@media (prefers-reduced-motion: reduce)` hides both bands entirely, so users who opted out get a clean static button — the brief's "no sweep" requirement.
- **Cheap:** animates only `transform` on a small pseudo-element; no JS, no layout thrash. One looping `.auto` button is negligible.
- **Pointer-safe:** the band is `pointer-events: none`, so it never intercepts clicks or hover on the real button.
- **Don't rely on it for meaning.** The shine is decoration; keep the label, colour contrast and `:focus-visible` ring doing the actual communicating. Ensure the text/background contrast passes WCAG on its own.

## Gotchas

- **No `overflow: hidden` = no clip.** Without it the skewed band spills outside the button as a floating diagonal bar.
- **Screen-blend vanishes on light buttons.** White screened over a near-white surface produces nothing — switch that variant's band to `mix-blend-mode: normal` (see `.light::after`).
- **Band parks off-screen, not mid-button.** Both keyframes must *start and end* fully off the edges (`-200%` / `320%` for a `55%`-wide band) or you'll see a stray sheen sitting still when idle.
- **`mix-blend-mode` needs a stacking context.** Without `isolation: isolate` (or an equivalent) the blend can sample sibling/page pixels and look wrong over busy backgrounds.
- **Re-triggering hover:** because the hover sweep is a CSS `animation` (not a `transition`), it replays cleanly every time the pointer re-enters and parks itself off-screen between runs — no half-finished band left behind.
