Hover and focus states

Components can respond differently based on hover or focus events. Here are a few techniques for capturing the result of these user events Chromatic.

Make your stories interactive

As of 6.4, stories are capable of simulating user interactions via the play function. Interactions allow you to verify how a component responds to user input (e.g., hover, focus, click, type). Chromatic awaits play function execution before taking a snapshot.

JavaScript-triggered hover states

If the hover behavior is triggered via JavaScript like tooltips or dropdowns, write a play function to simulate it using Storybook’s instrumented version of Testing Library. For example:

// Form.stories.js|jsx

import React from 'react';

import { userEvent, waitFor, within } from '@storybook/testing-library';

import { Form } from './LoginForm';

export default {
  component: Form,
  title: 'Form',
};

const Template = (args) => <LoginForm {...args} />;

export const WithHoverState = Template.bind({});
WithHoverState.play = async ({ canvasElement }) => {
  // Starts querying the component from its root
  const canvas = within(canvasElement);

  // Looks up the input and fills it.
  const emailInput = canvas.getByLabelText("email", {
    selector: "input",
  });

  await userEvent.type(emailInput, "Example");

  // Looks up the button and interacts with it.
  const submitButton = canvas.getByRole("button");
  await userEvent.click(submitButton);

  // Triggers the hover state
  await waitFor(async () => {
    await userEvent.hover(canvas.getByLabelText("Email error"));
  });
};

CSS :hover state

CSS includes the :hover pseudo-class that allow precise styling of an element on cursor hover. This is a “trusted event” for web browsers, meaning it can’t be simulated by the play function. There are multiple ways you can snapshot this state.

Use CSS class names

Add a CSS class name that mirrors the states you’re trying to test (e.g., hover, active):

/* Component styles */
MyComponent:hover,
MyComponent.hover {
  background: purple;
}

MyComponent:active,
MyComponent.active {
  background: green;
}

Then write a story that utilizes the class name:

// MyComponent.stories.js|jsx

import { MyComponent } from './MyComponent';

export default {
  component: MyComponent,
  title: 'MyComponent',
};

const Template = (args) => <MyComponent {...args} />;

export const HoverStatewithClass = Template.bind({});

HoverStatewithClass.args = {
  ...HoverState.args,
  className: "hover",
};

export const ActiveStatewithClass = Template.bind({});
ActiveStatewithClass.args = {
  ...ActiveState.args,
  className: "active",
};

You can also extend this technique using a JS wrapper that automates adding a class.

Trigger CSS states via props

Although not recommended, you can test an element’s states by creating a separate “pure” stateless component. Then use it to test the exact configurations you are after via props. For example:

// MyComponent.js|jsx

export function MyComponent({ isHovered, isActive, label }) {
  return (
    <Button isHovered={isHovered} isActive={isActive}>
      {label}
    </Button>
  );
}

MyComponent.defaultProps = {
  isHovered: false,
  isActive: false,
  label: "Submit",
};

You can write the following story to trigger the props:

// MyComponent.stories.js|jsx

import { MyComponent } from './MyComponent';

export default {
  component: MyComponent,
  title: 'MyComponent',
};


const Template = (args) => <MyComponent {...args}/>;

export const HoverState = Template.bind({});

HoverState.args = {
  isHovered: true,
  label: `I'm :hover`
};

export const ActiveState = Template.bind({});
ActiveState.args = {
  isActive: true,
  label: `I'm :active`
}:
With Storybook's Pseudo States addon

For atomic, functional components with CSS pseudo-classes (e.g., hover, active), try the Storybook’s Pseudo States addon to test pseudo states. For example:

// Button.stories.js|jsx

import React from 'react';

import { Button } from './Button';

export default {
  component: Button,
  title: 'Button',
};

const Template = (args) => <Button {...args} />;

export const WithHoverState = Template.bind({});
WithHoverState.args = {
  size: "small",
  label: "Button",
};

WithHoverState.parameters = {
  // Toggles the component hover state via parameter.
  pseudo: { hover: true },
};

Focus

To simulate how the component responds when an element is focused (i.e., through mouse or keyboard), write a play function emulating the behavior. For example:

// Button.stories.js|jsx

import React from 'react';

import { userEvent, within } from '@storybook/testing-library';

import { Button } from './Button';

export default {
  component: Button,
  title: 'Button',
};

const Template = (args) => <Button {...args} />;

export const WithFocusState = Template.bind({});

export const WithFocusState = Template.bind({});
WithFocusState.play = async ({ canvasElement }) => {
  // Starts querying the component from its root
  const canvas = within(canvasElement);

  // Looks up the button and interacts with it.
  canvas.getByRole("button").focus();
};