Custom design systems in Compose
Stay organized with collections
Save and categorize content based on your preferences.
While Material is our recommended design system and Jetpack Compose ships an implementation of Material, you are not forced to use it. Material is built entirely on public APIs, so it's possible to create your own design system in the same manner.
There are several approaches you might take:
- Extend
MaterialThemewith additional theming values. - Replace one or more Material systems —
Colors,Typography, orShapes— with custom implementations while keeping the others. - Implement a fully custom design system to replace
MaterialTheme.
You may also want to continue using Material components with a custom design system. It's possible to do this but there are things to keep in mind to suit the approach you've taken.
To learn more about the lower-level constructs and APIs used by MaterialTheme
and custom design systems, check out the Anatomy of a theme in Compose guide.
Extend Material Theming
Compose Material closely models Material Theming to make it straightforward and type-safe to follow the Material guidelines. However, it's possible to extend the color, typography, and shape sets with additional values. The simplest approach is to add extension properties:
// Use with MaterialTheme.colorScheme.snackbarAction
valColorScheme.snackbarAction:Color
@Composable
get()=if(isSystemInDarkTheme())Red300elseRed700
// Use with MaterialTheme.typography.textFieldInput
valTypography.textFieldInput:TextStyle
get()=TextStyle(/* ... */)
// Use with MaterialTheme.shapes.card
valShapes.card:Shape
get()=RoundedCornerShape(size=20.dp)
This provides consistency with MaterialTheme usage APIs. An example of this
defined by Compose itself is
surfaceColorAtElevation,
which determines the surface color that should be used depending on the
elevation.
Another approach is to define an extended theme that "wraps" MaterialTheme and
its values.
Suppose you want to add two additional colors — caution and onCaution, a
yellow color used for actions that are semi-dangerous — whilst keeping the
existing Material colors:
@Immutable
dataclassExtendedColors(
valcaution:Color,
valonCaution:Color
)
valLocalExtendedColors=staticCompositionLocalOf{
ExtendedColors(
caution=Color.Unspecified,
onCaution=Color.Unspecified
)
}
@Composable
funExtendedTheme(
/* ... */
content:@Composable()->Unit
){
valextendedColors=ExtendedColors(
caution=Color(0xFFFFCC02),
onCaution=Color(0xFF2C2D30)
)
CompositionLocalProvider(LocalExtendedColorsprovidesextendedColors){
MaterialTheme(
/* colors = ..., typography = ..., shapes = ... */
content=content
)
}
}
// Use with eg. ExtendedTheme.colors.caution
objectExtendedTheme{
valcolors:ExtendedColors
@Composable
get()=LocalExtendedColors.current
}
This is similar to MaterialTheme usage APIs. It also supports multiple themes
as you can nest ExtendedThemes in the same way as MaterialTheme.
Use Material components
When extending Material Theming, existing MaterialTheme values are maintained
and Material components still have reasonable defaults.
If you want to use extended values in components, wrap them in your own composable functions, directly setting the values you want to alter, and exposing others as parameters to the containing composable:
@Composable
funExtendedButton(
onClick:()->Unit,
modifier:Modifier=Modifier,
content:@ComposableRowScope.()->Unit
){
Button(
colors=ButtonDefaults.buttonColors(
containerColor=ExtendedTheme.colors.caution,
contentColor=ExtendedTheme.colors.onCaution
/* Other colors use values from MaterialTheme */
),
onClick=onClick,
modifier=modifier,
content=content
)
}
You would then replace usages of Button with ExtendedButton where
appropriate.
@Composable
funExtendedApp(){
ExtendedTheme{
/*...*/
ExtendedButton(onClick={/* ... */}){
/* ... */
}
}
}
Replace Material subsystems
Instead of extending Material Theming, you may want to replace one or more
systems — Colors, Typography, or Shapes — with a custom implementation,
while maintaining the others.
Suppose you want to replace the type and shape systems while keeping the color system:
@Immutable
dataclassReplacementTypography(
valbody:TextStyle,
valtitle:TextStyle
)
@Immutable
dataclassReplacementShapes(
valcomponent:Shape,
valsurface:Shape
)
valLocalReplacementTypography=staticCompositionLocalOf{
ReplacementTypography(
body=TextStyle.Default,
title=TextStyle.Default
)
}
valLocalReplacementShapes=staticCompositionLocalOf{
ReplacementShapes(
component=RoundedCornerShape(ZeroCornerSize),
surface=RoundedCornerShape(ZeroCornerSize)
)
}
@Composable
funReplacementTheme(
/* ... */
content:@Composable()->Unit
){
valreplacementTypography=ReplacementTypography(
body=TextStyle(fontSize=16.sp),
title=TextStyle(fontSize=32.sp)
)
valreplacementShapes=ReplacementShapes(
component=RoundedCornerShape(percent=50),
surface=RoundedCornerShape(size=40.dp)
)
CompositionLocalProvider(
LocalReplacementTypographyprovidesreplacementTypography,
LocalReplacementShapesprovidesreplacementShapes
){
MaterialTheme(
/* colors = ... */
content=content
)
}
}
// Use with eg. ReplacementTheme.typography.body
objectReplacementTheme{
valtypography:ReplacementTypography
@Composable
get()=LocalReplacementTypography.current
valshapes:ReplacementShapes
@Composable
get()=LocalReplacementShapes.current
}
Use Material components
When one or more systems of MaterialTheme have been replaced, using Material
components as-is may result in unwanted Material color, type, or shape values.
If you want to use replacement values in components, wrap them in your own composable functions, directly setting the values for the relevant system, and exposing others as parameters to the containing composable.
@Composable
funReplacementButton(
onClick:()->Unit,
modifier:Modifier=Modifier,
content:@ComposableRowScope.()->Unit
){
Button(
shape=ReplacementTheme.shapes.component,
onClick=onClick,
modifier=modifier,
content={
ProvideTextStyle(
value=ReplacementTheme.typography.body
){
content()
}
}
)
}
You would then replace usages of Button with ReplacementButton where
appropriate.
@Composable
funReplacementApp(){
ReplacementTheme{
/*...*/
ReplacementButton(onClick={/* ... */}){
/* ... */
}
}
}
Implement a fully custom design system
You may want to replace Material Theming with a fully custom design system.
Consider that MaterialTheme provides the following systems:
Colors,Typography, andShapes: Material Theming systemsTextSelectionColors: Colors used for text selection byTextandTextFieldRippleandRippleTheme: Material implementation ofIndication
If you want to continue using Material components, you must replace some of these systems in your custom themes or handle the systems in your components to avoid unwanted behavior.
However, design systems are not limited to the concepts Material relies on. You can modify existing systems and introduce entirely new ones — with new classes and types — to make other concepts compatible with themes.
In the following code, we model a custom color system that includes gradients
(List<Color>), include a type system, introduce a new elevation system,
and exclude other systems provided by MaterialTheme:
Screenshot of a mobile app UI demonstrating a custom design system with elements using gradients for colors, custom typography, and elevation.
@Immutable
dataclassCustomColors(
valcontent:Color,
valcomponent:Color,
valbackground:List<Color>
)
@Immutable
dataclassCustomTypography(
valbody:TextStyle,
valtitle:TextStyle
)
@Immutable
dataclassCustomElevation(
valdefault:Dp,
valpressed:Dp
)
valLocalCustomColors=staticCompositionLocalOf{
CustomColors(
content=Color.Unspecified,
component=Color.Unspecified,
background=emptyList()
)
}
valLocalCustomTypography=staticCompositionLocalOf{
CustomTypography(
body=TextStyle.Default,
title=TextStyle.Default
)
}
valLocalCustomElevation=staticCompositionLocalOf{
CustomElevation(
default=Dp.Unspecified,
pressed=Dp.Unspecified
)
}
@Composable
funCustomTheme(
/* ... */
content:@Composable()->Unit
){
valcustomColors=CustomColors(
content=Color(0xFFDD0D3C),
component=Color(0xFFC20029),
background=listOf(Color.White,Color(0xFFF8BBD0))
)
valcustomTypography=CustomTypography(
body=TextStyle(fontSize=16.sp),
title=TextStyle(fontSize=32.sp)
)
valcustomElevation=CustomElevation(
default=4.dp,
pressed=8.dp
)
CompositionLocalProvider(
LocalCustomColorsprovidescustomColors,
LocalCustomTypographyprovidescustomTypography,
LocalCustomElevationprovidescustomElevation,
content=content
)
}
// Use with eg. CustomTheme.elevation.small
objectCustomTheme{
valcolors:CustomColors
@Composable
get()=LocalCustomColors.current
valtypography:CustomTypography
@Composable
get()=LocalCustomTypography.current
valelevation:CustomElevation
@Composable
get()=LocalCustomElevation.current
}
Use Material components
When no MaterialTheme is present, using Material components as-is will result
in unwanted Material color, type, and shape values and indication behavior.
If you want to use custom values in components, wrap them in your own composable functions, directly setting the values for the relevant system, and exposing others as parameters to the containing composable.
We recommend that you access values you set from your custom theme.
Alternatively, if your theme doesn't provide Color, TextStyle, Shape, or
other systems, you can hardcode them.
@Composable
funCustomButton(
onClick:()->Unit,
modifier:Modifier=Modifier,
content:@ComposableRowScope.()->Unit
){
Button(
colors=ButtonDefaults.buttonColors(
containerColor=CustomTheme.colors.component,
contentColor=CustomTheme.colors.content,
disabledContainerColor=CustomTheme.colors.content
.copy(alpha=0.12f)
.compositeOver(CustomTheme.colors.component),
disabledContentColor=CustomTheme.colors.content
.copy(alpha=0.38f)
),
shape=ButtonShape,
elevation=ButtonDefaults.elevatedButtonElevation(
defaultElevation=CustomTheme.elevation.default,
pressedElevation=CustomTheme.elevation.pressed
/* disabledElevation = 0.dp */
),
onClick=onClick,
modifier=modifier,
content={
ProvideTextStyle(
value=CustomTheme.typography.body
){
content()
}
}
)
}
valButtonShape=RoundedCornerShape(percent=50)
If you've introduced new class types — such as List<Color> to represent
gradients — then it may be better to implement components from scratch instead
of wrapping them. For an example, take a look at
JetsnackButton
from the Jetsnack sample.
Recommended for you
- Note: link text is displayed when JavaScript is off
- Material Design 3 in Compose
- Migrate from Material 2 to Material 3 in Compose
- Anatomy of a theme in Compose