Skip to main content

Customize the catalog theme

Every story's canvas has a light/dark switch in its toolbar, letting you check how a component looks against both backgrounds without leaving the catalog.

Dark theme

Default behavior

By default, toggling it wraps the previewed story in a plain Material3 MaterialTheme using darkColorScheme()/lightColorScheme() — enough for components that already rely on MaterialTheme.colorScheme for their colors.

Using your own theme

If your design system uses its own theme instead of Material3 (a MyTheme(isDark) { ... }, for example), define a wrapper object:

import io.cstories.runtime.CStoriesThemeWrapper

object CustomCStoriesThemeWrapper : CStoriesThemeWrapper {
@Composable
override operator fun invoke(isDark: Boolean, content: @Composable () -> Unit) {
MyTheme(isDark = isDark, content = content)
}
}

The object is discovered automatically by the CStories plugin. No Gradle configuration is required. If the object is not present, DefaultCStoriesThemeWrapper is used.

Override a story

A story can use another theme without changing the global catalog wrapper. First define an object that implements CStoriesThemeWrapper. The object must implement invoke, which receives the selected light/dark state and must render the story inside your theme:

object MyThemeWrapper : CStoriesThemeWrapper {
@Composable
override operator fun invoke(isDark: Boolean, content: @Composable () -> Unit) {
MyTheme(isDark = isDark, content = content)
}
}

Then reference that object from the story's themeWrapper argument:


@CStory(
collection = "Components",
group = "Buttons",
name = "Alternative button",
themeWrapper = MyThemeWrapper::class,
)
@Composable
fun AlternativeButtonStory() {
AlternativeButton()
}

The story-specific wrapper takes priority over the wrapper passed to CStoriesApp. Stories without a themeWrapper argument use the catalog wrapper, or DefaultCStoriesThemeWrapper when no global wrapper was discovered.

Device previews

When a story uses DevicePreview, MobileDevicePreview, or DesktopDevicePreview, the light/dark selection is applied inside the simulated viewport. The phone or desktop window content is therefore rendered with the selected theme, while the surrounding canvas keep the catalog UI theme.

@CStory(collection = "Screens", group = "Profile", name = "Responsive")
@Composable
fun ResponsiveProfileStory() {
DevicePreview {
ProfileScreen()
}
}

For a direct MobileDevicePreview or DesktopDevicePreview, the same rule applies: only the content inside the simulated device is wrapped by the selected theme.