Join live session: Top 8 Storybook myths holding your team back
Docs
Storybook Docs

Play function

Watch a video tutorial

Play functions are small snippets of code executed after the story renders. Enabling you to interact with your components and test scenarios that otherwise required user intervention.

Setup the interactions addon

We recommend installing Storybook's addon-interactions before you start writing stories with the play function. It's the perfect complement for it, including a handy set of UI controls to allow you command over the execution flow. At any time, you can pause, resume, rewind, and step through each interaction. Also providing you with an easy-to-use debugger for potential issues.

Run the following command to install the addon and the required dependencies.

npm install @storybook/testing-library @storybook/jest @storybook/addon-interactions --save-dev

Update your Storybook configuration (in .storybook/main.js) to include the interactions addon.

.storybook/main.js
module.exports = {
  stories:[],
  addons:[
    // Other Storybook addons
    '@storybook/addon-interactions', //๐Ÿ‘ˆ The addon registered here
};

Writing stories with the play function

Storybook's play functions are small code snippets that run once the story finishes rendering. Aided by the addon-interactions, it allows you to build component interactions and test scenarios that were impossible without user intervention. For example, if you were working on a registration form and wanted to validate it, you could write the following story with the play function:

RegistrationForm.stories.ts|tsx
import React from 'react';
 
import { ComponentStory, ComponentMeta } from '@storybook/react';
 
import { within, userEvent } from '@storybook/testing-library';
 
import { RegistrationForm } from './RegistrationForm';
 
export default {
  /* ๐Ÿ‘‡ The title prop is optional.
  * See https://storybook.js.org/docs/6/configure#configure-story-loading
  * to learn how to generate automatic titles
  */
  title: 'RegistrationForm',
  component: RegistrationForm,
} as ComponentMeta<typeof RegistrationForm>;
 
const Template: ComponentStory<typeof RegistrationForm> = (args) => <RegistrationForm {...args} />;
 
/*
* See https://storybook.js.org/docs/6/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const FilledForm = Template.bind({});
FilledForm.play = async ({ canvasElement }) => {
  const canvas= within(canvasElement);
 
  const emailInput = canvas.getByLabelText('email', {
    selector: 'input',
  });
 
  await userEvent.type(emailInput, 'example-email@email.com', {
    delay: 100,
  });
 
  const passwordInput = canvas.getByLabelText('password', {
    selector: 'input',
  });
 
  await userEvent.type(passwordInput, 'ExamplePassword', {
    delay: 100,
  });
  // See https://storybook.js.org/docs/6/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
  const submitButton = canvas.getByRole('button');
 
  await userEvent.click(submitButton);
};

See the Interaction testing documentation for an overview of the available API events.

When Storybook finishes rendering the story, it executes the steps defined within the play function, interacting with the component and filling the form's information. All of this without the need for user intervention. If you check your Interactions panel, you'll see the step-by-step flow.

Composing stories

Thanks to the Component Story Format, an ES6 module based file format, you can also combine your play functions, similar to other existing Storybook features (e.g., args). For example, if you wanted to verify a specific workflow for your component, you could write the following stories:

MyComponent.stories.ts|tsx
import React from 'react';
 
import { ComponentStory, ComponentMeta } from '@storybook/react';
 
import { userEvent, within } from '@storybook/testing-library';
 
import { MyComponent } from './MyComponent';
 
export default {
  /* ๐Ÿ‘‡ The title prop is optional.
  * See https://storybook.js.org/docs/6/configure#configure-story-loading
  * to learn how to generate automatic titles
  */
  title: 'MyComponent',
  component: MyComponent,
} as ComponentMeta<typeof MyComponent>;
 
const Template: ComponentStory<typeof MyComponent> = (args) => <MyComponent {...args} />;
 
/*
* See https://storybook.js.org/docs/6/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const FirstStory = Template.bind({});
FirstStory.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
 
  await userEvent.type(canvas.getByTestId('an-element'), 'example-value');
};
 
export const SecondStory = Template.bind({});
SecondStory.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
 
  await userEvent.type(canvas.getByTestId('other-element'), 'another value');
};
 
export const CombinedStories = Template.bind({});
CombinedStories.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
 
  // Runs the FirstStory and Second story play function before running this story's play function
  await FirstStory.play({ canvasElement });
  await SecondStory.play({ canvasElement });
  await userEvent.type(canvas.getByTestId('another-element'), 'random value');
};

By combining the stories, you're recreating the entire component workflow and can spot potential issues while reducing the boilerplate code you need to write.

Working with events

Most modern UIs are built focusing on interaction (e.g., clicking a button, selecting options, ticking checkboxes), providing rich experiences to the end-user. With the play function, you can incorporate the same level of interaction into your stories.

A common type of component interaction is a button click. If you need to reproduce it in your story, you can define your story's play function as the following:

MyComponent.stories.ts|tsx
import React from 'react';
 
import { ComponentStory, ComponentMeta } from '@storybook/react';
 
import { fireEvent, userEvent, within} from '@storybook/testing-library';
 
import { MyComponent } from './MyComponent';
 
export default {
  /* ๐Ÿ‘‡ The title prop is optional.
  * See https://storybook.js.org/docs/6/configure#configure-story-loading
  * to learn how to generate automatic titles
  */
  title: 'ClickExamples',
  component: MyComponent,
} as ComponentMeta<typeof MyComponent>;
 
const Template: ComponentStory<typeof MyComponent> = (args) => <MyComponent {...args} />;
 
/*
* See https://storybook.js.org/docs/6/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const ClickExample = Template.bind({});
ClickExample.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
 
  // See https://storybook.js.org/docs/6/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
  await userEvent.click(canvas.getByRole('button'));
};
 
export const FireEventExample = Template.bind({});
FireEventExample.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
 
  // See https://storybook.js.org/docs/6/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
  await fireEvent.click(canvas.getByTestId('data-testid'));
};

When Storybook loads the story and the function executes, it interacts with the component and triggers the button click, similar to what a user would do.

Asides from click events, you can also script additional events with the play function. For example, if your component includes a select with various options, you can write the following story and test each scenario:

MyComponent.stories.ts|tsx
import React from 'react';
 
import { ComponentStory, ComponentMeta } from '@storybook/react';
 
import { userEvent, within } from '@storybook/testing-library';
 
import { MyComponent } from './MyComponent';
 
export default {
  /* ๐Ÿ‘‡ The title prop is optional.
  * See https://storybook.js.org/docs/6/configure#configure-story-loading
  * to learn how to generate automatic titles
  */
  title: 'WithSelectEvent',
  component: MyComponent,
} as ComponentMeta<typeof MyComponent>;
 
// Function to emulate pausing between interactions
function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
 
const Template: ComponentStory<typeof MyComponent> = (args) => <MyComponent {...args} />;
 
/*
* See https://storybook.js.org/docs/6/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const ExampleChangeEvent = Template.bind({});
ExampleChangeEvent.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
 
  const select = canvas.getByRole('listbox');
 
  await userEvent.selectOptions(select, ['One Item']);
  await sleep(2000);
 
  await userEvent.selectOptions(select, ['Another Item']);
  await sleep(2000);
 
  await userEvent.selectOptions(select, ['Yet another item']);
};

In addition to events, you can also create interactions with the play function based on other types of asynchronous methods. For instance, let's assume that you're working with a component with validation logic implemented (e.g., email validation, password strength). In that case, you can introduce delays within your play function to emulate user interaction and assert if the values provided are valid or not:

MyComponent.stories.ts|tsx
import React from 'react';
 
import { ComponentStory, ComponentMeta } from '@storybook/react';
 
import { userEvent, within } from '@storybook/testing-library';
 
import { MyComponent } from './MyComponent';
 
export default {
  /* ๐Ÿ‘‡ The title prop is optional.
  * See https://storybook.js.org/docs/6/configure#configure-story-loading
  * to learn how to generate automatic titles
  */
  title: 'WithDelay',
  component: MyComponent,
} as ComponentMeta<typeof MyComponent>;
 
const Template: ComponentStory<typeof MyComponent> = (args) => <MyComponent {...args} />;
 
/*
* See https://storybook.js.org/docs/6/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const DelayedStory = Template.bind({});
DelayedStory.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
 
  const exampleElement = canvas.getByLabelText('example-element');
 
  // The delay option set the amount of milliseconds between characters being typed
  await userEvent.type(exampleElement, 'random string', {
    delay: 100,
  });
 
  const AnotherExampleElement = canvas.getByLabelText('another-example-element');
  await userEvent.type(AnotherExampleElement, 'another random string', {
    delay: 100,
  });
};

When Storybook loads the story, it interacts with the component, filling in its inputs and triggering any validation logic defined.

You can also use the play function to verify the existence of an element based on a specific interaction. For instance, if you're working on a component and want to check what happens if a user introduces the wrong information. In that case, you could write the following story:

MyComponent.stories.ts|tsx
import React from 'react';
 
import { ComponentStory, ComponentMeta } from '@storybook/react';
 
import { userEvent, waitFor, within } from '@storybook/testing-library';
 
import { MyComponent } from './MyComponent';
 
export default {
  /* ๐Ÿ‘‡ The title prop is optional.
  * See https://storybook.js.org/docs/6/configure#configure-story-loading
  * to learn how to generate automatic titles
  */
  title: 'WithAsync',
  component: MyComponent,
} as ComponentMeta<typeof MyComponent>;
 
const Template: ComponentStory<typeof MyComponent> = (args) => <MyComponent {...args} />;
 
/*
* See https://storybook.js.org/docs/6/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const ExampleAsyncStory = Template.bind({});
ExampleAsyncStory.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
  
  const Input = canvas.getByLabelText('Username', {
    selector: 'input',
  });
 
  await userEvent.type(Input, 'WrongInput', {
    delay: 100,
  });
  // See https://storybook.js.org/docs/6/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
  const Submit = canvas.getByRole('button');
 
  await userEvent.click(Submit);
 
  await waitFor(async () => {
    await userEvent.hover(canvas.getByTestId('error'));
  });
};

Querying elements

If you need, you can also adjust your play function to find elements based on queries (e.g., role, text content). For example:

MyComponent.stories.ts|tsx
import React from 'react';
 
import { ComponentStory, ComponentMeta } from '@storybook/react';
 
import { userEvent, within } from '@storybook/testing-library';
 
import { MyComponent } from './MyComponent';
 
export default {
  /* ๐Ÿ‘‡ The title prop is optional.
  * See https://storybook.js.org/docs/6/configure#configure-story-loading
  * to learn how to generate automatic titles
  */
  title: 'QueryMethods',
  component: MyComponent,
} as ComponentMeta<typeof MyComponent>;
 
const Template: ComponentStory<typeof MyComponent> = (args) => <MyComponent {...args} />;
 
/* See https://storybook.js.org/docs/6/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const ExampleWithRole = Template.bind({});
ExampleWithRole.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
 
  // See https://storybook.js.org/docs/6/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
  await userEvent.click(canvas.getByRole('button', { name: / button label/i }));
};

You can read more about the querying elements in the Testing library documentation.

When Storybook loads the story, the play function starts its execution and queries the DOM tree expecting the element to be available when the story renders. In case there's a failure in your test, you'll be able to verify its root cause quickly.

Otherwise, if the component is not immediately available, for instance, due to a previous step defined inside your play function or some asynchronous behavior, you can adjust your story and wait for the change to the DOM tree to happen before querying the element. For example:

MyComponent.stories.ts|tsx
import React from 'react';
 
import { ComponentStory, ComponentMeta } from '@storybook/react';
 
import { userEvent, within } from '@storybook/testing-library';
 
import { MyComponent } from './MyComponent';
 
export default {
  /* ๐Ÿ‘‡ The title prop is optional.
   * See https://storybook.js.org/docs/6/configure#configure-story-loading
   * to learn how to generate automatic titles
   */
  title: 'Async Query Methods',
  component: MyComponent,
} as ComponentMeta<typeof MyComponent>;
 
const Template: ComponentStory<typeof MyComponent> = (args) => <MyComponent {...args} />;
 
/*
* See https://storybook.js.org/docs/6/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const AsyncExample = Template.bind({});
AsyncExample.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
   // Other steps
 
  // Waits for the component to be rendered before querying the element
  await canvas.findByRole('button', { name: / button label/i }));
};

Working with the Canvas

By default, each interaction you write inside your play function will be executed starting from the top-level element of the Canvas. This is acceptable for smaller components (e.g., buttons, checkboxes, text inputs), but can be inefficient for complex components (e.g., forms, pages), or for multiple stories. To accommodate this, you can adjust your interactions to start execution from the component's root. For example:

MyComponent.stories.ts|tsx
import React from 'react';
 
import { ComponentStory, ComponentMeta } from '@storybook/react';
 
import { userEvent, within } from '@storybook/testing-library';
 
import { MyComponent } from './MyComponent';
 
export default {
  /* ๐Ÿ‘‡ The title prop is optional.
  * See https://storybook.js.org/docs/6/configure#configure-story-loading
  * to learn how to generate automatic titles
  */
  title: 'WithCanvasElement',
  component: MyComponent,
} as ComponentMeta<typeof MyComponent>;
 
const Template: ComponentStory<typeof MyComponent> = (args) => <MyComponent {...args} />;
 
export const ExampleStory = Template.bind({});
ExampleStory.play = async ({ canvasElement }) => {
  // Assigns canvas to the component root element
  const canvas = within(canvasElement);
 
  // Starts querying from the component's root element
  await userEvent.type(canvas.getByTestId('example-element'), 'something');
  await userEvent.click(canvas.getByRole('another-element'));
};

Applying these changes to your stories can provide a performance boost and improved error handling with addon-interactions.