CalmUI
Styling

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.

On this page