Cards✨ Premium

Prismatic Refraction Card

A 340 px glass pane over four blurred color blobs: backdrop-filter blur 18 px + saturate 250%, two red and cyan borders offset up to ±4.2 px with the pointer (the chromatic aberration), a rainbow band translated up to ±27% and a ±10° / ±7° tilt, all smoothed by a rAF loop that stops at rest and wakes on pointer or keyboard. Measured: the white text drops to 1.4–3.9:1 on the glass.

CSSbackdrop-filterPointer

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Cards has 41 effects, including 6 free. Explore the category →

3 usage examples

Feature card — landing page hero — Prismatic Refraction Card example 1

① Feature card — landing page hero

WhenProduct hero: a glass card floating over light blobs, a title and two lines of text.
WhyThe whole scene is the pointer zone: the card reacts before the visitor touches it, and the blobs act as the light source the glass distorts. This is the code as sold — white text on the glass, measured contrast 1.4 to 3.9:1.
SettingsCode as sold, the scene fills the block (height: 100%; aspect-ratio: auto), 340 px card. To do before going live: the content scrim described under accessibility.
Pricing card — premium plan — Prismatic Refraction Card example 2

② Pricing card — premium plan

WhenPricing page: badge, 52 px price, three perks, a button — the plan card to highlight.
WhyThe sold CSS already contains these classes (.prc-card-wrap--pricing, .prc-glass--pricing, .prc-badge, .prc-price-*, .prc-features, .prc-cta) with no HTML to carry them: here they are in use. Glass says “premium” without shouting; a price has to stay readable.
SettingsShipped pricing classes (card min(64%, 288px)), rgba(8,10,28,.6) scrim under .prc-content (title ≥ 6.4:1, perks ≥ 5.4:1 on the lightest measured pixel), blobs at opacity .8.
KPI tile — highlighted metric on a dashboard — Prismatic Refraction Card example 3

③ KPI tile — highlighted metric on a dashboard

WhenDashboard: one metric, its change, a mini bar chart, in a 260 px tile.
WhyOn a small tile, tilt and fringes are enough to bring it alive; the smoked glass keeps the figure readable without a visible scrim and the toned-down blobs don't compete with the data.
Settings260 px card, smoked glass (background: linear-gradient(135deg, rgba(8,10,28,.55), rgba(8,10,28,.4)), saturate(160%) brightness(.8)), blobs at opacity .62, CA_SCALE 2.5 and TILT_X / TILT_Y 8 / 12 in the JS.

How it works

The scene: .prc-scene (width: 100%; aspect-ratio: 16 / 10, border-radius: 14px; overflow: hidden; isolation: isolate; perspective: 1000px, #0b1020 background, crosshair cursor) holds .prc-bg (inset: -14%) and its four .prc-blobs — discs 46% wide (#ff2050, #2855ff, #00d4aa, #bf38ff), filter: blur(34px), opacity .88, animated by prc-float over 20 s (±10% translation, scale .93 to 1.10, offsets 0 / −6 / −13 / −9 s). They exist so the glass has something to refract. .prc-card-wrap (min(72%, 340px), will-change: transform) receives the tilt and contains .prc-ca then .prc-glass.

The glass: background: linear-gradient(135deg, rgba(255,255,255,.16), .05), 1 px white border at 22%, four shadows (0 16px 56px .40, a light inner hairline inset 0 1px 1px .56, an inner penumbra inset 0 -2px 10px .20 and a green halo 0 0 3px rgba(0,255,130,.16)), and backdrop-filter: blur(18px) saturate(250%) brightness(1.06), prefixed -webkit-. ::before = static bevel (128° gradient, white .26 → transparent at 65%); ::after = specular radial-gradient(115px 95px at var(--mx) var(--my), white .40 → .08 → transparent) in screen. .prc-prism (210% × 210%, top/left: -55%) is a 108° gradient of seven colored bands at 14–22% alpha between 26 and 70% of its width, in screen, opacity --prc-prism-opacity (.78); the glass's overflow: hidden shows only a window of it, which the JS moves.

The chromatic aberration: .prc-ca (inset: 0; border-radius: 28px) carries ::before, a 1.8 px rgba(255,32,62,.82) border with a glow, translated by (--prc-ca-x, --prc-ca-y), and ::after, the same in cyan rgba(0,210,255,.82) translated the opposite way (calc(var() * -1)). Both have transition: transform .14s ease-out, on top of the JS smoothing. The wrapper receives the variables: cax = (lx − .5) × 8.4 → ±4.2 px, cay = (ly − .5) × 4.2 → ±2.1 px. The IDLE rest position (.66, .34) is off-center: without interaction, the fringes are already offset by +1.34 / −0.67 px and the card tilted 3.2° / 2.2°.

The script reads four ids (prcScene, prcWrap, prcGlass, prcPrism) and bails out if one is missing — a single card per page, plus an optional second set of ids (…Ex). pointermove on the scene normalizes the position (0 to 1, clamped); tick() moves 7.2% of the way per frame (95% in ≈ 40 frames, ≈ 670 ms at 60 Hz) and calls applyCard: rotateX((.5 − ly) × 14) rotateY((lx − .5) × 20) on the wrapper, the fringe variables, --mx/--my in % on the glass, translate((lx − .5) × 54%, (ly − .5) × 54%) on the band. tick() only reschedules itself while the gap between target and smoothed value exceeds .0005 on either scene; otherwise the loop stops (rafId = 0) and reveiller() restarts it on pointermove, pointerleave and every arrow key — at rest, no rAF call. On the keyboard, arrows move the target by .16, Escape and Home recenter it. No touchmove listener: pointermove already covers touch, and page scrolling stays free. The file also ships nine getElementById calls for review sliders (prcIntensity, prcTilt, prcA11y…) and ~105 lines of CSS for a pricing card (.prc-glass--pricing, .prc-price-*, .prc-features, .prc-cta) that no sold HTML uses.

Accessibility

  • Insufficient contrast as shipped — measured in Chromium (text rendered transparent, pixels sampled under the text, three pointer positions and three moments of the blob cycle): title #fff 20 px bold, minimum 1.35:1, median 2.6 to 3.05:1, maximum 3.9:1 — below the 3:1 large-text threshold on most pixels; body rgba(255,255,255,.88) 13.5 px, minimum 1.21:1, median ≈ 2.5:1 (4.5:1 threshold). The text-shadow: 0 1px 12px rgba(0,0,0,.55) doesn't change the minimum (1.57). Fix computed on the lightest measured pixel (rgb(26,249,218)): .prc-content { background: rgba(8,10,28,.6); padding: 14px 16px; border-radius: 14px } → title 6.4:1, body 5.4:1 (at .55 alpha: 5.3 / 4.5). Alternative: darken the glass itself (brightness(.7), rgba(8,10,28,.5) background).
  • Reduced motion: the blobs freeze, the fringe transitions go, tilt and band are disabled; but LERP becomes 1 and the specular still follows the pointer — no more animation, but still motion. Reduced transparency: opaque #13162e glass, band hidden — the text then reaches 17.8:1.
  • Roles: .prc-glass has role="img", tabindex="0" and reacts to the arrow keys — an image isn't interactive: screen readers announce an image and the keyboard hint in the label (shipped in French) means nothing to them. Use role="group" with the label, or drop tabindex if keyboard driving isn't wanted.
  • Touch: no touchmove listener, the page scrolls normally when the finger crosses the card; Pointer Events (pointermove) drive the effect with a finger, and the scene is width: 100% — the card spans the phone's full width. If you would rather have the finger drive the card without scrolling the page, set touch-action: none on .prc-scene.
  • Focus: .prc-glass:focus-visible draws outline: 3px solid rgba(129,140,248,.88) at 4 px. Performance: four 34 px-blurred blobs animating permanently and a backdrop-filter recomputed every frame — heavy on a modest GPU, even with the rAF loop stopped (it only runs during the smoothing, about 80 to 100 frames after the last gesture — computed, ≈ 1.5 s at 60 Hz). To lighten it, reduce the blobs' blur or freeze them outside hover.

Browser compatibility

Requires backdrop-filter (-webkit- prefix provided), aspect-ratio, inset, mix-blend-mode, calc() on variables and Pointer Events. No library.

Chrome 88+✓ Full
Firefox 103+✓ Full
Safari 15+✓ Full
Edge 88+✓ Full
Mobile iOS 15+✓ Full
Android Chrome✓ Full

Without backdrop-filter (Firefox before 103), the glass is just a white gradient at 16 → 5%: the blobs stay sharp behind it, the glass effect is gone, fringes and rainbow band remain. Without JavaScript, everything stays at the CSS variables' rest position (fringes offset 2.5 / 0.8 px, specular at 32% / 22%), nothing follows the pointer; the blobs keep floating.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<div class="prc-scene" id="prcScene">
  <div class="prc-bg" aria-hidden="true">
    <span class="prc-blob prc-blob--1"></span>
    <span class="prc-blob prc-blob--2"></span>
    <span class="prc-blob prc-blob--3"></span>
    <span class="prc-blob prc-blob--4"></span>
  </div>
  <div class="prc-card-wrap" id="prcWrap">
    <div class="prc-ca" aria-hidden="true"></div>
    <div class="prc-glass" id="prcGlass" tabindex="0" role="img" aria-label="Glass card with prismatic dispersion — move the mouse or use the arrow keys">
      <div class="prc-prism" aria-hidden="true" id="prcPrism"></div>
      <div class="prc-content">
        <h3 class="prc-title">Prismatic Glass</h3>
        <p class="prc-body">The edges split light into red and cyan. Move the cursor to sweep the rainbow.</p>
      </div>
    </div>
  </div>
</div>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
CA_SCALE (JS) 4.2 Maximum fringe offset: ±4.2 px in x (× 2), ±2.1 px in y. 8 = heavy prism; 1.5 = subtle. The two channels move apart symmetrically.
TILT_X / TILT_Y (JS) 14 / 20 Tilt amplitude in degrees (±7° / ±10°). TILT_MULT is a multiplier left over for a review slider — useless in production.
LERP (JS) 0.072 Fraction of the remaining distance per frame. .15 = responsive (~300 ms); .04 = slow (1.2 s). Under prefers-reduced-motion it is 1 (instant).
IDLE_X / IDLE_Y (JS) .66 / .34 Rest position (0 to 1): fringes and specular already offset without interaction, card tilted 3.2° / 2.2°. .5 / .5 = card perfectly face-on at rest.
Prismatic band (CSS — .prc-prism, --prc-prism-opacity) seven bands, opacity .78 Colors at 14–22% alpha between 26 and 70% of the band's width; JS travel ±27% ((lx − .5) × 54). .4 for a discreet rainbow; 0 removes it without touching the HTML.
Glass (CSS — .prc-glass backdrop-filter) blur(18px) saturate(250%) brightness(1.06) The 250% saturate makes the blobs blaze behind the glass — it's what crushes the contrast. saturate(140%) brightness(.8) gives smoked, more readable glass.
Blobs (CSS — .prc-blob: colors, blur 34px, opacity .88, 20 s) #ff2050, #2855ff, #00d4aa, #bf38ff The light the glass refracts. Swap in your brand colors; on an already colored page background, remove .prc-bg — the glass then filters whatever is behind it.

FAQ

Two, yes; more, not without editing the script. It initializes on four fixed ids (prcScene, prcWrap, prcGlass, prcPrism) and provides a second hard-coded set (prcSceneEx, prcWrapEx, prcGlassEx, prcPrismEx) — the catalog page's usage example. For N cards, replace the getElementById calls with document.querySelectorAll('.prc-scene').forEach(init) using scene.querySelector('.prc-card-wrap') etc., and one state (mx, my, tx, ty) per scene.
No, it's measured: title 1.35 to 3.9:1, body 1.2 to 3.4:1 depending on the pointer position and the moment in the blob cycle, drop shadow included. The cause is twofold: a translucent white glass (16 → 5%) and saturate(250%), which makes the cyan and pink blobs very bright behind it. The least visible remedy is a scrim under the content alone, rgba(8,10,28,.6): title 6.4:1 and body 5.4:1 on the lightest measured pixel. Otherwise, bring the saturate down to 140% and the brightness to .8.
Partly. The tick() loop stops on its own: it only reschedules itself while the gap between the target and the smoothed value exceeds .0005 (on the main card or on the example), then rafId goes back to 0 and reveiller() restarts it on the next pointermove, pointerleave or arrow key — at rest, no requestAnimationFrame call. What keeps running: the four blurred blobs, animated in CSS over 20 s, and the glass's backdrop-filter, recomputed on every frame where something moves behind it. Under prefers-reduced-motion, the blobs freeze (animation: none) and the loop, with LERP at 1, stops on the very next frame.