Structure a multi-module project
For a reusable or published component library, keep components and stories in separate modules.
The dev.cstories.gradle plugin adds cstories-runtime and the catalog infrastructure to the module where it is
applied. If that module is the published component library, consumers may be required to resolve CStories runtime
dependencies even when they only use the components.
A single-module setup is appropriate only for an application-owned design system or a module that is not published as a reusable library.
When a single module is acceptable
If your non-published design system module already targets jvm() (or you're fine adding it), you can apply the
CStories plugin directly on it. A jvm()-only catalog needs no wasmJs target at all, so it never forces Gradle to resolve
commonMain dependencies for a platform your module doesn't otherwise support:
// lib/build.gradle.kts
plugins {
kotlin("multiplatform") version "2.2.0"
id("org.jetbrains.compose") version "1.8.2"
id("org.jetbrains.kotlin.plugin.compose") version "2.2.0"
id("dev.cstories.gradle") version "1.1.5"
}
kotlin {
jvm()
}
Recommended structure for published libraries
The component module must not apply dev.cstories.gradle. It should only apply dev.cstories.gradle.components when
it needs component references. That plugin publishes component metadata, not the generated CStoryComponentRefs API.
The stories module is the only module that applies dev.cstories.gradle. This keeps cstories-runtime, catalog tasks,
and CStories-specific dependencies outside the published component library.
Why split components and stories
If you also want (or only want) the web catalog, and your design system module already targets other platforms and
pulls in dependencies that aren't published for wasmJs (a private icon library, a platform-specific SDK, ...),
applying the plugin directly to that module with wasmJs declared forces Gradle to resolve all of its
commonMain dependencies for wasmJs too — the build can break with errors like:
Could not resolve com.example:some-native-only-lib:1.0.0.
Required by:
project :lib
The recommended structure mirrors CStories' own core principle (@CStory never lives on the design system component
itself) at the module level: keep a separate stories module that depends on :lib as a regular dependency, and
is the only place wasmJs and the CStories plugin get applied. :lib itself stays completely untouched — no new
target, no new dependency resolution constraints.
:lib // your design system, untouched — jvm, ios, android, whatever it already targets
:lib:stories // new module — depends on :lib, applies the CStories plugin, targets wasmJs (and/or jvm)
Setting it up
settings.gradle.kts:
include(":lib", ":lib:stories")
lib/stories/build.gradle.kts:
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
kotlin("multiplatform") version "2.2.0"
id("org.jetbrains.compose") version "1.8.2"
id("org.jetbrains.kotlin.plugin.compose") version "2.2.0"
id("dev.cstories.gradle") version "1.1.5"
}
@OptIn(ExperimentalWasmDsl::class)
kotlin {
jvm() // optional — add it for a desktop catalog alongside the web one
wasmJs {
browser()
binaries.executable()
}
sourceSets {
commonMain.dependencies {
implementation(project(":lib"))
}
}
}
Stories live in lib/stories/src/commonMain, importing components from :lib and demonstrating them. :lib never
depends on CStories, and :lib:stories never needs to resolve :lib's non-wasmJs-published dependencies for any
target other than wasmJs.
The plugin takes care of the rest for whichever target(s) you declared: it wires cstories-annotations,
cstories-runtime, and the cstories-processor KSP dependency, and generates the catalog's entry point(s). These
dependencies belong to the stories module and are not required by consumers of :lib.
What's next
If your component and its story live in different modules, referencing the component safely from the story requires one extra step — see Reference a component in a story.