State & Controls
Controls give you the ability to interact with your components arguments.
Defining state
Declare the state with initState on <Hst.Variant> (or <Hst.Story>), and read it from the state your content receives. It is an ordinary prop, so it belongs in your <script>:
<script>
import MyButton from './MyButton.svelte'
export let Hst
const initState = () => ({
disabled: false,
content: 'Click me!',
})
</script>
<Hst.Story>
<Hst.Variant {initState}>
{#snippet children({ state })}
<MyButton disabled={state.disabled}>
{state.content}
</MyButton>
<input bind:value={state.content}>
{/snippet}
</Hst.Variant>
</Hst.Story>initState is a function rather than an object because every variant gets its own copy.
State cannot live in a component variable
A let disabled = false in your story will not work, even though it looks like it should.
Poveste mounts your story once per slot — the app renders it to fill the controls panel, the preview renders it to show the story — so a component-local variable exists twice. A control writes to one copy while your component reads the other, and nothing appears to happen.
Svelte 4 papered over this by capturing one instance's internals and injecting them into the other. Svelte 5 removed that, deliberately: component internals are private. So Poveste owns the state instead, which is also what lets it cross the sandbox iframe.
A story that has controls but no initState logs an error in the console saying exactly this.
Values that are genuinely local — a bind:this node, a DOM ref — should stay in the component. Only what your controls drive needs to be state.
Controls panel
To create the control panel, Poveste provides a controls snippet on <Hst.Variant> (and <Hst.Story>, more on that later). You are free to render any element or components inside it, and it receives the same state.
<script>
import MyButton from './MyButton.svelte'
export let Hst
const initState = () => ({
disabled: false,
content: 'Hello world',
})
</script>
<Hst.Story>
<Hst.Variant {initState}>
{#snippet children({ state })}
<MyButton disabled={state.disabled}>
{state.content}
</MyButton>
{/snippet}
{#snippet controls({ state })}
Content: <input type="text" bind:value={state.content} />
Disabled: <input type="checkbox" bind:checked={state.disabled} />
{/snippet}
</Hst.Variant>
</Hst.Story>You can also share the same default controls for all variants by putting the snippet directly under the <Hst.Story> component, with the state declared there too:
<Hst.Story {initState}>
{#snippet controls({ state })}
Content: <input type="text" bind:value={state.content} />
Disabled: <input type="checkbox" bind:checked={state.disabled} />
{/snippet}
<Hst.Variant title="Variant 1">
{#snippet children({ state })}
<MyButton disabled={state.disabled}>
{state.content}
</MyButton>
{/snippet}
</Hst.Variant>
<Hst.Variant title="Variant 2">
{#snippet children({ state })}
<MyButton disabled={state.disabled}>
{state.content}
</MyButton>
{/snippet}
</Hst.Variant>
</Hst.Story>A variant can then override the snippet if needed.
Note that the state is still per variant: shared controls drive whichever variant you are looking at, and each keeps its own values. Only variant state crosses the sandbox iframe, so there is no story-wide bucket to share.
Source code from state
The source panel shows the source prop. Because props are evaluated in your <script>, where state is not in scope, a plain string cannot follow the controls — pass a function instead:
<script lang="ts">
import type { Hst, StoryState } from '@poveste/plugin-svelte'
export let Hst: Hst
const initState = () => ({ disabled: false })
function source(state: StoryState) {
const attrs = state.disabled ? ' disabled' : ''
return `<MyButton${attrs}>Click me !</MyButton>`
}
</script>
<Hst.Story {initState} {source}>
{#snippet children({ state })}
<MyButton disabled={state.disabled}>Click me !</MyButton>
{/snippet}
</Hst.Story>Builtin controls
To build a control panel a bit more easily, Poveste provides builtin controls with design that fits the rest of the UI.
<script>
import MyButton from './MyButton.svelte'
export let Hst
const initState = () => ({
disabled: false,
content: 'Hello world',
})
</script>
<Hst.Story>
<Hst.Variant {initState}>
{#snippet children({ state })}
<MyButton disabled={state.disabled}>
{state.content}
</MyButton>
{/snippet}
{#snippet controls({ state })}
<Hst.Text bind:value={state.content} title="Content" />
<Hst.Checkbox bind:value={state.disabled} title="Disabled" />
{/snippet}
</Hst.Variant>
</Hst.Story>Check out all the available controls in the @poveste/controls package.