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.

The eight effect types — outer (shadow / glow), outline, inner overlay, gradient, shine, flash, tile and dissolve stacked on sample elements

Quick start

⚠️ Step 0 — assign UIEffectSettings or nothing renders. Set the asset on LunaUIManager's Effect Settings field; without it the filter pipeline stays uninitialised and no effects draw. Create one via Assets > Create > CupkekGames > Luna UI > UI Effect Settings and point it at the Essentials Effects/LunaFilter.asset; full detail in Setup.

From there, authoring is just classes in UXML:

xml
<!-- A shadowed, glossy yellow button --> <engine:Button class="luna-fx luna-fx-btn luna-fx-clay 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 cyan"/>

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 hand-written HLSL uber shader (Hidden/Luna/UberFilter). Each effect is gated by an _ActiveEffects bitmask uniform — disabled effects branch out at runtime and cost almost 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 — the plain yellow color class on a luna-fx element (the .luna-fx.{color} binding) — 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

Color bindings — .luna-fx.{color}

The palette tones the effects pull from — --luna-fx-grad-tone-1/2/3 plus the glow, inner, shine, and flash colors — are routed by color binding classes in the compound form .luna-fx.{color}: a plain palette word that only activates on elements carrying the luna-fx marker.

xml
<engine:Button class="luna-fx luna-fx-btn luna-fx-clay yellow" text="Forge"/>

The yellow here is the same palette word that drives class="btn yellow" elsewhere — one color vocabulary across components and effects. Each binding sets the three gradient tones (with hover/active steps for the state tween) plus the inner-overlay, shine, and flash colors from that ramp.

Where the classes come from:

  • Shipped in EssentialsTheme/Colors/component-colors.uss defines .luna-fx.{color} blocks for all 32 chromatic ramps: red, scarlet, crimson, ruby, rose, pink, magenta, fuchsia, purple, violet, lavender, indigo, navy, cobalt, blue, azure, sky, cyan, turquoise, teal, mint, jade, emerald, green, forest, lime, yellow, gold, amber, orange, peach, coral. component-colors-neutral.uss adds the slate / neutral / stone versions.
  • Re-exportable via the Palette Generator — the web tool's UIEffect_Colors.uss component template regenerates the same routing for your own custom palette.

See Colors for the full palette system and the export round-trip.

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 PanelRenderer use (no UIView), or to gate which subtrees are eligible, install inside the renderer's reload callback — PanelRenderer delivers its visual tree asynchronously:

csharp
using CupkekGames.Luna.Effects; using UnityEngine; using UnityEngine.UIElements; public class FxInstaller : MonoBehaviour { [SerializeField] private PanelRenderer _panelRenderer; void Awake() { _panelRenderer.RegisterUIReloadCallback(OnUIReload); } void OnDestroy() { _panelRenderer.UnregisterUIReloadCallback(OnUIReload); } private void OnUIReload(PanelRenderer renderer, VisualElement root, int version) { UIEffectPanelHook.Install(root, UIEffectClassDriver.DefaultMarkerClass); } }

Install is idempotent on the same root, so re-running it on every reload is safe. It also cleans up after itself: when an installed root detaches from its panel the hook auto-uninstalls, and a one-shot re-attach watcher re-installs the same root if it ever returns to a panel — effects self-recover across panel rebuilds without manual Uninstall bookkeeping.

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.

Without a LunaUIManager, you must also register the effect settings yourself — the filter pipeline stays uninitialised until something calls:

csharp
UIEffectSettings.SetInstance(settingsAsset);

LunaUIManager makes this call automatically in Awake when present; only manager-less setups need it.

UIEffectSettings asset (required)

Create a UIEffectSettings asset (Assets > Create > CupkekGames > Luna UI > UI Effect Settings) and assign the Luna FilterFunctionDefinition asset (the Essentials sample ships one: Effects/LunaFilter.asset, which references the Hidden/Luna/UberFilter shader). Then assign the settings asset to LunaUIManager's Effect Settings field — the manager calls UIEffectSettings.SetInstance in Awake. Without it the filter pipeline is uninitialised and no effects render.

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 (Samples~/Essentials/Theme/UIEffect_Flair.uss in the imported Essentials sample) 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 hand-written HLSL transparent uber shader (Hidden/Luna/UberFilter). Each effect is gated by an _ActiveEffects bitmask uniform — disabled effects branch out at runtime and cost almost nothing.

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). // Returns false if the element lacks the luna-fx binding. LunaEffectOverride.SetFloat(myElement, "--luna-fx-outer-strength", 1.5f); LunaEffectOverride.SetColor(myElement, "--luna-fx-outer-color", Color.red);

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 gated by the _ActiveEffects bitmask uniform in the uber shader. Disabled effects branch out at runtime and cost almost nothing.
  • The state-tween manipulator self-discovers suffix vars on first style-resolve and stays dormant if none are present.
  • Animated effects (shine sweeps, flash pulses, state tweens, per-frame repaints via UIEffectElement.StartAnimation) are driven by one global Luna ticker at a fixed ~16ms cadence — a single scheduled task fans out to every animated element instead of one scheduler per element. Per-element interval parameters (e.g. StartAnimation(intervalMs)) are retained for API compatibility but ignored.
  • Effects self-recover after panel detach/re-attach: an installed root that leaves its panel auto-uninstalls its hook and re-installs when the same root re-attaches — no per-screen lifecycle code needed.
  • 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 color class on the luna-fx element (yellow, etc. — the .luna-fx.{color} binding) or set --luna-fx-outer-color directly.
Filter pipeline uninitialised (no effects render)Create the UIEffectSettings asset, assign the Luna FilterFunctionDefinition asset, and assign the settings asset to LunaUIManager's Effect Settings field (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.

See also

  • UI Effect Baking — pre-render static stacks to 9-slice PNGs for one-draw-call cards and panels.
  • Colors — the palette system behind the .luna-fx.{color} bindings.
  • Transition Animation — the motion layer to pair with these looks: 53 presets + a fluent builder.

All eight effects ship with Luna — get it on the Asset Store.

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