Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions app/docs/theming/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ReactNode } from "react";
import { DiffuseComparisonDemo } from "@/components/diffuse-comparison-demo";
import { ForegroundTester } from "@/components/foreground-tester";
import { GlassPrebakedDemo } from "@/components/glass-prebaked-demo";
import { StainedComparisonDemo } from "@/components/stained-comparison-demo";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { VeilComparisonDemo } from "@/components/veil-comparison-demo";
Expand Down Expand Up @@ -835,6 +836,25 @@ npx shadcn@latest add @sistine/gradient-background # pure-CSS gradient wallpape
prop, with <IC>angle</IC>, <IC>position</IC>, and (radial) <IC>radialShape</IC> / <IC>radialSize</IC> geometry, e.g.{" "}
<IC>&lt;GradientBackground shape=&quot;conic&quot; position=&quot;30% 70%&quot; /&gt;</IC>.
</p>
<p className="text-muted-foreground">
A list of glass surfaces can skip the live <IC>backdrop-filter</IC>. <IC>glass-prebaked</IC> (the <IC>prebaked</IC> axis) paints a
viewport-sized copy of <IC>--app-bg-blurred</IC> behind each surface, shifted by that surface&apos;s screen position.{" "}
<IC>useGlassBackdrop</IC> writes the offsets from one animation frame. <IC>&lt;GradientBackground&gt;</IC> and the solid backdrop
publish the variable and set <IC>--app-bg-filter</IC> to <IC>blur(12px) saturate(180%)</IC>. A pre-blurred image is the cheaper source:
set <IC>--app-bg-blurred: url(bg.webp) center / cover</IC> and leave the filter unset. Pattern scenes and the canvas are not a single
image, so they stay on the live filter. Keep the bubble in normal flow. A transform, a filter, an opacity, or{" "}
<IC>isolation</IC> on it paints the slice in front of the glass fill.
</p>
<Code>{`import { useGlassBackdrop } from "@/hooks/use-glass-backdrop"
import { glassMaterial } from "@/lib/material"

const scrollRef = useRef<HTMLDivElement>(null)
useGlassBackdrop(scrollRef)

<div ref={scrollRef}>
<div className={glassMaterial({ border: true, prebaked: true }).className}>…</div>
</div>`}</Code>
<GlassPrebakedDemo />
</CardContent>
</Card>
</div>
Expand Down
13 changes: 13 additions & 0 deletions app/theme/registrations.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@
inherits: false;
}

/* Prebaked-frost offsets (hooks/use-glass-backdrop.ts writes these per bubble). Non-inheriting so a
nested bubble never samples its parent's screen position for a frame before its own sync. */
@property --gx {
syntax: "<length>";
inherits: false;
initial-value: 0px;
}
@property --gy {
syntax: "<length>";
inherits: false;
initial-value: 0px;
}

/* Gradient-text tokens are consumed ONLY from JS inline styles (components/gradient-text.tsx) — no CSS
rule references them, so Lightning CSS's optimizer would silently drop them (the same failure mode as
the harmonic tokens below). Registered here to pin them. DO NOT REMOVE. */
Expand Down
53 changes: 53 additions & 0 deletions app/theme/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,56 @@
--foreground-ui: var(--foreground-ui-opaque);
--muted-foreground: var(--muted-foreground-opaque);
}

/* ── Prebaked frost ───────────────────────────────────────────────────────────
A stand-in for backdrop-filter when many surfaces are on screen at once (a message list). Each
`.glass-prebaked` paints a viewport-sized copy of the page background, shifted by its own screen
position (--gx / --gy, written by useGlassBackdrop), and clips that copy to its box. A plain
`filter` on that copy blurs pixels the element already owns. backdrop-filter would re-sample
whatever is painted behind every bubble, every frame.

UNLAYERED on purpose, and it MUST stay that way:
- `@utility` rules live in `@layer utilities`. An unlayered [data-material] / [data-glass] rule
beats a layered custom property no matter the specificity, so `--srf-filter: none` inside an
@utility would lose and the live blur would stay on.
- `backdrop-filter: none` here replaces the property `glass` sets, including the --srf-stain
appendix. Stained optics are a backdrop-filter color matrix; they do not carry over.
- `position: relative` and `overflow: hidden` are part of the trick (the slice is 100vw × 100vh
and has to be clipped, and the bubble has to be the slice's containing block). Unlayered, they
beat `absolute` / `fixed` / `sticky` / `overflow-*` utilities on the same element. Keep this
class on in-flow bubbles. It does not belong on a sticky bar, a popover, or a scrolling panel.
- No `isolation: isolate`. A stacking context paints its own background FIRST and negative
z-index children AFTER it, so the slice would cover the glass fill and the tint would vanish.
Without one, z-index -1 sits behind this element's background and in front of the page
backdrop (z-index -10). A transform, filter, or opacity on the bubble itself re-traps the
slice above the fill — same failure — so don't put those on the bubble.
- `glass-sheen` paints its own ::before / ::after highlights. This slice owns ::before; the two
classes do not combine.

The app supplies the pixels:
- `--app-bg-blurred`: a pre-blurred image (`url(bg.webp) center / cover`) or the same CSS
background the page paints. GradientBackground and the solid "none" backdrop publish it.
- `--app-bg-filter`: `blur(12px) saturate(180%)` when that background is NOT already blurred.
Defaults to none, so a pre-blurred image is not blurred a second time.
Pattern scenes and the canvas are many layers, not one background, so they do not publish the
variable. A bubble with this class and no variable loses the live blur and shows only its fill. */
.glass-prebaked {
--srf-filter: none;
backdrop-filter: none;
position: relative;
overflow: hidden;
}
.glass-prebaked::before {
content: "";
position: absolute;
z-index: -1;
width: 100vw;
height: 100vh;
left: calc(var(--gx, 0px) * -1);
top: calc(var(--gy, 0px) * -1);
background: var(--app-bg-blurred);
background-repeat: no-repeat;
filter: var(--app-bg-filter, none);
border-radius: inherit;
pointer-events: none;
}
Loading
Loading