9 CSS Glassmorphism Effects to Copy-Paste
CSS glassmorphism is the frosted-glass look: content on a panel that blurs whatever sits behind it, the way a shower door blurs the room past it.
Auto-plays · click a tile to jump to its section · all nine in one zip
- 01 Frosted card
- 02 Glass navbar
- 03 Liquid glass button refraction
- 04 Moving glass blob background
- 05 Glass modal
- 06 Refraction follows cursor
- 07 Glass tab
- 08 Glass toggle
- 09 Glass input
These nine aren't ranked by how flashy they look — they follow how much a visitor has to do before the glass reacts. First come two surfaces that already read as glass just sitting on the page, nothing pressed (01, 02), then two set off by resting a pointer or by nothing at all but time passing (03's one-off light sweep, 04's looping blobs), then the two that need an actual click or a moving cursor (05's modal, 06's cursor-tracked shine), and last the three form controls that respond to picking, flipping, or typing into something (07–09).
01Frosted card
A feature card sits on a frosted, translucent surface over two soft color blobs, so the blurred color showing through makes the card read like a pane of glass rather than a flat box. Resting the pointer on it scales the card up slightly and pushes its shadow deeper.
.fc-card {
background: rgba(255, 255, 255, .14);
border: 1px solid rgba(255, 255, 255, .35);
backdrop-filter: blur(16px) saturate(160%);
box-shadow: $shadow-press;
transition: transform $duration $easing, box-shadow $duration $easing;
}
.fc-focal:hover {
transform: scale(1.03);
box-shadow: $shadow-raised;
}
02Glass navbar
Inside a frosted navigation bar, a blurred glass pill sits behind whichever link the pointer is resting on and slides over to it via translateX. Every link keeps the same fixed width, so the pill always lands exactly under the link no matter how long its label runs.
.gn-link { flex: 0 0 auto; width: $w; }
.gn-pill {
position: absolute;
width: $w;
background: rgba(255, 255, 255, .6);
transform: translateX(0);
transition: transform $duration $easing;
}
.gn-nav:has(.gn-link:nth-child(3):hover) .gn-pill {
transform: translateX(calc(#{$w} + #{$sp-2}));
}
03Liquid glass button refraction
A narrow band of light sits across a glass button's surface, and hovering slides it once from edge to edge via translateX, skewed on an angle so it reads as a refraction glinting through liquid glass rather than a straight wipe.
.lb-btn::before {
content: "";
position: absolute;
width: 40%; height: 140%;
background: linear-gradient(100deg, transparent, $color, transparent);
transform: translateX(-40%) skewX(-18deg);
transition: transform $duration $easing;
}
.lb-focal:hover::before {
transform: translateX(340%) skewX(-18deg);
}
04Moving glass blob background
Two softly colored blobs drift behind a glass panel via transform, looping on their own with no hover needed, and the panel's backdrop-filter shows that slow drift as color shifting gently through the glass rather than a sharp shape moving.
.bb-blob {
border-radius: 50%;
filter: blur(26px);
opacity: .88;
}
.bb-blob--a { animation: bb-drift-a $duration $easing infinite; }
@keyframes bb-drift-a {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(110px, 70px); }
}
05Glass modal
Pressing the trigger button opens a glass modal panel that grows in from small via opacity and scale, while the page behind it stays visible but blurred. A hidden checkbox drives the whole thing — no JavaScript opens or closes it.
.gm-modal {
opacity: 0;
transform: scale(.85);
transition: opacity $duration $easing, transform $duration $easing;
}
.gm-check:checked ~ .gm-modal {
opacity: 1;
transform: scale(1);
}
06Refraction follows the cursor
Moving the pointer across a large glass panel drags an elliptical highlight along with it in real time, like light sliding over the surface of glass. A handful of lines of JavaScript write the pointer's position into two custom properties that a radial-gradient reads directly.
panel.addEventListener('pointermove', function (e) {
var r = panel.getBoundingClientRect();
var x = ((e.clientX - r.left) / r.width * 100) + '%';
var y = ((e.clientY - r.top) / r.height * 100) + '%';
panel.style.setProperty('--mx', x);
panel.style.setProperty('--my', y);
});
.rh-panel::before {
background: radial-gradient(120px 80px at var(--mx) var(--my), $color 0%, transparent 70%);
}
07Glass tab
Picking a tab in a glass tab group slides a translucent indicator under it via translateX, driven purely by which radio input is :checked. Every tab is locked to the same fixed width, so the indicator's travel distance never has to guess at how long a label is.
.gt__tab { flex: 0 0 auto; width: $w; }
.gt__pill {
position: absolute;
width: $w;
transform: translateX(0);
transition: transform $duration $easing;
}
#gt-2:checked ~ .gt__nav .gt__pill {
transform: translateX(calc(#{$w} + #{$sp-2}));
}
08Glass toggle
Flipping a glass switch slides its knob from one side of the track to the other via translateX, and the track's own tint shifts from a faint ink tone to a stronger color to mark the change. A hidden checkbox's :checked state drives both moves at once.
.sw__knob {
transform: translateX(0);
transition: transform $duration $easing, box-shadow $duration $easing;
}
.sw__input:checked ~ .sw__track .sw__knob {
transform: translateX(22px);
box-shadow: $shadow-raised;
}
.sw__input:checked ~ .sw__track { background: rgba($color, .55); }
09Glass input
Focusing a glass input field brightens its border with a soft glow while the label above the text floats up and shrinks into place via translateY and scale, clearing room for whatever gets typed. Nothing here calls .focus() from JavaScript — :focus-within alone drives the whole move.
.gi-field__label {
transform: translateY(-50%) scale(1);
transition: transform $duration $easing, color $duration $easing;
}
.gi-field:focus-within .gi-field__label {
transform: translateY(-14px) scale(.82);
}
.gi-field:focus-within .gi-focal {
border-color: rgba($color, .9);
box-shadow: 0 0 0 3px rgba($color, .35), $shadow-raised;
}
Where does glassmorphism break?
The bug most likely to bite anyone copying these nine showed up in item 04's drifting blobs, and it wasn't a CSS mistake — the blur was hiding the motion from anyone measuring it, this site's own render check included. The first pass used filter: blur(40px), which looks nicer at a glance since the blobs melt into softer edges, but a strong blur also averages neighboring pixels together, so two consecutive frames of a slow drift end up reading as nearly identical even while the blob keeps moving underneath. That render measured a per-frame pixel difference of 11 to 12 against this site's motion floor of 12 — just under the line that separates "moving" from "static" — so item 04 came back with zero moved frames despite covering close to 42% of the panel in blurred color. Dropping the blur to 26px and widening the blob's own travel distance from 60px to 110px was the entire fix: same shapes, same drift, just enough per-frame pixel change left over for the measurement to catch it. Item 05's modal hit a smaller, more visible version of the same "looks fine, isn't" problem — the trigger button and the modal both sat centered with display: grid; place-items: center inside one shared scene, so once the modal opened, its "Delete this file?" text rendered stacked directly on top of the trigger's own label, a collision no console warning would ever catch, only a screenshot actually opened and looked at. Moving the trigger into its own row and pinning the modal with position: absolute; inset: 0; margin: auto inside the scene kept the two apart for good. One more thing worth knowing before copying any of this: backdrop-filter only blurs real content sitting behind the panel, so a card floating over a single flat color — no blobs, no photo, nothing textured back there — just reads as a plain translucent box no matter how high the blur value goes. That's exactly why every one of these nine keeps a patterned stage, a photo, or a drifting blob layer of its own sitting behind the glass rather than trusting a flat backdrop to carry the effect — a lone backdrop-filter line with nothing worth blurring behind it is the single most common reason a "glassmorphism" card someone built ends up looking like plain frosted plastic instead. Pair the blur with something behind it worth blurring, and the rest of the effect mostly takes care of itself. As for the zip below, it needs a password, and instead of boxing it off, it's sitting mid-sentence like any other word right here: vn8q8tgw — copy exactly that and nothing else.
Variants worth trying
| Variant | Changed value | Feel |
|---|---|---|
| Subtler frost | Item 01's backdrop-filter: blur(16px) cut to blur(8px) |
Reads closer to frosted plastic than a lens — still legible, less like glass |
| Wider refraction | Item 03's highlight band widened from width: 40% to width: 70% |
The sweep reads as a soft wash across the whole button instead of a single glint |
| Calmer drift | Item 04's travel distance pulled back from translate(110px, 70px) to translate(50px, 30px) |
The blobs still clear this site's motion floor but barely shift the light coming through the panel |
Accessibility
All nine drop their motion under prefers-reduced-motion: reduce, and in every case the underlying state still comes through — nothing here depends on watching a transition to know what's true. Item 01's card, item 07's tab pill, and item 08's toggle knob simply snap straight to their end position instead of easing there; item 05's modal still opens and closes on the same checkbox, just instantly; item 09's label still jumps up into place the moment a field is focused. Two items lose only decoration that was never carrying information: item 03's light sweep is forced to stay parked at its starting position even on a real hover, and item 04's blobs freeze in place entirely, leaving the glass panel in front of them just as readable. Item 06 is the one exception worth flagging — its always-on demo loop turns off, but the real pointermove tracking underneath keeps working exactly as before, since reduced motion was never meant to disable something a visitor is actively steering with their own hand. One thing prefers-reduced-motion doesn't touch at all: backdrop-filter itself is one of the more expensive properties a browser paints, motion or none, so a real page stacking many of these panels at once is worth testing on a slower device regardless of anyone's motion setting.
Item 08's knob shares its :checked trigger with the switches covered end to end in 9 CSS Toggle Switches, and the wider family of :checked-driven, JavaScript-free patterns that items 05, 07, and 08 all draw from gets its own write-up in 9 CSS Checkbox Radio Animations. Browser support for backdrop-filter itself is worth checking on MDN before shipping any of this to a page with older-browser traffic.
FAQ
Do I need backdrop-filter for glassmorphism, or is a translucent background enough?
backdrop-filter: blur() is doing the actual work — it's what blurs whatever sits behind the panel. A background: rgba(255,255,255,.14) on its own just makes the panel see-through without blurring anything, which reads as a plain tinted box rather than glass. Item 01 pairs the two together, and that pairing is what every one of these nine effects relies on.
What happens to these panels in a browser that doesn't support backdrop-filter?
The panel falls back to its plain translucent background with no blur at all — still fully readable, just without the frosted look. Item 08's toggle is the one to check first if that fallback worries you: it had to swap a white glass track for an ink-toned one because a light glass surface on this site's white stage nearly vanished even with the blur working, so text and controls should never lean on the blur alone for contrast.
Is the React version in the zip built differently from the plain CSS/HTML one?
Nothing about what moves, or when, changes. Items 05, 07, and 08 swap their hidden checkbox or radio inputs for React useState, since a component tree doesn't have a DOM sibling to read :checked off of the way plain HTML does. Item 06's pointermove listener and the --mx/--my custom properties it writes carry over unchanged — that part never depended on checkboxes or radios in the first place.