Files
vnidrop/.codex/skills/compose-skill/references/material-design.md
Hammed Abass fd4fff2c86 docs: add compose development guidelines and skills
Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory.

Co-authored-by: Junie <junie@jetbrains.com>
2026-07-07 16:55:43 +02:00

8.7 KiB
Raw Blame History

Material 3 Theming & Components

TL;DR Defaults

Concern Default
Theme entry point MaterialTheme(colorScheme, typography, shapes) wrapping app content
Dynamic color Enable on Android 12+; fall back to brand ColorScheme on older APIs
Dark/light Follow system via isSystemInDarkTheme(); expose user override if needed
Color pairing Always pair primary/onPrimary, surface/onSurface, *Container/on*Container
Typography Use default M3 type scale; override only specific slots for branding
Shapes Use default M3 shape scale; override per-slot (small, medium, large)
Scaffold Use Scaffold for screens with app bars, FAB, snackbar, or bottom bar
Navigation NavigationSuiteScaffold auto-switches bar/rail by window size
Snackbar SnackbarHostState in Route; show via Effect from ViewModel
Bottom sheet ModalBottomSheet with SheetState; control via show()/hide()
Dialog AlertDialog for simple confirm/dismiss; custom Dialog for complex content
Adaptive layout Derive window size class once at app level; pass down as state

Theming Baseline

Theme Setup

@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }
    MaterialTheme(
        colorScheme = colorScheme,
        typography = AppTypography,
        shapes = AppShapes,
        content = content
    )
}

Key Rules

  • Define LightColorScheme and DarkColorScheme using lightColorScheme() / darkColorScheme().
  • Generate brand colors via Material Theme Builder for guaranteed tonal palettes.
  • Dynamic color is Android-only; CMP projects fall back to brand schemes on non-Android targets.

Color Roles and Dark/Light

Role Pairing Rules

Container Content on it
primary onPrimary
primaryContainer onPrimaryContainer
secondary onSecondary
secondaryContainer onSecondaryContainer
tertiary onTertiary
tertiaryContainer onTertiaryContainer
surface onSurface
surfaceVariant onSurfaceVariant
error onError
errorContainer onErrorContainer

Accessibility Guardrails

  • Always use the correct on* color for text/icons on a container.
  • Do not mix unrelated pairs (e.g., tertiaryContainer background with primaryContainer text).
  • M3 tonal palettes guarantee 3:1+ contrast when paired correctly.

Do / Don't

Do Don't
containerColor = primary, contentColor = onPrimary containerColor = primary, contentColor = tertiaryContainer
Access colors via MaterialTheme.colorScheme.* Hardcode hex colors in components
Test both light and dark themes Assume light-only usage

Typography and Shapes

Typography

M3 defines 15 text styles across 5 categories:

Category Sizes
Display displayLarge, displayMedium, displaySmall
Headline headlineLarge, headlineMedium, headlineSmall
Title titleLarge, titleMedium, titleSmall
Body bodyLarge, bodyMedium, bodySmall
Label labelLarge, labelMedium, labelSmall

Default: Use M3 defaults. Override individual slots for brand fonts:

val AppTypography = Typography(
    titleLarge = TextStyle(fontFamily = BrandFont, fontWeight = FontWeight.SemiBold, fontSize = 22.sp)
)

Shapes

M3 shape scale: extraSmall, small, medium, large, extraLarge.

Default: Use M3 defaults. Override only when brand requires specific corner radii:

val AppShapes = Shapes(
    medium = RoundedCornerShape(12.dp),
    large = RoundedCornerShape(16.dp)
)

Component Decision Matrix

Scaffold

Slot When to use
topBar Screen has a top app bar
bottomBar Screen has bottom navigation or bottom app bar
floatingActionButton Primary action needs FAB
snackbarHost Screen can show snackbars
content Main screen content; receives PaddingValues to apply

Rule: Always apply innerPadding from Scaffold to content root.

Top App Bar

Variant Use case Scroll / default
TopAppBar (small) Simple screens, minimal actions Default: pinnedScrollBehavior unless you need collapse
CenterAlignedTopAppBar Single primary action, centered title Same bar family as small
MediumTopAppBar Moderate navigation, collapsible on scroll exitUntilCollapsedScrollBehavior (also enterAlwaysScrollBehavior where needed)
LargeTopAppBar Hero screens, prominent title, collapsible Same scroll behavior family as medium

Navigation

Window size Component
Compact (phones portrait) NavigationBar (bottom)
Medium/Expanded (tablets, landscape) NavigationRail (side)
Auto-switch NavigationSuiteScaffold

Default: Use NavigationSuiteScaffold for apps with 3-5 top-level destinations. It adapts automatically.

NavigationSuiteScaffold(
    navigationSuiteItems = {
        destinations.forEach { dest ->
            item(
                selected = currentDest == dest,
                onClick = { currentDest = dest },
                icon = { Icon(dest.icon, contentDescription = null) },
                label = { Text(dest.label) }
            )
        }
    }
) { DestinationContent(currentDest) }

Bottom Sheet

Type Use case
ModalBottomSheet Overlays content, dismissible
BottomSheetScaffold Persistent sheet integrated with screen

State control: Use rememberModalBottomSheetState() + SheetState.show()/hide().

MVI pattern: ViewModel emits Effect.ShowSheet; Route composable calls sheetState.show() in LaunchedEffect.

Snackbar

Setup: SnackbarHostState remembered in Route; passed to Scaffold.snackbarHost.

Pattern:

val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(Unit) {
    viewModel.effects.collect { effect ->
        when (effect) {
            is Effect.ShowSnackbar -> {
                val result = snackbarHostState.showSnackbar(effect.message, effect.actionLabel)
                if (result == SnackbarResult.ActionPerformed) viewModel.onEvent(Event.SnackbarAction)
            }
        }
    }
}
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { /* ... */ }

Dialog

Type Use case
AlertDialog Simple title + text + confirm/dismiss buttons
Dialog + Card Complex content, forms, custom layouts

MVI pattern: Dialog visibility controlled by state.showDialog: Boolean. Confirm/dismiss dispatch events.

Adaptive Layout Defaults

Window Size Classes

Class Width breakpoint Typical devices
Compact < 600dp Phones portrait
Medium 600dp 840dp Tablets portrait, large unfolded
Expanded ≥ 840dp Tablets landscape, desktop

Rule: Compute WindowSizeClass once at app/activity level via currentWindowAdaptiveInfo(). Pass derived layout decisions down as state.

Canonical Layouts

Layout Use case Compose component
List-detail Master list + detail pane ListDetailPaneScaffold, NavigableListDetailPaneScaffold
Supporting pane Main content + supplementary info SupportingPaneScaffold, NavigableSupportingPaneScaffold
Feed Grid of browsable content LazyVerticalGrid with GridCells.Adaptive

Default: For list-detail apps, use NavigableListDetailPaneScaffold which handles pane visibility and back navigation.

Adaptive navigation: read windowSizeClass (or related adaptive info) once at the root and pass derived flags (e.g. whether to show a top app bar) into your main screen composable.

M2 to M3 Migration Notes

M2 M3
Colors ColorScheme
lightColors() / darkColors() lightColorScheme() / darkColorScheme()
BottomNavigation NavigationBar
BottomNavigationItem NavigationBarItem
ModalBottomSheetLayout ModalBottomSheet
ModalDrawer ModalNavigationDrawer
Scaffold with scaffoldState Scaffold with snackbarHost slot
BackdropScaffold BottomSheetScaffold or custom
TopAppBar elevation TopAppBar with scrollBehavior

Key change: M3 Scaffold no longer has drawerState. Use ModalNavigationDrawer wrapping Scaffold instead.