Neumorphic (Soft UI) mode is a design style that uses highlights and shadows to create elements that appear to extrude from or be inset into the background surface.
Neumorphism (new skeuomorphism) combines the realism of skeuomorphism with the simplicity of flat design. It creates soft, extruded UI elements that appear to push out from or press into the background surface, using carefully positioned light and dark shadows.
To apply neumorphism style to any element, you must implement these three main properties:
1. Background Match: Element background must be the same as its container (seamless surface)
2. Dark Shadow: Applied to one corner (bottom-right for raised, top-left for inset)
3. Light Shadow: Applied to the opposite corner (top-left for raised, bottom-right for inset)
The effect assumes a top-left light source (like sunlight through a window):
- Dark shadow on bottom-right (where light doesn't reach)
- Light shadow on bottom-right (inner edge catching light)
Flowery.Uno implements neumorphism using a cross-platform approach via DaisyNeumorphicHelper with platform-specific optimizations.
| Platform | Implementation | Notes |
|---|---|---|
| Windows | Composition API DropShadow |
Single shadow (dark) for reliability |
| Desktop (Skia) | UIElementExtensions.SetElevation() |
Single shadow via ShadowState; some templates (e.g., DaisyButton) apply elevation to PART_ShadowContainer to avoid clipping |
| WASM | UIElementExtensions.SetElevation() |
CSS box-shadow |
| Android | UIElementExtensions.SetElevation() |
ViewCompat.SetElevation |
| iOS | UIElementExtensions.SetElevation() |
CALayer.shadow* |
DaisyNeumorphicHelper.Windows.cs):
#if WINDOWS)SpriteVisual with DropShadow (dark shadow, bottom-right)UIElementExtensions.SetElevation() also works on Windows and can be used as a fallback or a simpler path when Composition shadows are disabled.DaisyNeumorphicHelper.ShouldUseDirectElevation() to decide when to bypass the helper and call SetElevation directly (Skia/WASM/non-Windows).DaisyButton: apply elevation to PART_ShadowContainer (template wrapper) to avoid clipping; falls back to ButtonBorder when the container is missing.
- Most other controls: apply elevation to their neumorphic host element (e.g., DaisyCard uses PART_SolidBackground).
xmlns:utu="using:Uno.Toolkit.UI" for ShadowContainerxmlns:controls="using:Flowery.Controls" for Flowery controls (e.g., DaisyButton)DropShadow (single dark shadow); optional ThemeShadow for special cases.UIElementExtensions.SetElevation(); apply elevation to PART_ShadowContainer if the surface is wrapped (prevents clipping).box-shadow via SetElevation; apply elevation to the surface border (or container if used).SetElevation (single shadow).| Platform | Raised | Flat | Inset |
|---|---|---|---|
| Windows (WinAppSDK) | Composition DropShadow (dark only) |
Composition DropShadow (reduced) |
Gradient overlays (inset borders) |
| Desktop (Skia) | SetElevation (single shadow) |
SetElevation (reduced) |
Gradient overlays (inset borders) |
| WASM | SetElevation (CSS box-shadow) |
SetElevation (reduced) |
Gradient overlays (inset borders) |
| Android | SetElevation (native view elevation) |
SetElevation (reduced) |
Gradient overlays (inset borders) |
| iOS | SetElevation (CALayer shadow) |
SetElevation (reduced) |
Gradient overlays (inset borders) |
PART_SolidBackground).GetNeumorphicHostElement() to return that surface.SetElevation to that surface on Skia/WASM.Example (simplified):
<Border x:Name="PART_SolidBackground"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}">
<!-- content -->
</Border>
ShadowContainer so the shadow is not clipped by the border.ShadowContainer, not the inner Border.Example (simplified):
<!-- xmlns:utu="using:Uno.Toolkit.UI" -->
<utu:ShadowContainer x:Name="PART_ShadowContainer"
CornerRadius="{TemplateBinding CornerRadius}">
<Border x:Name="ButtonBorder"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}">
<!-- content -->
</Border>
</utu:ShadowContainer>
Border elements into a container grid.Border).PART_ShadowContainer (Pattern B).DaisyBaseContentControl, override GetNeumorphicHostElement() and return the surface element.ShadowContainer wrapper, make sure you can access it in code (e.g., GetTemplateChild("PART_ShadowContainer")).Example (surface host override):
protected override FrameworkElement? GetNeumorphicHostElement()
{
return _surfaceBorder ?? this;
}
<local:MyControl
NeumorphicEnabled="True"
NeumorphicMode="Raised"
NeumorphicIntensity="0.8" />
OnApplyTemplate, cache template parts (_surfaceBorder, _shadowContainer, etc.).RequestNeumorphicRefresh() after template is ready.DaisyNeumorphicHelper.ShouldUseDirectElevation() is true (Skia/WASM/non-Windows), apply SetElevation to:PART_ShadowContainer (if you use Pattern B), otherwise
- the surface border (Pattern A).
None, clear elevation on the same element you used for direct elevation.<ControlTemplate x:Key="MyCardTemplate" TargetType="local:MyCard">
<Border x:Name="PART_SolidBackground"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}">
<ContentPresenter Margin="{TemplateBinding Padding}" />
</Border>
</ControlTemplate>
public sealed partial class MyCard : DaisyBaseContentControl
{
private Border? _surfaceBorder;
public MyCard()
{
DefaultStyleKey = typeof(MyCard);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_surfaceBorder = GetTemplateChild("PART_SolidBackground") as Border;
RequestNeumorphicRefresh();
}
protected override FrameworkElement? GetNeumorphicHostElement()
{
return _surfaceBorder ?? this;
}
}
<local:MyCard NeumorphicEnabled="True"
NeumorphicMode="Raised"
NeumorphicIntensity="0.8" />
<!-- xmlns:utu="using:Uno.Toolkit.UI" -->
<ControlTemplate x:Key="MyButtonTemplate" TargetType="local:MyButton">
<utu:ShadowContainer x:Name="PART_ShadowContainer"
CornerRadius="{TemplateBinding CornerRadius}">
<Border x:Name="PART_SurfaceBorder"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}">
<ContentPresenter Margin="{TemplateBinding Padding}" />
</Border>
</utu:ShadowContainer>
</ControlTemplate>
public sealed partial class MyButton : DaisyBaseContentControl
{
private Border? _surfaceBorder;
private ShadowContainer? _shadowContainer;
public MyButton()
{
DefaultStyleKey = typeof(MyButton);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_surfaceBorder = GetTemplateChild("PART_SurfaceBorder") as Border;
_shadowContainer = GetTemplateChild("PART_ShadowContainer") as ShadowContainer;
RequestNeumorphicRefresh();
}
protected override FrameworkElement? GetNeumorphicHostElement()
{
return _surfaceBorder ?? this;
}
private UIElement? GetElevationTarget()
{
if (DaisyNeumorphicHelper.ShouldUseDirectElevation() && _shadowContainer != null)
return _shadowContainer;
return _surfaceBorder;
}
}
<local:MyButton NeumorphicEnabled="True"
NeumorphicMode="Raised"
NeumorphicIntensity="0.8" />
Inset Mode - Injects gradient overlay borders into the visual tree:
Border elements with LinearGradientBrush backgroundsElevation values are based on the control's DaisySize via DaisyResourceLookup.GetDefaultElevation():
| DaisySize | Elevation |
|---|---|
| ExtraSmall | 10 |
| Small | 11 |
| Medium | 12 |
| Large | 14 |
| ExtraLarge | 16 |
The caller (e.g., DaisyButton) passes the appropriate elevation based on its Size property.
public void Update(
bool enabled,
DaisyNeumorphicMode mode,
double intensity,
double elevation)
| Parameter | Description |
|---|---|
enabled |
Whether neumorphic effect is active |
mode |
Raised, Inset, Flat, or None |
intensity |
Multiplier for shadow strength (0.0 - 1.0) |
elevation |
Base shadow depth in pixels (from DaisyResourceLookup.GetDefaultElevation()) |
The neumorphic helper requires the control's visual tree to be fully laid out before it can find the correct injection point. When Content is set on a ContentControl, the ContentPresenter doesn't process it immediately—this happens on the next layout pass.
RefreshNeumorphicEffect() runs immediately after OnLoaded():
1. The helper calls VisualTreeHelper.GetChild() to find the visual tree
2. The ContentPresenter has 0 children (not laid out yet)
3. The helper falls back to attaching directly to the ContentPresenter
4. When layout completes, the composition surface renders ON TOP of the actual content
5. Controls appear as blank boxes
The Solution: Neumorphic attachment is deferred until after the firstLayoutUpdated event:
private void OnBaseLoaded(object sender, RoutedEventArgs e)
{
// ... subscriptions ...
OnLoaded(); // Derived control builds its visual tree here
// Defer until visual tree is laid out
LayoutUpdated += OnFirstLayoutForNeumorphic;
}
private void OnFirstLayoutForNeumorphic(object? sender, object e)
{
LayoutUpdated -= OnFirstLayoutForNeumorphic;
if (_isLoaded) RefreshNeumorphicEffect();
}
This ensures dynamically-built controls (like DaisyInput, DaisyTextArea) work correctly with neumorphic mode.
The following safeguards were added after real-world hangs (scrolling + page switches) to prevent layout storms and runaway visual tree updates:
1. Single-shot overlay injection
- The helper now attempts to inject the overlay container only once per instance.
- If the owner is not a direct child of a supported parent (Panel or ContentPresenter), injection is marked as failed and will not be retried.
- This avoids repeated LayoutUpdated loops when the visual tree cannot be safely re-parented.
2. Graceful fallback when injection fails
- If overlay injection fails, the helper disables inset overlays for that instance and falls back to direct elevation for Raised/Flat.
- This keeps the app responsive on complex templates and virtualized panels, at the cost of losing inset on that element.
3. Layout monitoring is gated
- LayoutUpdated monitoring is stopped as soon as the overlay is injected or injection is known to be impossible.
- Update calls are ignored while the element is unloaded or the injection has failed, preventing update storms during scroll.
4. No forced enablement
- Controls must respect IsNeumorphicEffectivelyEnabled() and never force-enable the effect when global mode is None.
- Example fix: DaisySlideToConfirm stopped enabling neumorphism unconditionally to avoid background update churn.
5. Gallery diagnostics guard (runtime only)
- Central in-app diagnostic log aggregation was disabled in the Gallery app to avoid runaway memory usage when many controls update simultaneously.
- This is a Gallery-only safety measure and does not affect library logging APIs.
These are proven fixes from real-world testing that eliminated regressions across Windows + Desktop:
1. Pick the real surface as the host element
- For composite controls, attach neumorphism to the element that actually renders the surface.
- Example: DaisyCard now uses PART_SolidBackground via GetNeumorphicHostElement() to prevent background loss on Windows and Skia.
2. ComboBox popup anchoring after host changes
- When the host is an inner element, anchor the drop-down to the trigger button (not the outer control) so the popup aligns with the input instead of its shadow.
3. Avoid raw Composition DropShadow on Skia
- Compositor.CreateDropShadow() is not implemented in Uno Skia and throws NotImplementedException.
- Use DaisyNeumorphicHelper or ShadowContainer for cross-platform shadows instead of raw Composition APIs.
4. Do NOT merge Uno.Toolkit.* Generic.xaml
- The toolkit does not ship Styles/Generic.xaml. Merging it causes startup failures.
- ShadowContainer only needs the package reference; no Generic.xaml merge.
DispatcherQueuePriority.Low. This prevents the UI thread from hanging when hundreds of controls update simultaneously (e.g., when toggling mode in the Sidebar).Neumorphic effects are integrated directly into the DaisyBaseContentControl class.
Global settings provide default values for controls that don't have local overrides. They are opt-in, not forced:
NeumorphicEnabled, NeumorphicMode, etc. ignore the global settings.DaisyButton has a heuristic that excludes certain buttons from global neumorphic:- Default variant + Default style buttons are excluded (protects sidebars, menus)
DaisyButtonGroup, DaisyJoin) explicitly disable neumorphic on their children.GlobalNeumorphicMode to a non-None value (Raised, Inset, Flat) automatically sets GlobalNeumorphicEnabled = trueGlobalNeumorphicMode = None automatically sets GlobalNeumorphicEnabled = falseGlobalNeumorphicEnabled = true when Mode was None restores the last non-None mode (defaults to Raised)This bidirectional synchronization ensures the dropdown selector works intuitively.
// Set mode preference
DaisyBaseContentControl.GlobalNeumorphicMode = DaisyNeumorphicMode.Inset;
// Disable (remembers Inset)
DaisyBaseContentControl.GlobalNeumorphicEnabled = false; // or Mode = None
// Re-enable (restores Inset, not Raised!)
DaisyBaseContentControl.GlobalNeumorphicEnabled = true;
// Fires GlobalNeumorphicChanged event so controls can refresh
<daisy:DaisyButton
Content="Raised Button"
NeumorphicEnabled="True"
NeumorphicMode="Raised" />
<daisy:DaisyCard
NeumorphicEnabled="True"
NeumorphicMode="Inset"
NeumorphicIntensity="0.7" />
DaisyNeumorphicMode)| Mode | Style Guide Term | Description |
|---|---|---|
None |
— | No effect applied. |
Raised |
Outer | Element appears to "pop out" from the surface. Uses platform elevation API. |
Inset |
Inner | Element appears "pressed into" the surface. Uses gradient overlays. |
Flat |
— | Single subtle shadow (half of Raised elevation). |
| Property | Type | Default | Description |
|---|---|---|---|
NeumorphicEnabled |
bool? |
null |
Enables/disables the effect. Falls back to global setting. |
NeumorphicMode |
DaisyNeumorphicMode? |
null |
The visual style (Raised, Inset, etc.). Falls back to global. |
NeumorphicIntensity |
double? |
null |
Adjusts shadow opacity/strength (0.0-1.0). Falls back to global. |
NeumorphicDarkShadowColor |
Color? |
null |
Override dark shadow color. Falls back to theme-derived global. |
NeumorphicLightShadowColor |
Color? |
null |
Override light shadow color. Falls back to theme-derived global. |
| Property | Type | Default | Description |
|---|---|---|---|
DaisyNeumorphicHelper.UseRoundedShadowMask |
bool |
true |
When true, applies rounded rectangle mask to match button corner radius. |
DaisyNeumorphicHelper uses a partial class pattern for platform-specific implementations:
Flowery.Uno/Controls/
├── DaisyNeumorphicHelper.cs # Shared code, partial method declarations
└── DaisyNeumorphicHelper.Windows.cs # Windows-only (#if WINDOWS), Composition API
Partial Method Hooks:
// In DaisyNeumorphicHelper.cs:
partial void OnApplyDirectElevation(DaisyNeumorphicMode mode, double intensity, double elevation, ref bool handled);
partial void OnDisposeWindowsShadows();
// In DaisyNeumorphicHelper.Windows.cs (#if WINDOWS):
partial void OnApplyDirectElevation(..., ref bool handled)
{
ApplyWindowsDropShadows(mode, intensity, elevation);
handled = true; // Skip default SetElevation path
}
This pattern isolates platform-specific code without risking regressions on other platforms.
Shadow colors are automatically derived from the theme when DaisyThemeManager.ApplyTheme() is called:
| Theme Type | Dark Shadow | Light Shadow |
|---|---|---|
| Dark themes | Pure black #000000 |
Pure white #FFFFFF |
| Light themes | Darkened Base300 (40%) | Pure white #FFFFFF |
The derivation happens in DaisyThemeManager.UpdateNeumorphicShadowColors() and updates:
DaisyBaseContentControl.GlobalNeumorphicDarkShadowColorDaisyBaseContentControl.GlobalNeumorphicLightShadowColor// Per-element via attached property (highest priority)
var darkColor = DaisyNeumorphic.GetDarkShadowColor(element)
?? DaisyBaseContentControl.GlobalNeumorphicDarkShadowColor; // Theme-derived fallback
DropShadow via partial class (DaisyNeumorphicHelper.Windows.cs). Current implementation uses a single dark shadow for reliability. Falls back to standard SetElevation path if Composition API fails.
Desktop (Skia): Shadows are clearly visible and scale well with elevation values. Uses single-shadow SetElevation approach.
WASM: Uses CSS box-shadow via SetElevation. May need slightly boosted opacity values for visibility.
DaisyButtonGroup and DaisyJoin are the single neumorphic surface - they draw ONE unified shadow/border around all children.
Child controls inside these containers have neumorphic explicitly disabled via DaisyNeumorphic.SetIsEnabled(child, false). This prevents:
The children are flat visual segments inside the container's neumorphic shell. The container itself (which inherits from DaisyBaseContentControl) provides the neumorphic effect for the whole group.
The base class automatically manages the creation and disposal of the neumorphic helper during Loaded and Unloaded events.