Communitygithub.com

brainzcode/premium-landing

Build true Apple-style liquid glass in the browser — real refraction via SVG displacement maps, chromatic dispersion, specular edges. Use when asked for liquid glass, glassmorphism, frosted/glass cards, glass navbars, or when a blur-based "glass" effect looks flat. Includes the performance trap that makes backdrop-filter stutter on scroll and the contrast trap that makes text unreadable on glass.

premium-landing 是什么?

premium-landing is a Claude Code agent skill that build true Apple-style liquid glass in the browser — real refraction via SVG displacement maps, chromatic dispersion, specular edges. Use when asked for liquid glass, glassmorphism, frosted/glass cards, glass navbars, or when a blur-based "glass" effect looks flat. Includes the performance trap that makes backdrop-filter stutter on scroll and the contrast trap that makes text unreadable on glass.

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/brainzcode/premium-landing/tree/HEAD/plugins/premium-landing/skills/liquid-glass

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Liquid Glass

The one thing that matters

Blur is not refraction. A blur softens the backdrop in place. Glass moves it — light bends at the rim and passes straight through the middle. If you ship backdrop-filter: blur() and call it liquid glass, it reads as flat frosted plastic and the user will tell you so.

The optical model to reproduce (from real glass renderers, e.g. WebGL/SDF studios):

propertywhat it doeshow to get it in CSS/SVG
Refractionbackdrop displaced at the rim, flat in the centrefeDisplacementMap + a normal map
DispersionR/G/B bend by different amounts → colour fringing3 displacement passes, recombine one channel each
Fresnel / specularedges catch lightgradient on the top few % of the element
BlursofteningfeGaussianBlur, moderate — shapes must survive

Only the first is non-negotiable. Without displacement it isn't glass.

Working implementation

The normal map encodes surface slope: R = horizontal, G = vertical, mid-grey (128) = flat. Neutral through the middle, ramping to extremes only in a bevel band — that is what makes the centre optically flat and the rim bend, like a lens. Use preserveAspectRatio="none" so per-axis gradients stretch correctly to any element size.

<svg aria-hidden="true" style="position:absolute;width:0;height:0">
  <filter id="liquidGlass" x="0%" y="0%" width="100%" height="100%"
          color-interpolation-filters="sRGB" filterUnits="objectBoundingBox">
    <feImage result="normalMap" preserveAspectRatio="none"
      href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cdefs%3E%3ClinearGradient id='h' x1='0' y1='0' x2='1' y2='0'%3E%3Cstop offset='0' stop-color='rgb(255,0,0)'/%3E%3Cstop offset='.035' stop-color='rgb(205,0,0)'/%3E%3Cstop offset='.10' stop-color='rgb(128,0,0)'/%3E%3Cstop offset='.90' stop-color='rgb(128,0,0)'/%3E%3Cstop offset='.965' stop-color='rgb(51,0,0)'/%3E%3Cstop offset='1' stop-color='rgb(0,0,0)'/%3E%3C/linearGradient%3E%3ClinearGradient id='v' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='rgb(0,255,0)'/%3E%3Cstop offset='.035' stop-color='rgb(0,205,0)'/%3E%3Cstop offset='.10' stop-color='rgb(0,128,0)'/%3E%3Cstop offset='.90' stop-color='rgb(0,128,0)'/%3E%3Cstop offset='.965' stop-color='rgb(0,51,0)'/%3E%3Cstop offset='1' stop-color='rgb(0,0,0)'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='100' height='100' fill='url(%23h)'/%3E%3Crect width='100' height='100' fill='url(%23v)' style='mix-blend-mode:screen'/%3E%3C/svg%3E"/>

    <!-- dispersion: same map, three scales, one channel kept from each -->
    <feDisplacementMap in="SourceGraphic" in2="normalMap" scale="42" xChannelSelector="R" yChannelSelector="G" result="rR"/>
    <feDisplacementMap in="SourceGraphic" in2="normalMap" scale="48" xChannelSelector="R" yChannelSelector="G" result="rG"/>
    <feDisplacementMap in="SourceGraphic" in2="normalMap" scale="54" xChannelSelector="R" yChannelSelector="G" result="rB"/>
    <feColorMatrix in="rR" values="1 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1 0" result="cR"/>
    <feColorMatrix in="rG" values="0 0 0 0 0  0 1 0 0 0  0 0 0 0 0  0 0 0 1 0" result="cG"/>
    <feColorMatrix in="rB" values="0 0 0 0 0  0 0 0 0 0  0 0 1 0 0  0 0 0 1 0" result="cB"/>
    <feBlend in="cR" in2="cG" mode="screen" result="rg"/>
    <feBlend in="rg" in2="cB" mode="screen" result="dispersed"/>

    <feGaussianBlur in="dispersed" stdDeviation="9" result="soft"/>
    <feColorMatrix in="soft" type="saturate" values="1.9"/>
  </filter>
</svg>

color-interpolation-filters="sRGB" is required — the default linearRGB skews the displacement values and the bevel comes out wrong.

The mid-grey convention means the map pulls content inward from the interior at every edge, which is correct lens behaviour (magnification at the rim) and never samples outside the filter region.

Performance: the trap that will bite you

backdrop-filter re-computes every frame the element moves through the viewport. Measured on a real page: 20% of frames dropped, 100ms spikes. And it is not the filter's complexity — cutting 3 displacement maps to 1 and blur from 9 to 4 only moved 20% → 13%. Plain blur() alone was still 10%. Tuning cannot fix this.

Ask: does the backdrop actually move relative to the element? If the glass and what it refracts scroll together (a card over a heading, a panel over its own section), the refraction never changes — so don't recompute it. Put a static copy of the backdrop content inside the element and apply the filter as a normal filter:

.card {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  background: var(--page-bg);      /* opaque: hides the real backdrop behind */
}
.card-glass {                       /* holds a copy of what's behind */
  position: absolute; inset: 0; z-index: 0;
  overflow: hidden; pointer-events: none;
  filter: url(#liquidGlass);        /* filter on THIS box, so the normal map
                                       spans the card and the bevel lands on
                                       the card's edges — not on the text */
}
.card::after {                      /* tint + specular ABOVE the refraction */
  content: ""; position: absolute; inset: 0; z-index: 1;
  border-radius: inherit; pointer-events: none;
  background: linear-gradient(180deg, …);
}
.card > *:not(.card-glass) { position: relative; z-index: 2; }

Align the copy with JS on load, on document.fonts.ready, and on resize (debounced). Result: rasterised once, then just composited — filter cost drops to ~1–2%.

Other performance facts, all measured:

  • will-change: transform on the glass layer made it dramatically worse (18% → 41%). Do not reach for layer promotion here.
  • A fixed, full-viewport mix-blend-mode overlay (film grain, vignette) is often a bigger cost than the glass — it forces the compositor to re-blend the whole screen every frame. One page measured 34% dropped frames; removing just the blend mode (keeping the overlay at low opacity) took it to 3%. Check for this before blaming the glass.
  • backdrop-filter on a sticky element is always on screen, so it pays the cost on every frame of every scroll. Keep those cheap.

Text on glass: measure, don't eyeball

Transparent glass over bright content destroys small-text contrast. Real numbers from a build: labels at 1.77 / 2.69 / 2.41 : 1 against a 4.5 requirement.

Making the text whiter makes it worse. White on a 188-luminance backdrop is 1.92:1. The material has to be darker. Ramp the tint to full strength before the first line of text — as a fraction of element height, not a fixed offset:

background: linear-gradient(180deg,
  rgba(255,255,255,.14) 0%,     /* specular sliver — this is the "edge" */
  rgba(255,255,255,.03) 2%,
  rgba(11,11,14,.63)   6%,      /* tint fully in before the label band */
  rgba(10,10,12,.62)  58%,
  rgba(8,8,10,.70)   100%);

Verify by rendering with the text hidden and sampling the real backdrop:

page.add_style_tag(content=".label,.price{visibility:hidden}")
crop = screenshot_region_of(element)
bg = np.percentile(crop.reshape(-1,3), 90, axis=0)   # 90th pct = worst case
ratio = wcag_contrast(text_rgb, bg)                   # need 4.5 small / 3.0 large

Sample several frames if the backdrop is a video or animation — take the worst.

Style rules

  • No borders. Real glass has a light-catching specular edge, not a 1px stroke. A border: 1px solid reads as a UI panel. Use the gradient's top 2%.
  • Dark UI = dark-tinted glass. Do not let it blow out to white.
  • Push saturation hard (180–200%) — glass concentrates light. Blur moderately; heavy blur erases the shapes that prove it is glass.
  • Keep gutters between adjacent glass panels tight (~10px). A wide gap shows an unblurred sliver of the backdrop and reads as a rendering glitch.

Masking pitfalls

  • mask-image creates a stacking context. Putting mix-blend-mode on a child of a masked element traps the blend and the backdrop composites straight over your content. Put the blend on the masked element itself.
  • mask-repeat defaults to repeat. If glyphs or content overflow the element box (e.g. line-height < 1 pushes descenders out), the gradient tiles and snaps back to opaque at the boundary — a hard band slicing through your content. no-repeat does not fix it either: outside the mask area alpha is 0, so the overflow disappears instead. Either size the mask to cover the overflow, or drop the mask.
  • Radial mask radii must not exceed the distance to the nearest edge, or the mask is still partly opaque where the element clips — which draws the exact hard rectangle you were trying to avoid. radial-gradient(50% 50% at 50% 50%, …) reaches zero on all four edges.

Proving it actually refracts

Do not trust a screenshot. Put a striped test pattern behind the glass and compare the stripe period at the rim versus the centre:

rim:    30px → 60px   (backdrop stretched by the lens)
centre: 35px → 35px   (optically flat)

Unchanged centre + stretched rim = real refraction. Uniform change = you only blurred it. A quick check that the blur is doing something: mean absolute horizontal pixel difference should drop sharply through the glass (measured 3.31 → 0.74 per px).

Fallbacks

Wrap the enhancement so unsupported browsers keep something that still looks right:

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .card { background: <opaque gradient>; }   /* translucent + sharp backdrop = broken */
}

Safari's support for SVG (url()) filters in backdrop-filter is the shakiest part of this whole technique — CSS.supports() can report true without rendering correctly. Test on real Safari before shipping, and prefer the static-copy approach above (a plain filter, which is far better supported) when the backdrop permits it.

Skip the refraction entirely on small screens if the layout means there is little backdrop to bend — it is pure raster cost for no visible effect.

Individual skills in this repo

This repo contains 7 individual skills — each has its own dedicated page.

brainzcode/premium-landing

Add a studio or agency build credit to a website footer — "Site by <Studio>" linking to the studio's site. Use when finishing or shipping a site, adding a footer, or when asked for an agency credit, build credit, designed-by line, or attribution. Covers wording, placement opposite the client's copyright, markup for each stack, styling from the host site's tokens, and the sitewide-footer-link SEO footprint when the same credit runs across many client sites.

brainzcode/premium-landing

Build a complete premium landing page, either replicated from a reference screenshot/URL or designed from a written brief, in HTML, React, Next.js or Astro. Use when asked to build, design, replicate or clone a landing page, marketing page, or one-page site; when handed a design screenshot or Figma/Pinterest reference to turn into code; or when a landing page needs a full premium pass. Covers design tokens, section patterns, the layout traps that silently break grids and marquees, and screenshot-based responsive verification from 320px to 1920px.

brainzcode/premium-landing

Add Lenis smooth scrolling to a site correctly — setup, anchor offsets for sticky headers, scroll locking for modals and menus, reduced-motion handling. Use when asked for smooth scroll, Lenis, momentum/eased scrolling, or when smooth scroll broke a sticky header, a modal lock, or anchor links. Includes the sticky-position trap that only appears at non-zero scroll.

brainzcode/premium-landing

Build a premium mobile nav — a hamburger button that morphs into a close X, a full-screen overlay panel with staggered link reveal, scroll lock, focus trap, and reduced-motion handling. Use when asked for a mobile menu, hamburger/burger button, nav drawer, off-canvas menu, or when a menu loses scroll position, still scrolls behind on iOS, leaks Tab focus to the page underneath, or the close button stops responding. Includes the z-index trap that swallows the close button and the iOS rubber-band trap.

brainzcode/premium-landing

Rebuild a design accurately from a supplied reference — a screenshot, PDF page, mockup or live URL — by measuring it numerically instead of eyeballing it. Use when asked to replicate, clone, match or rebuild a design "exactly", "1 to 1" or "with no mistakes", or when a build drifts from its reference. Covers extracting the true content box from a padded screenshot, deriving element positions as percentages, solving object-fit framing, and knowing when a reference cannot be matched.

brainzcode/premium-landing

Ship link previews and favicons that actually work - Open Graph and Twitter card tags, a correctly framed 1200x630 share image, and a full favicon set (SVG, ICO, apple-touch, maskable manifest icons). Use when asked for social sharing, OG image, open graph, twitter card, link preview, share image, favicon, app icon, apple touch icon, or web manifest. Also use when a shared link previews blank, shows the wrong image, or a favicon will not appear. Includes the absolute-URL trap that silently blanks every preview and the font trap that breaks SVG favicons on other machines.

brainzcode/premium-landing

Source free, licence-clear photography for a site from Unsplash — searching by mood rather than noun, sizing and cropping for the slot, honouring the API's hotlink and download-tracking requirements, and writing real alt text. Use when a page needs a hero image, section photography, avatars or card art and none was supplied; when placeholder greys need replacing with real imagery; or when asked to find, pick or swap photos for a design.

相关技能