Effects
Layered shadows, multi-stop gradients, and color filters.
.glossy-button {
position: relative;
box-shadow:
inset 0 1px 0 #ffffffaa,
0 1px 2px #ffffff99,
0 8px 24px #5b321f55;
outline: 2px solid #fff4ad;
outline-offset: 3px;
}
.glossy-button::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, #ffffff66, transparent);
}Up to eight comma-separated mixed inset and outer box-shadow layers are
supported. The first declared shadow is painted on top. Solid outlines and
quoted pseudo-element content are also supported. See
Backgrounds and gradients for background/
background-image fills, including linear, radial, and conic gradients.
An outline paints above its owner's background, border, inset shadows, and child content, while later siblings retain their normal paint order.
Static effect tokens can reference other tokens, including forward references:
--focus-color: var(--palette-amber) and shadow lists containing var() are
resolved before effect validation. Unresolved or cyclic references without a
valid fallback produce a compiler diagnostic; expansion is bounded to 64 levels
and 64 KiB per value.
Negative outline-offset values put the ring inside the control without changing
its layout size; they are useful for keyboard and gamepad focus indicators.
Shadows respect ancestor overflow clips, including nested rounded clips. A fully clipped element contributes no shadow coverage outside a scrolling container.
text-shadow supports up to twelve comma-separated layers with a
blur-radius of 0 to 16px (spread is rejected). A blurred shadow is not
a blur pass: both the WebGL2 and Unreal renderers expand it into a shared,
deterministic 13-tap kernel around the already-shaped glyphs, so the two
backends stay pixel-parity without a render target. Like in CSS, text-shadow
is inherited: a halo declared on a container reaches the text of every
descendant that declares no text-shadow of its own, and text-shadow: none
on a descendant stops it for that subtree.
An element whose transform is not invertible - scale(0, 1) at the start of a
card flip, scaleX(0) on an empty meter - is not painted at all, together with
its content, as the CSS Transforms specification requires. Its shadows, blurs
and outlines do not leave a strip behind.
filter supports color-only per-primitive operations. Foreground blur and SVG
filters remain capability gaps.
filter: grayscale(<number 0..1 | percentage>) (PaintFrame 1.12) is accepted
on any element and desaturates everything that element and its descendants
paint, like a browser does: background fills and gradients, borders, text,
box shadows, backdrop blurs, host textures (asset://...), <Icon> Lucide
rasters, vector icons and nine-slice patches. Nested filters combine by max (a
descendant cannot undo an ancestor's grayscale, and filter: none on a child
under a grayscale(1) parent still paints grey), the amount clamps to 0..1,
percentages divide by 100, and 0 is a no-op. Both renderers mix each painted
colour with its Rec. 709 luma at draw time, alpha untouched, so a currentColor
icon desaturates through its colour exactly like a texture does through its
texels, and a colour transition under the filter animates in grey. The amount
comes from both the cascaded style (a stylesheet rule or an inline style
attribute) and interactive pseudo-state rules, so
.hero-button:disabled { filter: grayscale(1) } greys a whole disabled button
- label, icon, gradient face, rim and shadow - with one declaration. Under the
hood the amount rides on the same per-element scope as
opacity(PushOpacityCommand.Grayscale); the PaintFrame 1.11 per-image field is kept for compatibility, so a renderer that only reads it still desaturates images.
A border can mix colors per side, for example a uniform border: 1px solid
with a single accent side (border-left: 4px solid #d89b35) as a tone stripe
on a card (PaintFrame 1.10, ADR-0053). One quad still paints the whole ring;
the renderer picks each pixel's owning side by the same closed-form miter
comparison CSS uses at corners, so an animated border-color transition
recolors only the sides that were not explicitly overridden. Every side still
renders solid - per-side border style is not supported.
Background blur
Use backdrop-filter: blur(12px) on a panel to sample and blur previously painted
content before painting its own background, text and controls. none disables it.
The initial range is 0–64 CSS pixels; nonzero values require px. Both renderers
use a two-pass Gaussian filter with rounded bounds and the active clip chain.
Studio samples its preview canvas; Unreal samples the composited scene colour and
every UI command already painted at that point, including earlier siblings
inside the same opacity group (the group's own opacity is applied once, when
the group itself composites onto its parent - it is not applied twice to a
descendant's backdrop sample). This is not equivalent to a darker overlay or
foreground filter: blur().
The command requires PaintFrame 1.7. Packages declaring backdrop filtering carry a required capability bit, so an older runtime rejects them rather than silently dropping the effect. Existing packages remain readable. Large full-screen blurred areas cost additional render-target memory and GPU passes; avoid unnecessary stacked full-screen backdrops.