Storybook Controls
Storybook Controls gives you UI to interact with a component's inputs dynamically, without needing to code. It creates an addon panel next to your component examples ("stories"), so you can edit them live.
It does not require any modification to your components, and stories for controls are:
- Convenient. Auto-generate controls based on React/Vue/Angular/etc. components.
- Portable. Reuse your interactive stories in documentation, tests, and even in designs.
- Rich. Customize the controls and interactive data to suit your exact needs.
Controls are built on top of Storybook Args, which is an open, standards-based format that enable stories to be reused in a variety of contexts.
- Documentation. 100% compatible with Storybook Docs.
- Testing. Import stories directly into your Jest tests.
- Ecosystem. Reuse stories in design/development tools that support it.
Controls replaces Storybook Knobs. It incorporates lessons from years of supporting Knobs on tens of thousands of projects and dozens of different frameworks. We couldn't incrementally fix knobs, so we built a better version.
Contents
Installation
Controls requires Storybook Docs. If you're not using it already, please install that first.
Next, install the package:
npm install @storybook/addon-controls -D
And add it to your .storybook/main.js
config:
module.exports = {
addons: [
'@storybook/addon-docs'
'@storybook/addon-controls'
],
};
Writing stories
Let's see how to write stories that automatically generate controls based on your component properties.
Controls is built on Storybook Args, which is a small, backwards-compatible change to Storybook's Component Story Format.
This section is a step-by-step walkthrough for how to upgrade your stories. It takes you from a starting point of the traditional "no args" stories, to auto-generated args, to auto-generated args with custom controls, to fully custom args if you need them.
Getting started
Let's start with the following component/story combination, which should look familiar if you're coming from an older version of Storybook.
import React from 'react';
interface ButtonProps {
label?: string;
}
export const Button = ({ label = 'FIXME' }: ButtonProps) => <button>{label}</button>;
And here's a story that shows that Button component:
import React from 'react';
import { Button } from './Button';
export default { title: 'Button', component: Button };
export const Basic = () => <Button label="hello" />;
After installing the controls addon, you'll see a new tab that shows the component's props, but it doesn't show controls because the story doesn't use args. That's not very useful, but we'll fix that momentarily.
Auto-generated args
To upgrade your story to an Args story, modify it to accept an args object. NOTE: you may need to refresh the browser at this point.
export const Basic = (args) => {
console.log({ args });
return <Button label="hello" />;
};
Now you'll see auto-generated controls in the Controls
tab, and you can see the args
data updating as you edit the values in the UI:
Since the args directly matches the Button
's props, we can pass it into the args directly:
export const Basic = (args) => <Button {...args} />;
This generates an interactive UI:
Unfortunately this uses the default values specified in the component, and not the label hello
, which is what we wanted. To address this, we add an args
annotation to the story, which specifies the initial values:
export const Basic = (args) => <Button {...args} />;
Basic.args = { label: 'hello' };
Now we're back where we started, but we have a fully interactive story!
And this fully interactive story is also available in the Docs
tab of Storybook:
Custom controls args
There are cases where you'd like to customize the controls that get auto-generated from your component.
Consider the following modification to the Button
we introduced above:
import React from 'react';
interface ButtonProps {
label?: string;
background?: string;
}
export const Button = ({ background, label = 'FIXME' }: ButtonProps) => (
<button style={{ backgroundColor: background }}>{label}</button>
);
And the slightly expanded story:
export const Basic = (args) => <Button {...args} />;
Basic.args = { label: 'hello', background: '#ff0' };
This generates the following Controls
UI:
This works as long as you type a valid string into the auto-generated text control, but it's certainly is not the best UI for picking a color.
We can specify which controls get used by declaring a custom ArgType
for the background
property. ArgTypes
encode basic metadata for args, such as name
, description
, defaultValue
for an arg. These get automatically filled in by Storybook Docs
.
ArgTypes
can also contain arbitrary annotations which can be overridden by the user. Since background
is a property of the component, let's put that annotation on the default export.
import { Button } from './Button';
export default {
title: 'Button',
component: Button,
argTypes: {
background: { control: 'color' },
},
};
export const Basic = (args) => <Button {...args} />;
Basic.args = { label: 'hello', background: '#ff0' };
This generates the following UI, which is what we wanted in the first place:
NOTE: @storybook/addon-docs
provide shorthand for type
and control
fields, so in the previous example, control: 'color'
is shorthand control: { type: 'color' }
. Similarly, type: 'number'
can be written as shorthand for type: { name: 'number' }
.
Fully custom args
Up until now, we've only been using auto-generated controls based on the component we're writing stories for. What happens when we want a control for something that's not part of the story?
Consider the following story for our Button
from above:
import range from 'lodash/range';
export const Reflow = ({ count, label, ...args }) => (
<>
{range(count).map((i) => (
<Button label={`${label} ${i}`} {...args} />
))}
</>
);
Reflow.args = { count: 3, label: 'reflow' };
This generates the following UI:
Storybook has inferred the control to be a numeric input based on the initial value of the count
arg. As we did above, we can also specify a custom control as we did above. Only this time since it's story specific we can do it at the story level:
Reflow.argTypes = {
count: { control: { type: 'range', min: 0, max: 20 } },
};
This generates the following UI with a custom range slider:
Note: If you add an ArgType
that is not part of the component, Storybook will only use your argTypes definitions.
If you want to merge new controls with the existing component properties, you must enable this parameter:
docs: { forceExtractedArgTypes: true },
Angular
To achieve this within an angular-cli build.
export const Reflow = ({ count, label, ...args }) => ({
props: {
label: label,
count: [...Array(count).keys()],
},
template: `<Button *ngFor="let i of count">{{label}} {{i}}</Button>`,
});
Reflow.args = { count: 3, label: 'reflow' };
Template stories
Suppose you've created the Basic
story from above, but now we want to create a second story with a different state, such as how the button renders with the label is really long.
The simplest thing would be to create a second story:
export const VeryLongLabel = (args) => <Button {...args} />;
VeryLongLabel.args = { label: 'this is a very long string', background: '#ff0' };
This works, but it repeats code. What we want is to reuse the Basic
story, but with a different initial state. In Storybook we do this idiomatically for Args stories:
export const VeryLongLabel = Basic.bind({});
VeryLongLabel.args = { label: 'this is a very long string', background: '#ff0' };
We can even reuse initial args from other stories:
export const VeryLongLabel = Basic.bind();
VeryLongLabel.args = { ...Basic.args, label: 'this is a very long string' };
Configuration
The controls addon can be configured in two ways:
Control annotations
As shown above in the custom control args and fully custom args sections, you can configure controls via a "control" annotation in the argTypes
field of either a component or story.
Here is the full list of available controls:
data type | control type | description | options |
---|
array | array | serialize array into a comma-separated string inside a textbox | separator |
boolean | boolean | checkbox input | - |
number | number | a numberic text box input | min, max, step |
| range | a range slider input | min, max, step |
object | object | json editor text input | - |
enum | radio | radio buttons input | options |
| inline-radio | inline radio buttons input | options |
| check | multi-select checkbox input | options |
| inline-check | multi-select inline checkbox input | options |
| select | select dropdown input | options |
| multi-select | multi-select dropdown input | options |
string | text | simple text input | - |
| color | color picker input that assumes strings are color values | - |
| date | date picker input | - |
Example customizing a control for an enum
data type (defaults to select
control type):
export default {
title: 'Widget',
component: Widget,
argTypes: {
loadingState: {
type: 'inline-radio',
options: ['loading', 'error', 'ready'],
},
},
};
Example customizing a number
data type (defaults to number
control type):
export default {
title: 'Gizmo',
component: Gizmo,
argTypes: {
width: { type: 'range', min: 400, max: 1200, step: 50 };
},
};
Parameters
Controls supports the following configuration parameters, either globally or on a per-story basis:
Expanded: show property documentation
Since Controls is built on the same engine as Storybook Docs, it can also show property documentation alongside your controls using the expanded
parameter (defaults to false
).
To enable expanded mode globally, add the following to .storybook/preview.js
:
export const parameters = {
controls: { expanded: true },
};
And here's what the resulting UI looks like:
Hide NoControls warning
If you don't plan to handle the control args inside your Story, you can remove the warning with:
Basic.parameters = {
controls: { hideNoControlsWarning: true },
};
Framework support
| Manual | Auto-generated |
---|
React | + | + |
Vue | + | + |
Angular | + | + |
Ember | + | # |
Web components | + | + |
HTML | + | |
Svelte | + | |
Preact | + | |
Riot | + | |
Mithril | + | |
Marko | + | |
Note: #
= WIP support
FAQs
How will this replace addon-knobs?
Addon-knobs is one of Storybook's most popular addons with over 1M weekly downloads, so we know lots of users will be affected by this change. Knobs is also a mature addon, with various options that are not available in addon-controls.
Therefore, rather than deprecating addon-knobs immediately, we will continue to release knobs with the Storybook core distribution until 7.0. This will give us time to improve Controls based on user feedback, and also give knobs users ample time to migrate.
If you are somehow tied to knobs or prefer the knobs interface, we are happy to take on maintainers for the knobs project. If this interests you, hop on our Discord.
How do I migrate from addon-knobs?
If you're already using Storybook Knobs you should consider migrating to Controls.
You're probably using it for something that can be satisfied by one of the cases described above.
Let's walk through two examples: migrating knobs to auto-generated args and knobs to custom args.
Knobs to auto-generated args
First, let's consider a knobs version of a basic story that fills in the props for a component:
import { text } from '@storybook/addon-knobs';
import { Button } from './Button';
export const Basic = () => <Button label={text('Label', 'hello')} />;
This fills in the Button's label based on a knob, which is exactly the auto-generated use case above. So we can rewrite it using auto-generated args:
export const Basic = (args) => <Button {...args} />;
Basic.args = { label: 'hello' };
Knobs to manually-configured args
Similarly, we can also consider a story that uses knob inputs to change its behavior:
import range from 'lodash/range';
import { number, text } from '@storybook/addon-knobs';
export const Reflow = () => {
const count = number('Count', 10, { min: 0, max: 100, range: true });
const label = number('Label', 'reflow');
return (
<>
{range(count).map((i) => (
<Button label={`button ${i}`} />
))}
</>
);
};
And again, as above, this can be rewritten using fully custom args:
export const Reflow = ({ count, label, ...args }) => (
<>{range(count).map((i) => <Button label={`${label} ${i}` {...args}} />)}</>
);
Reflow.args = { count: 3, label: 'reflow' };
Reflow.argTypes = { count: { control: { type: 'range', min: 0, max: 20 } } };