Documentation
Build your catalog
Scenes & catalog
Organize Compose scenes, configure thumbnails and badges, and use floating tools and the SceneHost API.
Organizing Scenes
Composium does not force a single scene structure.
Flat scenes
Scenes without a group stay at the root level:
internal val typographyTokens by scene { contentPadding ->
Box(Modifier.padding(contentPadding)) {
TypographyShowcase()
}
}
Grouped scenes
Use slash-separated paths to build nested groups:
internal val primaryFilled by scene(group = "Buttons/Primary/Filled") { contentPadding ->
Box(Modifier.padding(contentPadding)) {
PrimaryFilledButton()
}
}
internal val primaryOutlined by scene(group = "Buttons/Primary/Outlined") { contentPadding ->
Box(Modifier.padding(contentPadding)) {
PrimaryOutlinedButton()
}
}
internal val dangerFilled by scene(group = "Buttons/Danger/Filled") { contentPadding ->
Box(Modifier.padding(contentPadding)) {
DangerButton()
}
}
Group nesting is unlimited. The UI tree is derived from the group path, not from where the property is declared in code.
That means you can:
- keep scenes as a flat list in source code;
- group them with
group = "..."; - or combine that with
@ComposiumSceneCatalogobjects for code organization.
Scene content padding and tools
Every scene receives the full preview bounds and an explicit contentPadding. For regular content, apply it to the root so content stays clear of system bars and the default Composium top bar:
val RegularScene by scene { contentPadding ->
Box(Modifier.fillMaxSize().padding(contentPadding)) {
Content()
}
}
val FullScreenScene by scene(
tools = SceneTools.Floating(),
) { contentPadding ->
Box(Modifier.fillMaxSize()) {
FullScreenBackground()
Actions(Modifier.padding(contentPadding))
}
}
For edge-to-edge content, apply contentPadding only to the children that must remain unobscured. SceneTools.TopBar is the default. Use SceneTools.Floating() to replace the top bar with a compact overlay containing Back, Properties, Eyedropper, and Theme controls.
Floating tools do not contribute to contentPadding or appear in eyedropper samples.
Configure their initial position with SceneTools.Floating(initialPosition = FloatingToolsPosition.CenterStart).
Pass actionsInitiallyExpanded = true to show secondary actions when the scene opens (default: false).
Available positions are TopStart, CenterStart, BottomStart, TopEnd,
CenterEnd (default), and BottomEnd. Start and end follow the host's layout direction:
start is left in LTR and right in RTL; end is the opposite.
Positions respect system insets. The initial position applies until the user drags the toolbar;
reopening the scene restores it.
Use SceneTools.None to omit Composium's built-in toolbars and provide your own UI.
System insets still contribute to contentPadding; no toolbar space is reserved.
The bundled lint check reports an error when scene content does not reference its padding. For intentional full-bleed content, suppress that one issue explicitly:
@Suppress("UnusedComposiumContentPaddingParameter")
val BackgroundScene by scene {
FullScreenBackground()
}
Naming the lambda argument _ still reports an error. The rule checks for an explicit reference or an explicit suppression; it does not try to prove where the padding was applied.
Custom scene tools
SceneScope.host provides a SceneHost for controlling the Composium UI around your scene.
Its observable tool states and navigation actions are available in every tools mode:
| State | Read | Commands |
|---|---|---|
host.controls |
isVisible |
show(), hide(), toggle() |
host.eyedropper |
isVisible |
show(), hide(), toggle() |
host.theme |
isDark |
setDark(Boolean), toggle() |
val ProfileScene by scene(tools = SceneTools.None) { contentPadding ->
Column(Modifier.padding(contentPadding)) {
MyTools(
settingsActive = host.controls.isVisible,
eyedropperActive = host.eyedropper.isVisible,
isDarkTheme = host.theme.isDark,
onSettingsClick = host.controls::toggle,
onEyedropperClick = host.eyedropper::toggle,
onThemeClick = host.theme::toggle,
onBack = host.onBack,
)
ProfileContent()
}
}
Composium owns these objects; no additional remember or state copies are needed.
They reflect the same state as the built-in tools, including changes from Back and gestures.
host.controls.show() opens a closed panel in split mode and preserves an already open panel.
The eyedropper cannot be opened while controls occupy the full screen.
Theme commands use ComposiumScreen's existing onThemeChange handling. If you supply
isDarkTheme to ComposiumScreen, apply the requested value in the host; host.theme.isDark
reflects the effective theme, not a pending request.
Call commands from event handlers or effects, not directly during composition. Commands
do nothing in thumbnails, RenderPreview(), or after the scene leaves composition.
In thumbnails, host.theme.isDark reports the theme used to capture the image;
controls and eyedropper visibility remain false. In RenderPreview() or after an
opened scene leaves composition, state objects report false.
None hides the toolbars, not the built-in
Properties / Environment panel or eyedropper that these commands operate.
Closing a scene from your own UI
Use host.closeScene() to connect your screen's own Back button to the catalog:
val ProfileScene by scene(tools = SceneTools.Floating()) { contentPadding ->
ProfileScreen(
modifier = Modifier.padding(contentPadding),
onBack = host::closeScene,
)
}
ProfileScreen only needs an ordinary onBack: () -> Unit callback and does not need to depend on Composium.
host.closeScene() returns directly to the catalog, even when Properties or the eyedropper is open. It does not change the tools-first behavior of Composium's own Back button or system Back.
To match Composium's own Back button instead, pass the host.onBack callback directly:
val ProfileScene by scene(tools = SceneTools.Floating()) { contentPadding ->
ProfileScreen(
modifier = Modifier.padding(contentPadding),
onBack = host.onBack,
)
}
Each call performs one step: close the eyedropper if open; otherwise restore expanded controls to split mode; otherwise hide the controls; otherwise return to the catalog.
Invoke either action from an event handler or effect, not directly during composition. Both do nothing in thumbnails and RenderPreview(), or after their scene's screen leaves composition. Retained callbacks from an old opening cannot affect a later opening.
Scene thumbnails
The main catalog screen automatically creates thumbnails for scenes. By default, Composium renders each scene on a hidden capture surface, stores the resulting image in memory, and shows that image in the scene card thumbnail area.
This keeps the catalog visual without requiring extra setup for every scene:
internal val primaryButton by scene(group = "Buttons") { contentPadding ->
Box(Modifier.padding(contentPadding)) {
PrimaryButton(text = "Continue", onClick = {})
}
}
If the full scene is too heavy, too large, animated, or not representative enough for a compact card, provide a custom thumbnail. The custom thumbnail is used only for catalog thumbnail capture; opening the scene still renders the regular scene content.
internal val paymentForm by scene(
group = "Forms",
thumbnail = {
PaymentFormPreview()
},
) { contentPadding ->
Box(Modifier.padding(contentPadding)) {
PaymentForm()
}
}
Use custom thumbnails for cases where the catalog should show a simplified, stable, or intentionally framed version of the component while keeping the real scene interactive and complete.
Omitting thumbnail captures the scene's content. Passing thumbnail = null explicitly disables capture for that scene and removes the entire preview area from its catalog card, without a placeholder or reserved space:
internal val paymentScreen by scene(thumbnail = null) { contentPadding ->
PaymentScreen(Modifier.padding(contentPadding))
}
The card remains clickable and opens the scene normally. If a badge is provided, it appears beside the card title instead of over a preview. These defaults also apply to scenes created directly through Scene.
A nullable thumbnail variable that evaluates to null also disables capture.
Thumbnail capture runs a real composition: effects in the captured content can start requests, subscriptions, or analytics before the scene is opened. Use a custom thumbnail with static sample data, or disable capture, for scenes with such effects. A custom thumbnail is also ordinary composable content and does not automatically suppress effects.
Scene card badges
Use badge when a scene needs an extra marker inside its catalog card. Composium renders it in the top-end corner of the thumbnail area, or beside the title when thumbnail = null; the badge content controls its own size, shape, colors, and behavior.
This is useful for lightweight per-scene metadata that should be visible before opening the scene, for example:
- readiness status: draft, ready for review, reviewed;
- QA or design-review state;
- platform or feature availability markers;
- warnings for incomplete, deprecated, or experimental components;
- selection or pinning indicators in a custom catalog flow.
The badge is regular Compose content. It can be a small colored dot, text label, icon, progress marker, or an interactive control if your catalog UX needs it.
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
internal val paymentButton by scene(
group = "Buttons",
badge = {
// Reviewed
Box(
modifier = Modifier
.size(10.dp)
.clip(CircleShape)
.background(Color(0xFF2E7D32)),
)
},
) { contentPadding ->
Box(Modifier.padding(contentPadding)) {
PaymentButton()
}
}