Migration to 2.0
How to migrate Compose Unstyled projects to 2.0.
View MarkdownCompose Unstyled 2.0 removes the deprecated 1.x APIs and makes components more strictly unstyled.
The last 1.x release is 1.49.9, published on 2025-04-24. The first 2.0 release is 2.0.0,
published on 2026-05-11. Migrate to 1.49.9 first, apply the available IDE ReplaceWith() fixes,
then upgrade to 2.0.0.
Need help?
Ask migration questions in the Compose Unstyled GitHub repository.
What changed
2.0 is a breaking release focused on three changes:
- The old aggregate
composeunstyledartifact was removed. - The old
com.composables.corepackage was removed. - Components no longer apply styling, layout, or sizing opinions for you.
Most migrations are either dependency changes or moving visual parameters into your own layout and modifiers.
Update dependencies
If you want the same broad API surface as 1.x, replace the old aggregate artifact with
composeunstyled-primitives:
implementation("com.composables:composeunstyled-primitives:2.10.0")For smaller dependency graphs, depend only on the modules you use:
implementation("com.composables:composeunstyled-button:2.10.0")
implementation("com.composables:composeunstyled-dropdown-menu:2.10.0")
implementation("com.composables:composeunstyled-text-field:2.10.0")
implementation("com.composables:composeunstyled-theming:2.10.0")Components and theming are now separate. Add composeunstyled-theming when you use Text,
LocalContentColor, LocalTextStyle, ProvideContentColor, ProvideTextStyle, themes, or minimum
interactive size helpers.
Update imports
Remove imports from com.composables.core. Bottom Sheet and Modal Bottom Sheet APIs now live in
com.composeunstyled.
// 1.x
import com.composables.core.BottomSheet
import com.composables.core.ModalBottomSheet
// 2.0
import com.composeunstyled.UnstyledBottomSheet
import com.composeunstyled.UnstyledModalBottomSheetThe theming APIs moved under com.composeunstyled.theme:
import com.composeunstyled.theme.LocalContentColor
import com.composeunstyled.theme.TextMove styling to your code
2.0 components expose behavior and slots. Visual parameters such as shape, backgroundColor,
contentColor, borderColor, layout arrangements, and many padding/layout parameters were removed
from component APIs.
Move those decisions into modifiers, wrappers, or your design-system components:
// 1.x
UnstyledButton(
onClick = onClick,
shape = RoundedCornerShape(8.dp),
backgroundColor = Color.Black,
contentColor = Color.White,
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 10.dp),
) {
Text("Save")
}
// 2.0
UnstyledButton(
onClick = onClick,
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.background(Color.Black),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 10.dp),
) {
Text("Save", color = Color.White)
}This applies across components: arrange slots with Row, Column, Box, or your own component
wrappers instead of relying on the component to create internal layout.
Replace removed deprecated names
All deprecated 1.x APIs were removed. Prefer this order:
- Upgrade to
1.49.9. - Build the project and use IDE quick fixes for deprecated APIs with
ReplaceWith(). - Upgrade to
2.0.0. - Fix the remaining behavioral API changes below.
Common replacements:
| 1.x | 2.0 |
|---|---|
Button |
UnstyledButton |
DropdownMenu |
UnstyledDropdownMenu |
DropdownMenuPanel |
DropdownMenuPanel scoped inside UnstyledDropdownMenu |
TabGroup, TabList, Tab, TabPanel |
UnstyledTabGroup with scoped TabList, Tab, and TabPanel |
ScrollArea |
Your own scroll container plus UnstyledVerticalScrollbar or UnstyledHorizontalScrollbar |
ScrollAreaState |
ScrollbarState from rememberScrollbarState(...) |
ModalBottomSheet |
UnstyledModalBottomSheet |
Component-specific changes
Bottom Sheet
BottomSheet was split into a container and panel:
val sheetState = rememberBottomSheetState(
initialDetent = SheetDetent.Hidden,
)
UnstyledBottomSheet(state = sheetState) {
Sheet {
DragIndication()
}
}Use sheetState.targetDetent, animateTo(...), or jumpTo(...) to move the sheet. Modal Bottom
Sheet now reuses the same Sheet and DragIndication model.
Modal Bottom Sheet
Pass dimming UI through the new overlay slot. The sheet is IME-aware by default through
ModalBottomSheetProperties.offsetForIme.
val sheetState = rememberModalBottomSheetState(
initialDetent = SheetDetent.Hidden,
)
UnstyledModalBottomSheet(
state = sheetState,
overlay = {
Scrim()
},
) {
Sheet {
DragIndication()
}
}Dialog
UnstyledDialog is controlled with visible. Put the rendered dialog content in DialogPanel and
use paneTitle when the dialog needs an accessible pane title.
var visible by remember { mutableStateOf(false) }
UnstyledDialog(
visible = visible,
onDismissRequest = { visible = false },
) {
DialogPanel(paneTitle = "Settings") {
Text("Settings")
}
}Disclosure
UnstyledDisclosure is now controlled:
var expanded by remember { mutableStateOf(false) }
UnstyledDisclosure(
expanded = expanded,
onExpandedChange = { expanded = it },
) {
DisclosureButton {
Text("Details")
}
DisclosedContent {
Text("More information")
}
}Dropdown Menu
Menu anchor and panel content are now slots on UnstyledDropdownMenu. Use DropdownMenuPanel
and UnstyledDropdownMenuItem.
Dropdown Menu also has new anchor placement parameters: side, alignment, sideOffset, and
alignmentOffset. Use them instead of the old DropdownPanelAnchor values:
var expanded by remember { mutableStateOf(false) }
UnstyledDropdownMenu(
expanded = expanded,
onExpandedChange = { expanded = it },
side = AnchorSide.Bottom,
alignment = AnchorAlignment.End,
sideOffset = 8.dp,
alignmentOffset = 0.dp,
panel = {
DropdownMenuPanel {
UnstyledDropdownMenuItem(onClick = { expanded = false }) {
Text("Item")
}
}
},
anchor = {
Text("Open")
},
)Tooltip
Tooltip placement is now controlled with the same anchor placement model as Dropdown Menu. Use
side, alignment, sideOffset, and alignmentOffset on UnstyledTooltip.
TooltipPanel is scoped inside UnstyledTooltip, and its content receives TooltipPlacement so
custom visuals can react to the resolved placement:
UnstyledTooltip(
side = AnchorSide.Top,
alignment = AnchorAlignment.Center,
sideOffset = 8.dp,
alignmentOffset = 0.dp,
panel = {
TooltipPanel { placement ->
Text("Placed on ${placement.side}")
}
},
anchor = {
Text("Help")
},
)Text Field
UnstyledTextField now uses Compose’s state-based text field API. Store text in a TextFieldState
and render the actual editable text through the scoped TextInput slot.
Text Field does not provide leading or trailing icon slots. Place icons in your own layout around
TextInput:
val state = rememberTextFieldState()
UnstyledTextField(state = state) {
Row {
SearchIcon()
TextInput(
placeholder = {
Text("Email")
},
)
}
}Slider
UnstyledSlider now exposes track and thumb slots that receive a SliderState:
var value by remember { mutableStateOf(0f) }
UnstyledSlider(
value = value,
onValueChange = { value = it },
modifier = Modifier.fillMaxWidth(),
track = { state ->
Box(
Modifier
.fillMaxWidth()
.height(8.dp)
.padding(horizontal = 16.dp)
.clip(RoundedCornerShape(100.dp)),
) {
Box(
Modifier
.fillMaxHeight()
.fillMaxWidth()
.background(Color(0xFFCACACA)),
)
Box(
Modifier
.fillMaxHeight()
.fillMaxWidth(state.fraction)
.background(Color.Black),
)
}
},
thumb = { state ->
val thumbColor = if (state.isDragging) Color.DarkGray else Color.Black
Box(
modifier = Modifier
.size(18.dp)
.clip(CircleShape)
.background(thumbColor),
)
},
)Checkbox, TriState Checkbox, and Radio Group
Indicators are scoped child APIs so they can receive the component interaction source:
var checked by remember { mutableStateOf(false) }
UnstyledCheckbox(
checked = checked,
onCheckedChange = { checked = it },
) {
CheckedIndicator()
}Radio groups also support generic values and scope RadioButton to RadioGroupScope.
Toggle Switch
Switch behavior and thumb placement are split. Put the visual thumb in SwitchThumb:
var checked by remember { mutableStateOf(false) }
UnstyledSwitch(
checked = checked,
onCheckedChange = { checked = it },
) {
SwitchThumb()
}Scrollbars
ScrollArea was removed. Build the scrollable layout yourself and connect scrollbars with
rememberScrollbarState(...):
val scrollState = rememberScrollState()
val scrollbarState = rememberScrollbarState(scrollState)
Box {
Column(Modifier.verticalScroll(scrollState)) {
// content
}
UnstyledVerticalScrollbar(scrollbarState) {
Thumb()
}
}Review behavior changes
After the project compiles, review the screens that use migrated components:
- Add your own size constraints if a component used to fill or align content for you.
- Add your own background, clipping, border, text color, and content color propagation.
- Check modal, dialog, sheet, tooltip, and menu dismissal paths.
- Check keyboard navigation for dropdown menus, tab groups, radio groups, sliders, and sheets.
- Check scrollable content with scrollbars and bottom sheets.
2.0 keeps accessibility and behavior in the components, but your app now owns the visual and layout contract around them.