Skip to main content

Add controls to a story

Stories become much more useful when they let you explore a component's states interactively. CStories provides a small set of knobs for this, declared inside a KnobPanel.

Code

KnobPanel​

KnobPanel declares the controls for a story. Call it from within a @CStory composable, alongside the actual preview:

@CStory(collection = "DesignSystem", group = "Buttons", name = "Primary")
@Composable
fun PrimaryButtonStory() {
var label by remember { mutableStateOf("Click me") }

KnobPanel {
TextKnob(label = "Label", value = label, onValueChange = { label = it })
}

PrimaryButton(text = label, onClick = {})
}

When rendered inside the catalog, this content is transparently moved into the runtime's dedicated controls panel, so the canvas only ever shows the story's actual preview.

TextKnob​

A free-text input, for props that take an arbitrary string:

var label by remember { mutableStateOf("Click me") }

KnobPanel {
TextKnob(
label = "Label",
value = label,
onValueChange = { label = it },
codeKey = "label",
)
}

BooleanKnob​

A toggle, for props that take a Boolean:

var enabled by remember { mutableStateOf(true) }

KnobPanel {
BooleanKnob(
label = "Enabled",
value = enabled,
onValueChange = { enabled = it },
codeKey = "enabled",
)
}

IntKnob​

A slider, for props that take an Int within a known range (a progress, a width, a number of items...):

var progress by remember { mutableStateOf(40) }

KnobPanel {
IntKnob(
label = "Progress (%)",
value = progress,
onValueChange = { progress = it },
range = 0..100,
step = 5,
codeKey = "progress",
)
}
  • range sets the minimum and maximum values (both included).
  • step (default 1) makes the slider snap to range.first, range.first + step, and so on. If the range is not a multiple of step, the last reachable value is the highest one that fits.
  • The current value is displayed next to the label, and codeKey substitutes it as an Int literal in the "Code" tab.

SelectKnob​

A dropdown/select-style knob, for props that take one value out of a fixed set of options.

With a list of strings​

var tone by remember { mutableStateOf("Success") }

KnobPanel {
SelectKnob(
label = "Tone",
value = tone,
options = listOf("Success", "Warning", "Danger"),
onValueChange = { tone = it },
codeKey = "tone",
)
}

With an enum​

When the prop is backed by an enum, prefer the enum-typed overload: it derives the list of options from enumValues<T>() automatically, so you never need to keep the option list in sync by hand.

var tone by remember { mutableStateOf(BadgeTone.Success) }

KnobPanel {
SelectKnob(
label = "Tone",
value = tone,
onValueChange = { tone = it },
codeKey = "tone",
)
}

DemoBadge(text = "New", tone = tone)

About codeKey​

codeKey connects a knob's current value to the story's generated "Code" tab: whenever you change a knob, the code snippet shown in the catalog updates to reflect the new value. For the enum overload, this automatically renders the fully-qualified constant (e.g. BadgeTone.Success) instead of a quoted string, so the snippet stays valid Kotlin without any extra work on your part.

The "Code" tab shows every call to the story's component found in the story body, in source order and separated by a blank line. The surrounding code (layouts, loops, knob declarations) is not included.

Best practices​

tip
  • expose knobs for the props that actually matter for exploring the component's states
  • keep a story focused: a handful of well-chosen knobs beats a dozen of everything
  • prefer the enum-typed SelectKnob overload whenever the prop is backed by an enum
  • use IntKnob for numeric props with a bounded range rather than a TextKnob that has to be parsed