Jetpack Compose Surface

A Jetpack Compose Surface is one of the most fundamental building blocks Jetpack Compose gives every screen, because almost every visible rectangle, panel, or background in a Compose UI eventually sits on some kind of Surface. Whenever a screen needs a solid area of color, a raised card-like panel, or a bordered container, Jetpack reaches for the Jetpack Surface composable to draw that shape and paint it with a background color. A Jetpack Compose Surface handles the color, the shape, the elevation, and the border for whatever content sits inside it, so composables that live inside a Jetpack Surface never have to worry about drawing their own background.

This blog walks through every parameter the Jetpack Compose Surface composable exposes — modifier, shape, color, contentColor, tonalElevation, shadowElevation, border, onClick, enabled, interactionSource, and content — along with the selectable and toggleable Surface variants Jetpack Compose ships alongside the basic one. Every code sample below is fully self-contained, so you can paste it into an online Kotlin compiler and watch the Jetpack Surface composable behave exactly as Jetpack Compose describes it.

What Is the Jetpack Compose Surface Composable

The Surface composable is a container that Jetpack Compose uses to draw a background shape and clip its content to that same shape. Unlike a plain layout container, which only groups children together, a Jetpack Compose Surface actually paints a background color, can raise its content with elevation, and can wrap the whole area in a border. As a Material Design surface, a Jetpack Compose Surface always starts from a neutral background tone before elevation or tint changes it. Jetpack ships the Jetpack Surface composable in the material3 package, so a Jetpack Compose Surface automatically pulls its default color straight from your app's Material color scheme.

Here's the simplest Jetpack Compose Surface example you'll run into, showing the Jetpack Surface composable at its most basic:

kotlin
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun BasicSurfaceExample() {
    Surface(modifier = Modifier.padding(16.dp)) {
        Text("Hello from a Surface")
    }
}

Running this Jetpack Compose Surface example draws a plain rectangle filled with the default surface color from MaterialTheme, with the Text label sitting inside it. The Surface composable itself never draws the label directly — it only supplies the background, the shape, and the clipping boundary that everything inside a Jetpack Compose Surface gets drawn within. This is why a Jetpack Compose Surface can wrap a Text, an Icon, or an entire Column and still behave the same way every time Jetpack Compose renders it.

Jetpack designed the Jetpack Surface composable so a developer never has to draw a background rectangle by hand. A Jetpack Surface stays this simple no matter which screen it appears on, because Jetpack Compose keeps the same Surface behavior everywhere it's used. Jetpack documents the Jetpack Surface composable alongside its other Material building blocks, so the mental model learned for one Jetpack Compose Surface applies immediately the next time a Jetpack Surface shows up. Jetpack ships this exact same container across phones, tablets, and desktop windows, so a screen never needs a different background approach per platform.

Setting Background Color With the Color Parameter

The color parameter is what actually fills a Jetpack Compose Surface with paint, and it defaults to MaterialTheme.colorScheme.surface whenever you don't set it yourself. A jetpack compose surface color argument accepts any Color value, so swapping in MaterialTheme.colorScheme.primaryContainer or a custom Color instantly changes the background every composable inside that Surface sits on top of. Jetpack Compose recalculates this background any time the color parameter changes, repainting the whole Jetpack Compose Surface without touching anything nested inside it.

kotlin
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun TintedSurfaceColorExample() {
    Surface(
        color = MaterialTheme.colorScheme.primaryContainer,
        modifier = Modifier.padding(16.dp)
    ) {
        Text(
            text = "Tinted Surface Background",
            modifier = Modifier.padding(12.dp)
        )
    }
}

Passing primaryContainer as the jetpack compose surface color value tints the whole background a soft accent shade instead of the default neutral surface tone. Because color only changes the fill and never touches shape or elevation, a Jetpack Compose Surface color swap is one of the cheapest ways to give a panel a distinct visual identity. Setting color to Color.Transparent is also valid, and it turns a Jetpack Compose Surface into a shape purely for clipping and elevation, with no visible surface background color at all. Jetpack pulls the default value straight from the active theme, so a screen never needs a hardcoded background value checked into source control. Jetpack treats this neutral default as a safe starting point for nearly any panel on a screen.

Giving a Jetpack Compose Surface Tonal Elevation

The tonalElevation parameter controls how much of a tonal elevation tint Jetpack Compose mixes into the surface color, defaulting to 0.dp. Material Design's tonal elevation system says a Jetpack Compose Surface elevation of a higher value should look like it sits closer to the viewer, so raising tonalElevation blends progressively more of the primary color into the background instead of drawing a literal drop shadow. A jetpack compose surface elevation set through tonalElevation is what gives a raised card-like panel its subtly brighter tint compared to the screen behind it.

kotlin
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun TonalElevationSurfaceExample() {
    Surface(
        tonalElevation = 8.dp,
        modifier = Modifier.padding(16.dp)
    ) {
        Text(
            text = "Raised Tonal Surface",
            modifier = Modifier.padding(12.dp)
        )
    }
}

Setting tonalElevation to 8.dp on this Jetpack Compose Surface mixes in enough of the primary tint that the panel reads as clearly raised above whatever sits behind it, even without a visible shadow. Jetpack Compose computes this tonal elevation blend automatically from the current color scheme, so a Jetpack Compose Surface elevation change like this one still looks correct under both light and dark themes. Because tonalElevation only affects color, stacking several Surface layers with increasing tonalElevation values is a common way Jetpack Compose signals visual hierarchy across a screen. Jetpack calculates this blend from the active color scheme automatically, so nobody has to hand-pick a tint for every elevation level. Jetpack keeps the math behind this blend hidden from a developer on purpose, exposing only the single tonalElevation number that actually matters.

Adding Depth With the shadowElevation Parameter

Where tonalElevation blends color, shadowElevation draws an actual drop shadow beneath a Jetpack Compose Surface, also defaulting to 0.dp. Raising shadowElevation casts a visible shadow that follows whatever shape the Jetpack Surface uses, giving a Jetpack Compose Surface elevation effect that reads as physical depth rather than just a tint change. Both tonalElevation and shadowElevation can be combined on the same Jetpack Compose Surface for a panel that's both tinted and physically raised.

kotlin
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun ShadowElevationSurfaceExample() {
    Surface(
        shape = RoundedCornerShape(12.dp),
        shadowElevation = 6.dp,
        modifier = Modifier.padding(16.dp)
    ) {
        Text(
            text = "Shadowed Surface Panel",
            modifier = Modifier.padding(12.dp)
        )
    }
}

The 6.dp shadowElevation on this Jetpack Compose Surface casts a soft shadow that traces the rounded corners from the shape parameter, lifting the panel visually off the background behind it. Jetpack Compose redraws this shadow any time shadowElevation changes, so animating the value produces a smooth lift-and-drop effect on a Jetpack Compose Surface. Because a heavy shadow can look out of place on a flat design, most Jetpack Compose Surface panels stay under 8.dp of shadowElevation unless the design calls for a dialog or a floating panel. Jetpack renders this shadow with the same shadow engine used throughout the framework, keeping shadow behavior predictable from one screen to the next. Jetpack rarely needs any extra configuration beyond this single shadowElevation number to get a believable lifted panel.

Shaping a Jetpack Compose Surface

The shape parameter decides the silhouette Jetpack Compose clips a Jetpack Surface into, defaulting to RectangleShape. A jetpack compose surface shape doesn't have to stay a plain rectangle — passing a RoundedCornerShape softens the corners, while a CutCornerShape gives the panel angular, cut-off corners instead. Because border and shadowElevation both trace whatever shape you pass in, changing shape on a Jetpack Compose Surface reshapes the outline, the clipping, and the shadow all together.

kotlin
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun RoundedSurfaceShapeExample() {
    Surface(
        shape = RoundedCornerShape(20.dp),
        modifier = Modifier.padding(16.dp)
    ) {
        Text(
            text = "Rounded Surface Shape",
            modifier = Modifier.padding(16.dp)
        )
    }
}

Passing RoundedCornerShape(20.dp) turns the corners of this Jetpack Compose Surface into smooth curves instead of sharp right angles, and every child inside the Jetpack Surface gets clipped to that same rounded outline. A Jetpack Compose Surface shape can just as easily use CircleShape for a perfectly round panel, which is common for avatar backgrounds or icon containers. Jetpack reuses this same Shape type across nearly every Material composable it ships, so a shape already familiar from one part of an app transfers directly to a Jetpack Compose Surface. Jetpack applies this shape consistently across the whole container, so the outline, the clipping, and the shadow never drift out of sync with each other. Jetpack never forces a fixed corner radius, leaving the exact curve entirely up to the screen being built.

Drawing a Border Around the Jetpack Surface

The border parameter accepts a BorderStroke describing an outline width and color, defaulting to null so a Jetpack Compose Surface has no border unless you set one. A jetpack compose surface border traces the exact same shape the Jetpack Surface already clips to, so a rounded Surface gets a rounded border and a circular Surface gets a circular outline. Because border sits independent of color, a Jetpack Compose Surface can keep a transparent fill while still showing a crisp outline around its edge.

kotlin
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun BorderedSurfaceExample() {
    Surface(
        shape = RoundedCornerShape(12.dp),
        border = BorderStroke(1.5.dp, MaterialTheme.colorScheme.outline),
        modifier = Modifier.padding(16.dp)
    ) {
        Text(
            text = "Outlined Surface Border",
            modifier = Modifier.padding(12.dp)
        )
    }
}

This 1.5.dp BorderStroke in the outline color draws a thin line around the rounded edge of the Jetpack Compose Surface, giving the panel a defined boundary without adding any shadow or tonal tint. Because a Jetpack Compose Surface border shares the shape parameter, changing the shape later automatically reshapes the border to match, so the two never fall out of sync. Leaving border set to null, the default, is the right choice whenever a Jetpack Compose Surface only needs a background fill or elevation without a visible outline. Jetpack leaves this outline fully optional on purpose, since plenty of screens never need a visible edge at all. Jetpack never bundles a shadow into the border itself, keeping the two effects independent of one another.

Styling Content Color Inside the Jetpack Surface

The contentColor parameter sets the default color that Text, Icon, and other composables inside a Jetpack Compose Surface inherit automatically, and it defaults to whatever contentColorFor(color) calculates as a readable match for the background. Because every child composable reads contentColor through LocalContentColor, a Jetpack Compose Surface only needs one contentColor value to keep every piece of text or iconography inside it legible against the background.

kotlin
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun ContentColorSurfaceExample() {
    Surface(
        color = MaterialTheme.colorScheme.errorContainer,
        contentColor = MaterialTheme.colorScheme.onErrorContainer,
        modifier = Modifier.padding(16.dp)
    ) {
        Text(
            text = "Error Message Surface",
            modifier = Modifier.padding(12.dp)
        )
    }
}

Setting contentColor to onErrorContainer alongside an errorContainer background keeps the Text readable against the tinted Jetpack Compose Surface, matching the contrast pair Material Design expects for error states. Any Text placed inside this Surface picks up onErrorContainer automatically, without needing an explicit color argument on the Text composable itself. Overriding contentColor this way is far more common than leaving it at the calculated default whenever a Jetpack Compose Surface uses a strong or unusual background color. Jetpack calculates this pairing automatically so a developer rarely has to reach for a manual override outside of unusual background colors. Jetpack expects every nested composable to read this same contentColor value rather than hardcoding its own.

Making a Jetpack Compose Surface Clickable

Jetpack Compose also ships a clickable overload of Surface that takes an onClick lambda, turning the entire Jetpack Compose Surface into a tappable region with its own ripple feedback. Adding onClick to a Jetpack Surface is how Jetpack Compose builds tappable panels — list rows, selectable tiles, expandable sections — without wrapping a separate clickable modifier around a plain Surface by hand. The enabled parameter on this overload defaults to true and, when set to false, disables both the click handling and the ripple animation.

kotlin
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun ClickableSurfaceExample() {
    var tapCount by remember { mutableStateOf(0) }
    Surface(
        onClick = { tapCount++ },
        modifier = Modifier.padding(16.dp)
    ) {
        Text(
            text = "Tapped $tapCount times",
            modifier = Modifier.padding(16.dp)
        )
    }
}

Tapping anywhere inside this Jetpack Compose Surface runs the onClick lambda, incrementing tapCount and showing a ripple that spreads from the tap position across the whole Surface. Because the clickable Surface overload accepts the same color, shape, tonalElevation, shadowElevation, and border parameters as the basic one, a tappable Jetpack Compose Surface can look identical to a static panel while still responding to touch. This clickable Surface pattern is what Jetpack Compose uses under the hood inside several of its own higher-level Material components. Jetpack reuses this exact tap-and-ripple contract across several of its higher-level building blocks, not only this one container. Jetpack never asks a developer to wire up a separate ripple manually once onClick is supplied.

Modifier, InteractionSource, and Other Surface Options

A few smaller pieces round out what a Jetpack Compose Surface can do, each useful in narrower situations.

  • Modifier: a jetpack compose surface modifier is a normal modifier chain, adding size, padding, or a content description like any other composable.
  • InteractionSource: a custom MutableInteractionSource on the clickable overload exposes press, hover, and focus events before onClick fires.
  • Selectable Surface: a selected-and-onClick overload builds single-choice tiles, with selected tracking the visual selection state.
  • Toggleable Surface: a checked-and-onClick overload flips a Boolean state each time the Surface is tapped, useful for multi-choice toggles.
kotlin
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun InteractionSourceSurfaceExample() {
    val interactionSource = remember { MutableInteractionSource() }
    Surface(
        onClick = { },
        interactionSource = interactionSource,
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        Text(
            text = "Surface With Custom Interaction Source",
            modifier = Modifier.padding(12.dp)
        )
    }
}

This jetpack compose surface modifier chain stretches the Surface across the available width, while interactionSource lets a parent observe press state without touching onClick. Jetpack rarely needs interactionSource, selected, or checked day to day, but exposes all three for screens needing finer control.

Comparing Surface Parameters

ParameterTypeDefaultPurpose
modifierModifierModifierSizes, styles, and positions the Jetpack Compose Surface
shapeShapeRectangleShapeControls the outline, clipping, and shadow silhouette
colorColorMaterialTheme.colorScheme.surfaceFills the Jetpack Surface background
contentColorColorcontentColorFor(color)Default color inherited by content inside the Jetpack Surface
tonalElevationDp0.dpBlends primary color tint to simulate raised elevation
shadowElevationDp0.dpDraws an actual drop shadow beneath the Jetpack Surface
borderBorderStroke?nullDraws an outline around the Jetpack Surface's shape
onClick(() -> Unit)?noneMakes the Jetpack Surface tappable with ripple feedback
enabledBooleantrueTurns click handling on or off on a clickable Surface
interactionSourceMutableInteractionSource?nullExposes press, hover, and focus events
content@Composable () -> UnitrequiredThe Text, Icon, or layout drawn inside the Jetpack Surface

Reading across this table shows how a Jetpack Compose Surface layers four independent ideas on top of a plain container: a background fill, an optional elevation effect, an optional border, and an optional click handler. Once these pieces are familiar, reaching for a Jetpack Compose Surface becomes a quick, repeatable decision for almost any panel or background a screen needs. Jetpack designed this whole parameter set to stay small on purpose, so a beginner never feels overwhelmed by unnecessary options. Jetpack keeps every one of these parameters optional except content, so a minimal container is always just one line of code away.

Practical Use Case: Building a Notification Panel With a Jetpack Compose Surface Example

A common jetpack compose surface example combines color, shape, tonalElevation, and border into a single notification panel that sits above the rest of a screen. This next example builds a dismissible alert panel using several Surface parameters together, the kind of layout a real settings screen or dashboard might need.

kotlin
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun NotificationSurfacePanel() {
    var dismissed by remember { mutableStateOf(false) }

    if (!dismissed) {
        Surface(
            color = MaterialTheme.colorScheme.secondaryContainer,
            contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
            shape = RoundedCornerShape(16.dp),
            tonalElevation = 4.dp,
            shadowElevation = 2.dp,
            border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
        ) {
            Row(
                modifier = Modifier.padding(16.dp),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                Text("Your storage is almost full")
                Text(
                    text = "Dismiss",
                    modifier = Modifier.clickable { dismissed = true }
                )
            }
        }
    }
}

This Jetpack Compose Surface example stacks tonalElevation and shadowElevation together, so the panel reads as both tinted and physically raised above the screen behind it, while the border adds a crisp edge around the rounded shape. Tapping the Dismiss text flips the dismissed state, and because that state lives outside the Jetpack Surface, the whole panel disappears the moment dismissed turns true. Building a notification panel this way shows how color, shape, elevation, and border on a Jetpack Compose Surface combine into one cohesive, reusable layout instead of five separate pieces of styling code. Jetpack scales this exact pattern easily — stack a second panel above or below and the same logic still holds without any extra styling code.

Complete Working Example

This final example pulls several pieces together — color, shape, tonalElevation, border, and onClick — into one runnable screen built entirely around the Jetpack Compose Surface composable.

kotlin
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface {
                    ProfileCardScreen()
                }
            }
        }
    }
}

@Composable
fun ProfileCardScreen() {
    var isSelected by remember { mutableStateOf(false) }

    Column(
        modifier = Modifier.padding(24.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        Surface(
            onClick = { isSelected = !isSelected },
            color = if (isSelected) MaterialTheme.colorScheme.primaryContainer
                    else MaterialTheme.colorScheme.surfaceVariant,
            contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
            shape = RoundedCornerShape(18.dp),
            tonalElevation = if (isSelected) 6.dp else 1.dp,
            shadowElevation = if (isSelected) 4.dp else 0.dp,
            border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
            modifier = Modifier
                .fillMaxWidth()
                .padding(4.dp)
        ) {
            Text(
                text = if (isSelected) "Selected Profile" else "Tap to Select Profile",
                modifier = Modifier.padding(20.dp)
            )
        }
    }
}

Launching this Jetpack Compose Surface example shows a single tappable panel that swaps color, tonalElevation, and shadowElevation the moment isSelected flips to true, while shape and border stay fixed across both states. Tapping the Jetpack Surface toggles isSelected, and Jetpack Compose recomposes the panel with the primaryContainer background and a stronger elevation, giving the whole card a clearly selected look. Every piece covered above — color, shape, tonalElevation, shadowElevation, border, and onClick — comes together in this one runnable Jetpack Compose Surface, the kind of flexible, reusable panel a Jetpack Surface composable is built to support. Jetpack intends every container built this way to feel consistent, whether it's one panel in a settings screen or several scattered across a dashboard.