Compose Unstyled
repository·main·Indexed 22 days ago
https://github.com/composablehorizons/compose-unstyledA library built on Compose Foundation providing renderless, accessible UI components and a flexible theming system for cross-platform design systems. It includes headless components such as UnstyledCheckbox, UnstyledTabGroup, Bottom Sheets, Dropdown Menus, and Animated Dialogs, as well as tools for defining responsive window width and height breakpoints.
What's inside compose-unstyled
- Compose Unstyled is a library built on top of Compose Foundation. It provides fully-renderless and fully accessible components, such as Bottom Sheets, Dropdown Menus, and Animated Dialogs. It includes a flexible theming system designed to allow developers to build custom design systems across any platform supported by Compose, extending beyond just Android.
What is Compose Unstyled?
mainCompose Unstyled is a library of unstyled, accessible building blocks for Jetpack Compose and Compose Multiplatform. Unlike Material Compose, which enforces Google's Material Design, Compose Unstyled provides components with zero default styling. This allows developers to build custom design systems without having to implement complex UX details, accessibility semantics, or keyboard navigation from scratch.
Key characteristics:
- Zero Styling: Components render nothing visually by design; you provide all the styling.
- Themable: You can create themes using your own design system's tokens.
- Accessible: Components support keyboard navigation and follow ARIA spec semantics (tested on Android TalkBack).
- Multiplatform: The public API is platform-agnostic, ensuring consistent behavior across desktop, web, and mobile.
How UnstyledTextField and TextInput work together
mainThe text field is composed of two main parts:
UnstyledTextField: The container that manages the state, configuration (like line limits, keyboard options, and styling), and accessibility.TextInput: The component placed inside theUnstyledTextFieldcontent slot that actually renders the editable text and optional placeholder.
To use them, pass a
TextFieldStateto the container and callTextInput()within its trailing lambda.UnstyledTextField(state = state) { TextInput() }How Toggle Switch and SwitchThumb work together
mainThe Toggle Switch component is composed of two main parts:
UnstyledSwitch: Represents the interactive switch container. It manages the checked state and interaction logic.SwitchThumb: A slot used to place content (the 'thumb') at the start or end of the switch layout.
To build a switch, you wrap
SwitchThumbinside theUnstyledSwitchcontent lambda.UnstyledSwitch( checked = checked, onCheckedChange = onCheckedChange, ) { SwitchThumb { // Thumb content goes here } }How UnstyledSlider works
mainThe
UnstyledSlideris a component that represents an interactive range. It is highly customizable through slots:trackslot: Renders the visual track of the slider. It is rendered below the thumb.thumbslot: Renders the interactive handle at the current slider offset.
Both slots receive a
SliderStateobject, which provides access to the current slider state (such as the current value) to allow for reactive styling or content.UnstyledSlider( value = value, onValueChange = onValueChange, track = { state -> /* Render track using state */ }, thumb = { state -> /* Render thumb using state */ }, )How scrollbars work in Compose Unstyled
mainScrollbars are composed of three main parts:
ScrollbarState: Represents the scroll position and is connected to the scrollable content's state.UnstyledVerticalScrollbar/UnstyledHorizontalScrollbar: The container components that render the scrollbar track.Thumb: The draggable element rendered inside the scrollbar container.
You connect them by creating a state using
rememberScrollbarState(targetState)and passing that state to the scrollbar component.val scrollbarState = rememberScrollbarState(scrollState) UnstyledVerticalScrollbar(scrollbarState) { Thumb() }How UnstyledProgress and Indicator work together
mainThe Progress Indicator is composed of two main parts:
UnstyledProgress(): Represents the visible bounds of the progress, including the track.Indicator(): A component that fills the available width based on the current progress value.
To use them, place the
Indicator()inside theUnstyledProgresscontent lambda.UnstyledProgress(progress = progress) { Indicator() }How Bottom Sheet components work together
mainThe Bottom Sheet implementation is composed of several key abstractions:
SheetDetent: Defines a specific height where the sheet can rest. Detents can be predefined (likeSheetDetent.HiddenorSheetDetent.FullyExpanded) or custom.UnstyledBottomSheet: The main draggable container that manages the sheet's state and movement.Sheet: The component that renders the actual visible part of the sheet.DragIndication: An interactive handle added inside theSheetto provide expand, collapse, and dismiss actions. It is highly recommended for accessibility, as it provides semantic actions for users who cannot drag.
val sheetState = rememberBottomSheetState( initialDetent = SheetDetent.Hidden, ) UnstyledBottomSheet(state = sheetState) { Sheet { DragIndication() } }How Modal Bottom Sheet components work together
mainThe Modal Bottom Sheet is composed of several key abstractions:
UnstyledModalBottomSheet: The top-level component representing the modal layer.Sheet: The actual rendered content area of the sheet.SheetDetent: Defines specific height levels where the sheet can rest (e.g.,SheetDetent.Hidden,SheetDetent.FullyExpanded).DragIndication: An interactive handle used for expand, collapse, and dismiss actions. It also provides semantic accessibility actions for users to control the sheet without dragging.ModalBottomSheetProperties: Configuration object used to control behavior like IME (keyboard) offset and dismissal logic.
val sheetState = rememberModalBottomSheetState( initialDetent = SheetDetent.Hidden, ) UnstyledModalBottomSheet(state = sheetState) { Sheet { DragIndication() } }How to use Compose Unstyled components
mainCompose Unstyled is not a standalone component library; it is a set of building blocks used to create your own component library.
Instead of using a component as a finished UI element, you use it to handle the logic, accessibility, and state management, then apply your own styling to it. This is particularly useful when you need components that behave like standard UI elements (e.g., a
ModalBottomSheet) but require a custom look or more flexible API than Material Compose provides.How Portal and PortalHost work together
mainThe Portal utility uses a host-destination model to render content from one part of the composition into a shared location:
PortalHost: Acts as the destination. It defines the location where all child portal content will be rendered.Portal: Acts as the sender. It sends its content to the nearest availablePortalHostin the composition tree. If noPortalHostis found, thePortalrenders nothing.
Rendering Order: Portal content is rendered after the host's own content, but within the same window.
PortalHost { // Host content renders first Portal { // Portal content renders here, after host content } }How the Stack component works
mainThe
Stackcomponent is a single layout utility used to arrange children in either a horizontal or vertical orientation.Key behaviors:
- Orientation: Controls whether children are laid out in a row or a column.
- Spacing: Can apply uniform gaps between children.
- Alignment: Uses
mainAxisArrangementfor alignment along the primary direction andcrossAxisAlignmentfor alignment perpendicular to that direction. - Space Distribution: Uses the
weight()modifier on children to distribute any remaining space in the stack's current orientation.