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 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.
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.
// 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.
// 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().
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: 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: 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.
// 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")
}
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).
| Level | Elevation | Elements | Shadow Characteristic |
|---|---|---|---|
| 0 | 0 dp | Text, icons, background | No shadow, flat layer |
| 1 | 1–4 dp | Buttons, input fields | Light shadow, small offset |
| 2 | 6–12 dp | Cards, Snackbar | Moderate shadow, noticeable blur |
| 3 | 16–24 dp | Dialogs, menus | Deep shadow, large blur |
| 4 | 24+ dp | Modal windows | Maximum 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).
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).
// 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
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.
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.
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).
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.
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
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.
Read also