Shadow: essence, creating shadows in mobile applications

Author: IT Sectr Published: 2026-02-28 Reading time: 9 min

Shadow is a visual effect in mobile interfaces that creates the illusion of depth and hierarchy between elements. Shadows show which element is higher in the z-plane and, therefore, is more important or interactive. According to the Material Design Guidelines (Google, 2026), system shadows use five key elevation levels: 0 dp (flat), 1–4 dp (buttons), 6–12 dp (cards), 16–24 dp (dialogs), 24+ dp (modal windows). Learn more about element height in the article about Elevation.

Key Takeaways

  • Shadow — a visual effect that creates depth by darkening the area under an element with edge blurring.
  • On iOS, shadows are set via CALayer properties: shadowColor, shadowOffset, shadowRadius, shadowOpacity.
  • On Android, shadows are managed through elevation (Material Design) or custom rendering via OutlineProvider.
  • Jetpack Compose supports shadows via Modifier.shadow() and elevation in Card/Surface.
  • In Flutter, shadows are implemented via BoxShadow in BoxDecoration or through PhysicalModel.

What is Shadow in UI?

Shadow in user interfaces is a visual effect that simulates lighting and depth. A shadow is created by drawing a dark blurred copy of an element, offset from the original. In mobile design, shadows serve three functions: show hierarchy (higher element = longer shadow), signal interactivity (buttons with shadows look pressable), separate content from background (cards with shadows read as separate blocks).

In Google's Material Design, shadows are inextricably linked to the concept of elevation. Each element occupies a certain height in the z-coordinate, and the shadow is a projection of top lighting. Apple's Human Interface Guidelines do not prescribe a strict elevation system but recommend using shadows to create visual hierarchy and depth in interfaces.

According to a Nielsen Norman Group study (2024), properly placed shadows reduce the time to find an interactive element by 24%. Shadows also help users with visual impairments distinguish element boundaries. However, excessive use of shadows (3+ levels on a screen) increases cognitive load and impairs perception.

Shadows on iOS: CALayer and SwiftUI

On iOS, shadows are implemented via CALayer properties: shadowColor sets the shadow color, shadowOffset — offset in CGSize (x, y), shadowRadius — blur radius, shadowOpacity — opacity from 0 to 1. For performance, it is also recommended to set shadowPath — a UIBezierPath that describes the shadow shape, so Core Animation doesn't compute it automatically.

swift
// UIKit: configuring shadow via CALayer
import UIKit

let cardView = UIView()
cardView.backgroundColor = .systemBackground
cardView.layer.cornerRadius = 12

// Shadow configuration
cardView.layer.shadowColor = UIColor.black.cgColor
cardView.layer.shadowOpacity = 0.15
cardView.layer.shadowOffset = CGSize(width: 0, height: 4)
cardView.layer.shadowRadius = 8

"> Optimization: explicit shadowPath
cardView.layer.shadowPath = UIBezierPath(
    roundedRect: cardView.bounds,
    cornerRadius: 12
).cgPath

In SwiftUI, shadows are set via the .shadow() modifier with parameters color, radius, x, y. SwiftUI automatically applies shadowPath and manages rendering performance. For advanced shadows (inner shadows, multiple shadows), use overlay() with LinearGradient.

swift
// SwiftUI: shadow via shadow modifier
import SwiftUI

VStack {
    Text("Card with shadow")
        .padding()
        .background(Color.white)
        .cornerRadius(16)
        .shadow(
            color: .black.opacity(0.15),
            radius: 10,
            x: 0,
            y: 4
        )
}

Shadow performance on iOS: if shadowPath is not set, Core Animation computes the shadow based on the layer's shape every frame, which can cause lag during animation. For static elements, always set shadowPath. For animated elements, use shouldRasterize = true on CALayer to cache the shadow in a separate layer. In SwiftUI, rasterization is managed via .drawingGroup().

Shadows in Android: elevation and custom shadows

In Android, shadows in Material Design are implemented via elevation — a View property that sets the element's height in the z-coordinate. The system automatically renders a shadow based on elevation and outlineProvider (element outline). The higher the elevation, the larger and more blurred the shadow. For Views that don't inherit Material Design (e.g., AppCompat), shadows only work with hardware acceleration enabled.

xml
<!-- XML: elevation via attributes -->
<androidx.cardview.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:cardCornerRadius="12dp"
    app:cardElevation="6dp">
    <!-- Card content -->
</androidx.cardview.widget.CardView>

For custom shadows in the Android View System, use OutlineProvider. ViewOutlineProvider allows you to set an arbitrary shape via outline.setRoundRect() or outline.setPath(). Without an OutlineProvider, the shadow is rendered based on the View's rectangular shape, which doesn't account for corner rounding.

kotlin
// Kotlin: custom OutlineProvider for shadow
import android.graphics.Outline
import android.view.View
import android.view.ViewOutlineProvider

val myView: View = findViewById(R.id.myView)
myView.outlineProvider = object : ViewOutlineProvider() {
    override fun getOutline(view: View, outline: Outline) {
        outline.setRoundRect(
            0, 0, view.width, view.height,
            12f  // corner radius
        )
    }
}
myView.elevation = 8f  // shadow height in px
myView.translationZ = 4f  // additional height on press

In Jetpack Compose, shadows are managed via Modifier.shadow() and elevation in the Card and Surface components. Modifier.shadow() accepts elevation and shape. Unlike the View System, Compose renders shadows in its own render pipeline, providing more predictable behavior across all Android versions.

kotlin
// Jetpack Compose: shadow via Modifier.shadow
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

Card(
    modifier = Modifier.shadow(
        elevation = 6.dp,
        shape = RoundedCornerShape(12.dp),
        clip = false
    ),
    colors = CardDefaults.cardColors(containerColor = Color.White)
) {
    Text("Card with custom shadow")
}

Recommendations for Using Shadows

Proper use of shadows improves interface perception, while improper use creates visual noise and degrades performance. Material Design defines five elevation levels: 0 dp (no shadow), 1–4 dp (buttons, chips), 6–12 dp (cards, menus), 16–24 dp (dialogs, bottom sheets), 24+ dp (modal windows, FAB).

LevelElevationElementsShadow Characteristic
00 dpText, icons, backgroundNo shadow, flat layer
11–4 dpButtons, input fieldsLight shadow, small offset
26–12 dpCards, SnackbarModerate shadow, noticeable blur
316–24 dpDialogs, menusDeep shadow, large blur
424+ dpModal windowsMaximum shadow, element floats

Best practices: (1) don't use shadows for elements that shouldn't attract attention (backgrounds, dividers), (2) shadow height should match element importance — the more important, the higher, (3) on button press, decrease elevation with animation for tactile feedback, (4) avoid shadows on dark backgrounds — use overlay (lighten/darken) instead of shadow. In Material You (Android 12+), shadows adapt to the theme: on dark backgrounds, elevation is replaced by a color layer (surface tint).

Shadows in Flutter: BoxShadow and PhysicalModel

In Flutter, shadows are implemented via BoxShadow inside BoxDecoration. BoxShadow accepts color, offset, blurRadius, and spreadRadius. Flutter supports multiple shadows: passing a list of BoxShadow allows creating complex effects — for example, an outer shadow + inner highlight. PhysicalModel is an alternative that simulates a physical shadow based on shape (BeveledRectangleBorder, CircleBorder).

dart
// Flutter: shadow via BoxDecoration + BoxShadow
import 'package:flutter/material.dart';

Container(
    decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12.0),
        boxShadow: [
            BoxShadow(
                color: Colors.black.withOpacity(0.15),
                offset: Offset(0, 4),
                blurRadius: 8.0,
                spreadRadius: 0,
            ),
        ],
    ),
    child: Text('Container with shadow'),
)

// PhysicalModel for physical shadow
PhysicalModel(
    color: Colors.white,
    elevation: 6.0,
    shadowColor: Colors.black.withOpacity(0.2),
    borderRadius: BorderRadius.circular(12.0),
    child: Text('Physical model with shadow'),
)

Performance: BoxShadow with multiple shadows can cause repaint on each change. For static elements, use PhysicalModel — it caches the shadow in a separate layer (RepaintBoundary). In Flutter 3.16+, Material 3 elevation with adaptive shadows is supported, which changes color and blur depending on the theme (light/dark).

Frequently Asked Questions

How does elevation differ from shadow?

Elevation is the numeric height of an element in the z-coordinate, while shadow is the visual effect created by this height. In Android, elevation is a View property that automatically generates a shadow. In design, elevation and shadow are often used interchangeably, but technically elevation is the cause and shadow is the effect.

Why is the shadow not displaying on Android?

Check: (1) the View has elevation set greater than 0, (2) outlineProvider is set (if the View is not from the Material library), (3) hardware acceleration is enabled (android:hardwareAccelerated="true" in the manifest). CardView automatically manages outlineProvider. For custom Views, call view.outlineProvider = ViewOutlineProvider.BACKGROUND.

How to create an inner shadow on iOS?

CALayer does not support inner shadows directly. Use a combination: CAGradientLayer with black color and transparency inside a mask, or overlay a UIImageView with a raster inner shadow. In SwiftUI, inner shadow is implemented via overlay with LinearGradient and blendMode(.multiply).

How to animate shadow on button press?

In Android, use translationZ for animation: at rest elevation = 2 dp, on press translationZ = 4 dp (total height 6 dp). On iOS, animate shadowOffset and shadowRadius simultaneously with button transformation. In SwiftUI, use withAnimation with elevation change via Modifier.shadow(). In Flutter, use AnimatedContainer with BoxShadow change.

How many shadows can be used on one screen?

Material Design recommends no more than three elevation levels on one screen. If there are more elements with shadows, group them with the same height. Excessive shadow count creates visual noise and reduces rendering performance on mid-range and budget devices.

Summary

  • Shadow is a visual depth effect based on element elevation in the z-coordinate.
  • On iOS, shadows are configured via CALayer: shadowColor, shadowOffset, shadowRadius, shadowOpacity.
  • In Android, shadows are managed via elevation and OutlineProvider for custom shape.
  • Material Design defines five elevation levels: 0, 1–4, 6–12, 16–24, and 24+ dp.
  • Jetpack Compose uses Modifier.shadow() and elevation in Card/Surface for shadow management.
  • Flutter implements shadows via BoxShadow (BoxDecoration) and PhysicalModel (physical shadow).
  • Optimization: set shadowPath (iOS), outlineProvider (Android), use PhysicalModel (Flutter).

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also