Compose Unstyled 2.0 is out! Check the official announcement blog ->

Migration to 2.0

How to migrate Compose Unstyled projects to 2.0.

Compose Unstyled 2.0 removes the deprecated 1.x APIs and makes primitives 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?

Check the Compose Unstyled community to ask questions about your migration.

What changed

2.0 is a breaking release focused on three changes:

  • The old aggregate composeunstyled artifact was removed.
  • The old com.composables.core package was removed.
  • Primitives 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")

For smaller dependency graphs, depend only on the modules you use:

implementation("com.composables:composeunstyled-button")
implementation("com.composables:composeunstyled-dropdown-menu")
implementation("com.composables:composeunstyled-text-field")
implementation("com.composables:composeunstyled-theming")

Primitives 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.UnstyledModalBottomSheet

The theming APIs moved under com.composeunstyled.theme:

import com.composeunstyled.theme.LocalContentColor
import com.composeunstyled.theme.Text

Move styling to your code

2.0 primitives 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 primitives: arrange slots with Row, Column, Box, or your own component wrappers instead of relying on the primitive to create internal layout.

Replace removed deprecated names

All deprecated 1.x APIs were removed. Prefer this order:

  1. Upgrade to 1.49.9.
  2. Build the project and use IDE quick fixes for deprecated APIs with ReplaceWith().
  3. Upgrade to 2.0.0.
  4. 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.

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")
  }
}

Menu anchor and panel content are now slots on UnstyledDropdownMenu. Use scoped DropdownMenuPanel and MenuItem.

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 {
      MenuItem(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 primitive 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 primitives:

  • Add your own size constraints if a primitive 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 primitives, but your app now owns the visual and layout contract around them.