Window Size Classes in Jetpack Compose

Quick answer: A window size class is a named range for the space your app window has right now. Use it for high-level layout policy—such as whether a destination can show persistent navigation or a useful supporting surface—not as a proxy for “phone” or “tablet.” In current Material 3 Adaptive 1.3+, read it with
currentWindowAdaptiveInfoV2().windowSizeClass, derive a small policy once near the app boundary, and pass that policy down as ordinary state.
Window size classes turn raw dimensions into a small, consistent set of layout ranges. They make it easier to design, implement, and test adaptive Compose UI without scattering arbitrary dp checks through every screen. They also react while the app is running: a resize, split-screen change, orientation change, or fold/unfold can move the window into a different class.
The key word is window. A tablet in split-screen can be compact, and a resizable phone window can become wide. Android’s window size class guidance is explicit that the classes describe the space available to the app, not the physical size or category of the device.
The width and height ranges
Android classifies available width and height independently. Most scrolling apps make their largest layout decision from width, but height matters when a landscape or half-open foldable window is short.
| Dimension | Size class | Range |
|---|---|---|
| Width | Compact | Less than 600dp |
| Width | Medium | 600dp to less than 840dp |
| Width | Expanded | 840dp to less than 1200dp |
| Width | Large | 1200dp to less than 1600dp |
| Width | Extra-large | 1600dp or more |
| Height | Compact | Less than 480dp |
| Height | Medium | 480dp to less than 900dp |
| Height | Expanded | 900dp or more |
The ranges are design breakpoints, not a mandate to build five completely different versions of every screen. In most apps, compact, medium, and expanded width are the meaningful first decisions. Large and extra-large make it possible to refine an already-good wide layout for desktop and connected displays without treating every large window as the same thing.
Likewise, a medium-width window with compact height may be unsuitable for a side-by-side layout even though its width alone looks promising. Android specifically calls out phones and open flippables in landscape as cases where height should influence a multi-pane decision.
Add the current adaptive API
currentWindowAdaptiveInfoV2() is available in the Material 3 Adaptive adaptive artifact. At the time of writing, 1.3.0 is the current stable release; use the version managed by your dependency policy and confirm upgrades in the Material 3 Adaptive release notes.
dependencies {
implementation("androidx.compose.material3.adaptive:adaptive:1.3.0")
}The V2 suffix is intentional. The older currentWindowAdaptiveInfo() function was deprecated in 1.3.0; its API reference directs apps to currentWindowAdaptiveInfoV2(), which supports large and extra-large width classes by default. Do not copy an older example with supportLargeAndXLargeWidth = true into new 1.3+ code.
Derive layout policy once
Read adaptive information near the app or destination boundary. Then translate the size class into product language that the rest of the UI understands. A screen should receive showPersistentNavigation or allowSupportingContent, not repeatedly ask whether it is “medium.”
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2
import androidx.compose.runtime.Composable
import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowSizeClass.Companion.HEIGHT_DP_MEDIUM_LOWER_BOUND
import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_EXPANDED_LOWER_BOUND
import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND
private data class AppLayoutPolicy(
val showPersistentNavigation: Boolean,
val allowSupportingContent: Boolean,
)
@Composable
fun AppRoot() {
val windowSizeClass = currentWindowAdaptiveInfoV2().windowSizeClass
AppContent(
layoutPolicy = windowSizeClass.toAppLayoutPolicy(),
)
}
private fun WindowSizeClass.toAppLayoutPolicy(): AppLayoutPolicy =
when {
// Check the most demanding combination first.
isAtLeastBreakpoint(
widthDpBreakpoint = WIDTH_DP_EXPANDED_LOWER_BOUND,
heightDpBreakpoint = HEIGHT_DP_MEDIUM_LOWER_BOUND,
) -> AppLayoutPolicy(
showPersistentNavigation = true,
allowSupportingContent = true,
)
isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND) -> AppLayoutPolicy(
showPersistentNavigation = true,
allowSupportingContent = false,
)
else -> AppLayoutPolicy(
showPersistentNavigation = false,
allowSupportingContent = false,
)
}This is a deliberately small policy, not a universal breakpoint recipe. A media editor may need more height before adding controls beside the canvas; a messaging app may gain more from a list-detail layout at a different threshold. Validate the policy against the content’s minimum useful width, readable line length, touch targets, and Back behavior.
The checks are ordered from the largest requirement to the smallest. isAtLeastBreakpoint() and its width/height variants match any value at or above their bounds, so testing the medium case first would make a wider case unreachable. The WindowSizeClass API reference documents this ordering requirement.
Do not send window classes into every component
Window classes describe the overall app window, so they are appropriate for app- and destination-level decisions. They are often the wrong input for a reusable card, row, chip group, or dialog. Those elements may have much less space after padding, navigation, or a containing pane is applied.
For a component-level reflow, use the constraints supplied by its parent instead. BoxWithConstraints is a good fit when a component genuinely needs to choose a row or column arrangement from its local width. For repeated content, an adaptive grid can let the component count respond naturally; see LazyVerticalGrid in Jetpack Compose.
Keeping these scopes separate prevents a common failure: a card becomes overly wide or cramped because it was told that the window is expanded even though it lives in a narrow pane.
Use a class to choose a layout, not to stretch one
A larger class is an invitation to make the task better, not to make every element larger. Common high-level choices include:
| Window capability | Product-oriented policy | Possible UI result |
|---|---|---|
| Compact width | Prioritize a focused task and reachable controls. | Single content pane and compact navigation. |
| Medium width | Reveal persistent navigation or improve a dense collection. | Navigation rail or more grid columns. |
| Expanded width with adequate height | Show useful, related information simultaneously. | Persistent navigation plus a context, tool, or detail surface. |
| Large and extra-large width | Refine content measure and desktop-oriented workflows. | Constrained reading column, resizable supporting area, keyboard-friendly controls. |
For top-level navigation, NavigationSuiteScaffold can select an appropriate navigation presentation from adaptive window information while your destination data stays the same. The class does not make the content adaptive by itself; decide separately whether the destination benefits from a different structure.
The previous adaptive-layouts overview explains that distinction in detail. The next step for an app with a genuine list-detail or supporting relationship is to model the navigation and state transition deliberately—not simply display two arbitrary columns because the window is wide.
Handle transitions as normal runtime changes
Do not calculate a size class once in onCreate() and assume it will remain true. A Compose call such as currentWindowAdaptiveInfoV2() participates in composition and reflects window changes. Your policy should therefore be derived from the latest value, just as the rest of the UI is derived from current state.
Keep state separate from the policy:
- Store a selected item by ID, not by “the item in the left pane.”
- Preserve user-entered drafts and filters with the appropriate state holder or
rememberSaveable. - Keep navigation state and Back behavior valid whether a detail view is separate or visible alongside its source.
- Avoid persisting
isTablet,isExpanded, or a calculated layout policy as user preference. It is environment state and must be recomputed.
This separation is what makes an in-place resize feel continuous rather than like a screen reset.
Window classes do not replace posture and insets
Size classes say how much window space exists. They do not describe whether a fold hinge divides it, whether a keyboard currently covers the lower region, or whether a system bar overlaps actionable content.
Use folding and posture information when the layout must avoid or use a hinge. Assign system-bar and IME insets to explicit owners in every layout branch. The edge-to-edge Compose guide covers the latter: a wider layout still needs safe, reachable first and last items.
Test at the thresholds and through the change
Test each product policy on both sides of its threshold. For the standard width ranges, that means at least 599dp/600dp and 839dp/840dp; if the app has large-screen refinements, include 1199dp/1200dp and 1599dp/1600dp. For height-sensitive UI, test 479dp/480dp and 899dp/900dp as well.
Then test the transition itself:
- Select content, scroll, type a draft, or open a sheet in one size class.
- Resize, rotate, enter split-screen, or fold/unfold.
- Check that the right branch appears and that selection, focus, drafts, scroll intent, and Back behavior remain understandable.
- Repeat with large font scaling, visible system bars, and keyboard or pointer input where the product supports them.
Previews and screenshot tests are useful for catching visual regressions at exact dimensions; emulators and physical devices confirm behavior during real configuration changes. Test the layout policy as a pure function separately, so breakpoint intent remains clear when the design evolves.
Common mistakes
Treating a class as a device detector
“Expanded” does not mean tablet and “compact” does not mean phone. Both classes can occur on several form factors and windowing modes. Use classes to describe space, not hardware identity.
Giving every class a wholly separate screen
Most screens need a small number of purposeful decisions, not five forked implementations. Keep shared content and state; vary navigation, pane visibility, density, or component arrangement only when it improves the task.
Ignoring height
Width often drives the first decision, but a short landscape window can make a two-pane or persistent-control layout unpleasant. Combine width and height when the design has a real vertical-space requirement.
Copying the deprecated API from older tutorials
If the project uses Material 3 Adaptive 1.3 or later, prefer currentWindowAdaptiveInfoV2(). Older docs may still show the predecessor; check the API reference and the project’s pinned dependency version before changing production code.
FAQ
Do I need a different layout for all five width classes?
No. Start with compact, medium, and expanded decisions that solve a user problem. Use large and extra-large only when desktop-scale space creates a distinct improvement, such as a better content measure or a resizable supporting area.
Should a reusable composable accept WindowSizeClass?
Usually not. Pass it a small semantic flag or let it react to its own constraints. That makes the component reusable in panes, dialogs, grids, and previews whose local size differs from the whole window.
Can I use custom breakpoints?
Yes, when the product’s content has a verified minimum requirement that the standard classes do not express. Keep custom values localized, name the resulting policy for the user benefit it enables, and test immediately on both sides of each boundary.