Luna's UI Effects system adds composable shader-based visual effects to any UI Toolkit VisualElement — drop shadows, glows, insets, outlines, gradients, shine sweeps, flash pulses, tiled patterns, dissolves. Authoring is USS-driven: an element opts in with the luna-fx marker class, then tier / body / fill classes layer the look. C# exists as an imperative escape hatch but isn't the recommended path.

Quick start

xml
<!-- A shadowed, glossy yellow button --> <engine:Button class="luna-fx luna-fx-btn luna-fx-clay luna-fx-yellow" text="Forge"/>
xml
<!-- A card with a soft drop shadow --> <engine:VisualElement class="luna-fx luna-fx-shadow-md"/>
xml
<!-- Pulse-glowing chip --> <engine:VisualElement class="luna-fx luna-fx-glow-md luna-fx-pulse"/>

If you have LunaUIManager in the scene with Auto Install Fx enabled (the default), every UIView automatically wires its descendants — no extra setup. The first class is always luna-fx; everything else is composable.

Effect catalog

Eight effects, all running through one Shader Graph uber shader. Each is keyword-toggled; disabled effects compile out and cost nothing.

EffectClass hintUse it for
Outerluna-fx-shadow-*, luna-fx-glow-*, luna-fx-ringDrop shadow, halo glow, colored rim
Outline(auto in default stack)Crisp border, dashed/dotted/circling/shine border
InnerOverlayluna-fx-inset-*, luna-fx-track, luna-fx-rim-bottomRecessed/embossed look, claymorphic depth, side-mask rims
Gradientluna-fx-clay, luna-fx-bar, luna-fx-matte, luna-fx-rainbow, luna-fx-aurora, luna-fx-holo, …Surface fill, linear/radial CSS gradients
Shineluna-fx-shineAnimated sweeping highlight (linear or radial)
Flashluna-fx-pulsePeriodic full-element flash/breathe
Tile--luna-fx-tile-imageTiled texture overlay (animated, with reveal/coverage)
Dissolve--luna-fx-dissolve-rateEdge-banded dissolve / appear / disappear effect

The default stack always includes Outer + Outline. The other six are lazy-stacked the first time their marker variable resolves on the element — so a card that only uses luna-fx-shadow-md never pays for the Shine or Tile slots.

Predefined classes

Ship with Luna out of the box. Use them as building blocks; combine freely.

Marker

ClassEffect
luna-fxRequired. Opts the element into the system.

Tier classes — drop shadow / outer halo

Outer effect. Six tier sizes per family.

Class familySizesUse
luna-fx-shadow-*xs, sm, md, lg, xl, 2xlBlack drop shadow under the element
luna-fx-glow-*xs, sm, md, lg, xl, 2xlPalette-colored halo (pulls from --luna-fx-grad-tone-2)

Glow tiers also bump strength on :hover automatically.

Tier classes — inner overlay (claymorphic depth)

InnerOverlay effect. Six tier sizes.

ClassUse
luna-fx-inset-xsluna-fx-inset-2xlRecessed/embossed inner shadow on all four edges
luna-fx-rim-bottomSide-mask modifier — pair with an inset tier; only the bottom rim renders. Signature "card lip" depth.

Body classes — multi-effect bundles

Each body bundles outer + inner + outline overrides for a typical UI shape.

ClassWhat it is
luna-fx-btnTight drop shadow + bottom-heavy inset, with built-in :hover / :active / :disabled state values
luna-fx-trackRecessed rail (top-heavy inset) — progress-bar bg, slot bg
luna-fx-bannerBottom-edge inset + soft cast — modal headers, accent strips
luna-fx-ringPalette-colored halo + thin inner highlight — portrait frames

Fill classes — gradient surface

Gradient effect. Each sets --luna-fx-gradient to a CSS gradient string referencing --luna-fx-grad-tone-1/2/3. Always combine with a color binding (e.g. luna-fx-yellow) so the tones resolve.

ClassLook
luna-fx-clay4-stop vertical "puffy clay"
luna-fx-bar3-stop horizontal with end-cap (progress fills)
luna-fx-matteNear-flat 2-stop

Composable modifiers

ClassEffect
luna-fx-shineAnimated sweeping highlight
luna-fx-pulseFlash/breathe pulse

Flair classes (Essentials theme)

The Essentials sample ships extra opinionated presets in UIEffect_Flair.uss:

  • Multi-stop gradients: luna-fx-rainbow, luna-fx-sunset, luna-fx-aurora, luna-fx-holo, luna-fx-candy, luna-fx-orb, luna-fx-spotlight, luna-fx-vignette
  • Animated outlines: luna-fx-rainbow-outline, luna-fx-candy-outline, luna-fx-gold-outline, luna-fx-silver-outline, luna-fx-bronze-outline
  • Outline patterns: luna-fx-scan, luna-fx-stripes, luna-fx-dots, luna-fx-hearts, luna-fx-stars, luna-fx-dashed-10, luna-fx-dashed-20

These are sample-side; copy what you like into your project's USS.

USS variables

The full --luna-fx-* vocabulary, by effect. Every variable supports var(), :hover / :active / :disabled selectors, and theme tokens. Suffixed variants (-hover, -active, -disabled) on the animatable scalars are picked up by the auto-attached state-tween for smooth transitions.

Outer (drop shadow / halo)

VariableTypeNotes
--luna-fx-outer-strengthfloat0 = dormant. Animatable.
--luna-fx-outer-widthfloatPixel reach. Animatable.
--luna-fx-outer-edge-spreadfloatSoft falloff from element edge.
--luna-fx-outer-offset-xfloatHorizontal offset (px).
--luna-fx-outer-offset-yfloatVertical offset (px). Positive = down.
--luna-fx-outer-colorcolorHalo color. Use var(--luna-fx-grad-tone-2) for palette-tinted glow.

Outline

VariableTypeNotes
--luna-fx-outline-widthfloatPixel width. 0 = dormant.
--luna-fx-outline-opacityfloat0 = dormant (animatable).
--luna-fx-outline-colorcolor
--luna-fx-outline-softnessfloat0 = crisp, higher = blurred edge.
--luna-fx-outline-pattern-imagespriteURL/resource. Tints into the outline (dashed, hearts, …).
--luna-fx-outline-pattern-scalefloat
--luna-fx-outline-pattern-opacityfloat
--luna-fx-outline-pattern-tintcolor
--luna-fx-outline-dash-countfloat0 = solid; non-zero gives dashed segments.
--luna-fx-outline-dash-ratiofloatDash : gap ratio.
--luna-fx-outline-circling-speedfloatAnimated rotation speed.
--luna-fx-outline-shine-speedfloatTravelling-shine speed.
--luna-fx-outline-shine-sizefloatTravelling-shine band size.
--luna-fx-outline-shine-colorcolor
--luna-fx-outline-gradientstringCSS gradient applied to the outline (e.g. rainbow ring).

InnerOverlay (recessed / embossed)

VariableTypeNotes
--luna-fx-inner-opacityfloat0 = dormant (animatable).
--luna-fx-inner-softnessfloatEdge blur.
--luna-fx-inner-colorcolorDefault semi-transparent black.
--luna-fx-inner-widthfloatShorthand — sets all four edges.
--luna-fx-inner-width-leftfloatPer-edge override.
--luna-fx-inner-width-topfloatPer-edge override.
--luna-fx-inner-width-rightfloatPer-edge override.
--luna-fx-inner-width-bottomfloatPer-edge override.

Gradient (surface fill)

VariableTypeNotes
--luna-fx-gradientstringCSS gradient string. References to --luna-fx-grad-tone-* resolve at draw time, so palette swaps re-tint without re-parsing.
--luna-fx-gradient-opacityfloat
--luna-fx-gradient-anglefloatLinear gradients only.
--luna-fx-gradient-typefloat0 = Linear, 1 = Radial.
--luna-fx-gradient-reversefloat0 / 1.
--luna-fx-gradient-color-biasfloatPush midpoint of two-stop gradients.
--luna-fx-grad-tone-1/2/3colorThe three tones a fill class references. Set by palette/color-binding classes.

Shine (animated highlight sweep)

VariableTypeNotes
--luna-fx-shine-opacityfloat0 = dormant.
--luna-fx-shine-colorcolor
--luna-fx-shine-widthfloatBand width (0–1 of element).
--luna-fx-shine-anglefloatDegrees.
--luna-fx-shine-cycle-timefloatSeconds per loop.
--luna-fx-shine-sweep-anglefloatTotal arc the shine traverses.
--luna-fx-shine-modefloat0 = Linear, 1 = Radial.

Flash (pulse / breathe)

VariableTypeNotes
--luna-fx-flash-intensityfloat0 = dormant (animatable).
--luna-fx-flash-colorcolor
--luna-fx-flash-speedfloatHz.

Tile (texture overlay)

VariableTypeNotes
--luna-fx-tile-imagespriteRequired to light up.
--luna-fx-tile-opacityfloat0 = dormant.
--luna-fx-tile-colorcolor
--luna-fx-tile-scalefloat
--luna-fx-tile-rotationfloatDegrees.
--luna-fx-tile-ratefloatAnimation rate.
--luna-fx-tile-speedfloatTranslation speed.
--luna-fx-tile-coverage-startfloatReveal start (0–1).
--luna-fx-tile-coverage-endfloatReveal end (0–1).
--luna-fx-tile-coverage-start-softnessfloat
--luna-fx-tile-coverage-end-softnessfloat
--luna-fx-tile-cell-zoom-startfloat
--luna-fx-tile-cell-zoom-endfloat
--luna-fx-tile-full-revealfloat

Dissolve

VariableTypeNotes
--luna-fx-dissolve-ratefloat0 = dormant. Animate to 1 to fully dissolve.
--luna-fx-dissolve-imagespriteOptional — built-in noise pattern is the fallback.
--luna-fx-dissolve-colorcolorEdge-band tint.
--luna-fx-dissolve-widthfloatEdge-band width.
--luna-fx-dissolve-softnessfloat
--luna-fx-dissolve-scalefloatPattern scale.

State-aware authoring

Append -hover, -active, or -disabled to any animatable scalar to declare its target state value. The auto-attached UIEffectStateTween interpolates smoothly:

css
.my-card { --luna-fx-outer-strength: 0.5; --luna-fx-outer-strength-hover: 0.9; /* tweens to here on hover */ --luna-fx-outer-strength-active: 0.3; --luna-fx-outer-strength-disabled: 0.2; }

Pseudo-class selectors (:hover, :active, :enabled, :disabled) work too and snap immediately. luna-fx-btn uses both: suffix vars for the tween, pseudo-class forwarders so non-tweened renderers still snap correctly.

Setup

Auto-install (default)

In LunaUIManager Inspector:

  • Auto Install Fxtrue by default. Every UIView automatically calls UIEffectPanelHook.Install(view.ParentElement, "luna-fx") on attach, so all descendants carrying the marker class get wired up. No code needed.

Manual install

For raw UIDocument use (no UIView), or to gate which subtrees are eligible:

csharp
using CupkekGames.Luna.Effects; void Start() { var root = GetComponent<UIDocument>().rootVisualElement; UIEffectPanelHook.Install(root, UIEffectClassDriver.DefaultMarkerClass); }

The marker class must be present before the element enters the panel for auto-pickup. If you toggle the marker after attach, call UIEffectClassDriver.Attach(element) directly.

UIEffectSettings asset (required)

The system needs to find the LunaUIEffect Shader Graph at runtime. Create a UIEffectSettings asset (Assets > Create > CupkekGames/Luna UI/UIEffectSettings) and assign the shader. Without it, Shader.Find("Shader Graphs/LunaUIEffect") is the fallback, which only works if a material in the build references the shader.

Samples

The Showcase sample ships three Effects scenes:

  • Components/Effects/EffectsShowcase.unity — gallery of every preset class on every base shape. Browse this first.
  • Components/Effects/EffectsPresets.unity — preset cards isolated for screenshot grids.
  • Components/Effects/EffectsPlayground.unity — interactive playground. Toggle effects, scrub uniforms, see the shader uniforms update live.

UIEffect_Flair.uss is the source for the multi-stop / animated-outline classes listed above — copy what you like.

How it works

  • The luna-fx marker class hooks the element into UIEffectClassDriver. The driver builds a default stack (Outer + Outline, both dormant) and registers CustomStyleResolvedEvent to react to USS changes.
  • Tier / body / fill classes are pure parameter bundles — they only write --luna-fx-* variables. They never replace the stack, so every combination composes naturally.
  • On every style resolve, LunaEffectUSSOverrides reads the resolved --luna-fx-* values from the element's customStyle and writes them into the shader's MaterialPropertyBlock — overriding whatever the C# stack baked. USS is always the final word.
  • InnerOverlay / Shine / Flash / Tile / Dissolve are stacked lazily — only when their marker variable resolves. Elements that never need them stay on the minimal Outer + Outline stack.
  • Gradient strings (--luna-fx-gradient, --luna-fx-outline-gradient) are parsed once into a retained template; pseudo-state tone shifts re-resolve var() references without re-parsing.
  • The shader is one Unlit transparent uber shader with one boolean keyword per effect. Disabled effects are stripped at compile time — zero runtime cost.

Opacity & visibility

Luna effects respect the standard UITK visibility properties — opacity, visibility, and display — including ancestor inheritance. A faded parent fades its filtered children's gradients, glows, shadows, shines, and every other decorative pass uniformly with the element's own content.

How Luna handles it

Every frame the filter pass runs, Luna walks the target up its hierarchy and folds the result into a single _LunaElementOpacity shader uniform:

  • resolvedStyle.opacity on each ancestor is multiplied together (cumulative).
  • Any ancestor with display: none or visibility: hidden collapses the value to 0.

The shader multiplies its final composited output by this uniform. Decorative passes (gradient, outer glow/shadow, shine, flash, tile, etc.) don't read element content — they generate pixels directly — so without this multiply they would render at full intensity even when the element's own content is faded by UITK's pre-multiplied opacity.

The walk happens inside the filter pass's per-frame OnApplySettings callback, so:

  • Opacity transitions animate correctly. UITK lerps resolvedStyle.opacity natively; Luna picks up the live value every frame and the shader fades smoothly with the element.
  • Display flips are free. When the target or an ancestor goes display: none, the filter pass itself is detached (no SetPass cost). GeometryChangedEvent fires on the toggle back, re-attaches automatically.

Gotcha: UITK's native filter: blur() and filter: drop-shadow()

opacity on an ancestor doesn't reach UITK-filtered subtrees through the cascade. UITK renders each element with style.filter into an isolated pass, and that pass's output is composited back without ancestor opacity propagation. This is UITK behavior, not Luna behavior — Luna's own filter pass works around it via _LunaElementOpacity, but native blur / drop-shadow use Unity's built-in filter pipeline which Luna can't reach.

Symptom: you fade a screen with opacity: 0.1 on a parent, every element fades except one — a stubborn dark rectangle from a blurred backdrop, or a leftover drop-shadow.

Fix: write inline opacity directly on the filtered element, not an ancestor.

css
/* ✗ Doesn't fade — UITK filter isolation breaks the cascade. */ .modal-container { opacity: 0.1; } .modal-container .scrim-bg { filter: blur(8px); background-color: rgba(0,0,0,0.4); } /* ✓ Fades — write opacity on the filtered element itself. */ .modal-container .scrim-bg.fading { opacity: 0.1; }
csharp
// Or in C# — set the inline opacity on the filtered element: scrimBg.style.opacity = 0.1f;

Alternative: animate the filter parameter to zero instead of opacity. filter: blur(0px) produces no visible blur. This avoids the isolation entirely.

If you author Luna effects exclusively, you'll never hit this — Luna's pipeline handles ancestor opacity correctly. The gotcha applies only when you mix in UITK's native filter functions.

Imperative C# API (escape hatch)

For dynamic cases that don't fit USS — gameplay-driven effects, pooled VFX overlays, programmatic preview tools — the C# API is unchanged:

csharp
using CupkekGames.Luna.Effects; // Direct API var fx = new UIEffectElement(myElement); fx.AddEffect(new OuterEffect { Color = Color.red, Strength = 1f, Width = 12f }); fx.AddEffect(new ShineEffect { Opacity = 0.4f, Width = 0.1f }); fx.RefreshEffects(); // One-off override on a USS-driven element (pinned over the dormancy gate) LunaEffectOverride.Apply(myElement, new OuterEffect { Strength = 1.5f });

Most projects don't need this — the USS path covers static styling, hover/active states, theme swaps, and palette tinting. Reach for C# only when the effect parameters genuinely need to live in code.

Performance

  • Default stack is two effects (Outer + Outline). The other six lazy-attach only when their USS marker resolves — pay only for what you author.
  • The --luna-fx-* resolved set is cached per element and rebuilt on CustomStyleResolvedEvent. The per-frame write path iterates only the resolved bindings, not the full vocabulary.
  • Effects are independent shader-keyword variants. Disabled effects compile out of the variant the element actually uses.
  • The state-tween manipulator self-discovers suffix vars on first style-resolve and stays dormant if none are present.
  • For purely static decorations (fixed cards, panels, frames), see UI Effect Baking — the editor tool pre-renders the stack to a 9-slice PNG so the element renders as a regular sprite with a single batch.

Troubleshooting

SymptomFix
No effects visibleLunaUIManager in scene? Auto Install Fx enabled? Element has the luna-fx marker class? Element has non-zero size?
Background is washed-white / blank when using luna-fx-shadow-*The element has overflow: hidden. Either drop it, or pair with a fill class (luna-fx-clay / matte / bar) and a color binding so the gradient paints the surface.
Glow renders black--luna-fx-grad-tone-2 isn't set. Apply a palette/color-binding class (luna-fx-yellow, etc.) or set --luna-fx-outer-color directly.
Shader-not-found at runtimeCreate the UIEffectSettings asset and assign the LunaUIEffect Shader Graph (see Setup).
Marker class added at runtime not picked upCall UIEffectClassDriver.Attach(element) manually — auto-pickup only fires for elements that carry the marker on panel-attach.
Parent opacity fades the screen but one filtered element stays visibleElement has UITK's native filter: blur() / filter: drop-shadow(). UITK isolates filter passes from the opacity cascade — write inline opacity directly on the filtered element, or animate the filter parameter to zero instead. See Opacity & visibility.

Settings

Theme

Light

Contrast

Material

Dark

Dim

Material Dark

System

Sidebar(Light & Contrast only)

Light
Dark

Font Family

DM Sans

Wix

Inclusive Sans

AR One Sans

Direction

LTR
RTL