Bubble Glass Grid
Twelve 51 px glass bubbles in a four-column grid: the hovered bubble swells to ×1.35 as a rounded square (peaking at ×1.38 with a spring), its two neighbors in HTML order go to ×1.1 thanks to the :has() selector — pure CSS, zero JavaScript, on a declared background that the glass blurs.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Glassmorphism has 47 effects, including 6 free. Explore the category →
3 usage examples



How it works
One grid, twelve divs, four rules. .lg-bubble-grid sets grid-template-columns: repeat(4, 1fr), gap: 6px and width: 220px: each bubble is 50.5 px (measured 51 × 51 in Chromium, Firefox and WebKit), three rows, a 256 × 200 px grid including its frame. Each .lg-bubble is a glass disc (aspect-ratio: 1, border-radius: 50%): a white radial gradient from 25% to 5% lit at 35% / 30%, a white rim at 15%, backdrop-filter: blur(8px), a drop shadow and an inner highlight. On hover, transform: scale(1.35), border-radius: 40% — the disc becomes a square with very round corners —, a stronger violet-tinted highlight and z-index: 2 to move in front of its neighbors.
The signature lives in one rule with two selectors: .lg-bubble:hover + .lg-bubble targets the next bubble, .lg-bubble:has(+ .lg-bubble:hover) the previous one — a “previous sibling selector” that only :has() makes possible. Both go to scale(1.1) and border-radius: 45%. Measured while hovering the sixth bubble: the fifth and seventh at ×1.1, the sixth at ×1.35, the others still. Neighborhood follows HTML order, not geometry: hover the fourth (end of the first row) and it is the fifth, first of the next row, that reacts; the bubbles above and below do not move. The .5s cubic-bezier(.34, 1.56, .64, 1) transition overshoots its target: measured, ×1.384 around 280 ms then back to ×1.35 before 500 ms, in all three engines. The grid “breathes” around the pointer without a line of script.
The grid declares its own background, because glass only exists if it has something to blur: set over a flat color (a page's, or a demo frame's #0a0a0f), the bubbles would just be gray discs, as an 8 px blur over a flat color does not show. Hence box-sizing: content-box, padding: 18px, border-radius: 26px and four sharp-edged dots (pink, blue, yellow, violet) on an indigo gradient. Between the bubbles the dot edges stay sharp; under each bubble they soften: that contrast is what makes the glass read. Bubbles keep their 51 px, the grid is 256 px wide with its padding.
Accessibility
- Keyboard and touch: twelve
divs with notabindexor role, and an effect limited to:hover. With a keyboard nothing is reachable; with a finger, a tap triggers hover, which sticks until the next tap elsewhere. For a choice grid, use<button>s (or styled<input type="radio">s) and add:focus-visibleto every selector:.lg-bubble:is(:hover, :focus-visible),.lg-bubble:is(:hover, :focus-visible) + .lg-bubbleand.lg-bubble:has(+ .lg-bubble:is(:hover, :focus-visible)). - No prefers-reduced-motion: under emulation, the bubble still rises to ×1.38 then settles at ×1.35 in 0.5 s. The motion only starts from a user gesture, but the bounce is what bothers sensitive users:
@media (prefers-reduced-motion: reduce) { .lg-bubble { transition-timing-function: ease; transition-duration: .15s } }keeps the swell without the spring. - Screen readers: the bubbles in the sold code are empty, nothing is announced. As soon as they carry a label (flavor, mood, shade), give the grid
role="group"and anaria-label, and each bubble real text rather than a color alone — color cannot be the only information (WCAG 1.4.1). - Contrast: the glass is white at 5-25% over the grid background; text placed in a bubble reads against the blurred color underneath. The scenes on this page use white text with
text-shadow: 0 1px 4px rgba(0,0,0,.55): it holds on mid and dark tones, it weakens on the lightest ones (vanilla, coconut) — switch the text to dark on those cells or darken the dot.
Browser compatibility
CSS only: grid, aspect-ratio, backdrop-filter, :has(), transitions. Zero JavaScript, zero dependencies. Measured in Chromium 151, Firefox 153 and WebKit 26: same sizes (51 px bubbles), same swell (×1.35, peak ×1.384), same neighbors at ×1.1. backdrop-filter is declared with and without prefix (-webkit-backdrop-filter), which covers Safari before version 18. The sensitive point is :has(): Chrome and Edge 105, Safari 15.4, Firefox 121. A browser that does not know it drops the whole rule, both selectors included — measured by replacing :has() with an unknown selector, the rule is not kept in any of the three engines: the hovered bubble still swells, but no neighbor reacts.
Without :has() (Chrome < 105, Firefox < 121, Safari < 15.4), the neighbor rule is dropped entirely: only the hovered bubble swells. To keep at least the next neighbor, split the rule in two, one per selector. Without backdrop-filter, bubbles remain translucent discs over the grid background, unblurred. Without aspect-ratio (before 2021), bubbles collapse to their content height: give them a fixed height.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<div class="demo-preview">
<div class="lg-bubble-grid">
<div class="lg-bubble"></div>
<div class="lg-bubble"></div>
… (12 bubbles)
</div>
</div>
The full HTML + CSS + JS of Bubble Glass Grid, ready to paste — plus all 840 effects. Or $299 once, lifetime access.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Grid size (CSS — .lg-bubble-grid { width: 220px }) |
220 px of content, 50.5 px bubbles | Bubble = (width − 3 × gap) ÷ 4. 270 px gives 63 px bubbles, 400 px gives 96 px. |
Number of columns (CSS — grid-template-columns: repeat(4, 1fr)) |
4 columns, 12 bubbles over 3 rows | Neighbors are HTML neighbors: with 3 columns, bubble 3 triggers bubble 4, first of the next row. Add min-width: 0 to the bubbles if they carry text, otherwise a long word widens its column. |
Swell (CSS — .lg-bubble:hover { transform: scale(1.35); border-radius: 40% }) |
×1.35, rounded square at 40% | scale(1.2) for a tight grid, border-radius: 50% to keep the disc. Beyond ×1.5 the bubble covers its neighbors widely: increase the gap. |
Neighbor reaction (CSS — .lg-bubble:hover + .lg-bubble, .lg-bubble:has(+ .lg-bubble:hover)) |
previous and next neighbor at ×1.1, radius 45% | To make neighbors recede instead of growing: scale(0.92). To reach the second neighbor too: .lg-bubble:hover + .lg-bubble + .lg-bubble and .lg-bubble:has(+ .lg-bubble + .lg-bubble:hover). |
Spring (CSS — transition: .5s cubic-bezier(.34, 1.56, .64, 1)) |
0.5 s, peak ×1.384 around 280 ms | The 1.56 creates the overshoot. cubic-bezier(.34, 1.2, .64, 1) softens it, ease removes it. The same transition plays on the way back: the bubble you leave bounces too. |
Background behind the glass (CSS — .lg-bubble-grid { background: … }) |
4 sharp dots on an indigo gradient, 18 px frame rounded at 26 px | Replace it with a photo (url(…) center / cover) or one dot per cell as in the scenes. A flat background makes the blur invisible: keep sharp edges or detail under the bubbles. |
Glass blur (CSS — backdrop-filter: blur(8px)) |
8 px | 4 px let the drawing under the bubble show through, 14 px keep only its color. Change the value with and without the -webkit- prefix. |
FAQ
z-index: 2): it covers its neighbors, it does not fuse with them. A real merging effect needs a “gooey” SVG filter (blur then alpha threshold) applied to the whole grid, and that filter destroys the glass: the threshold wipes out the translucent gradient and the highlight. The two effects do not combine.+ means the next one, :has(+ …) the previous one. The grid draws rows, but the HTML is a sequence of twelve elements; for bubble 6 the neighbors are 5 and 7, and for bubble 4 they are 3 and 5 — 5 being at the start of the next row. To target the bubble below in a 4-column grid, skip three siblings: .lg-bubble:hover + * + * + * + .lg-bubble, and .lg-bubble:has(+ * + * + * + .lg-bubble:hover) for the one above. It works as long as the column count does not change.backdrop-filter blurs what is behind the element; over a flat color the result is the same flat color. That is why the grid declares its own background (sharp dots on a gradient): if you remove it to place the grid over your page, the page needs detail behind the bubbles — photo, illustration, pattern. Also check that no ancestor of the grid has a filter or an opacity below 1: they create a new root for the blur, which then no longer sees the page background.