Storybook's Interactions addon runs user-interaction simulations inside a story. That is useful for testing behavior, but when the addon is left on while someone browses the component library, the simulated interactions execute automatically on page load. What looks like a component stuck in a re-render loop is often just an interaction replaying itself.

The problem is worse for anyone whose operating system has prefers-reduced-motion enabled. Their setting is ignored, they get no way to opt out, and the motion fires the moment the story loads. Rapid screen movement of that kind can disorient users and, in some cases, trigger a seizure. Storybook currently offers no built-in toggle for interactions, so the fix has to be a custom addon — one that reads motion preferences and lets any user switch interactions on or off.

What the addon has to guarantee

  1. With prefers-reduced-motion enabled, interactions are off by default.
  2. Those same users can toggle interactions without changing their OS-level preferences.
  3. All users have the same toggle available.

Building the addon

Create a new file in your project's .storybook folder and add the following:

import React, {useCallback, useEffect} from 'react'

import {IconButton} from '@storybook/components'
import {PlayIcon, StopIcon} from '@storybook/icons'

export const ADDON_ID = 'toggle-interaction'
export const TOOL_ID = `${ADDON_ID}/tool`

export const INTERACTION_STORAGE_KEY = 'disableInteractions'

export const InteractionToggle = () => {
  const [disableInteractions, setDisableInteractions] = React.useState(
       window?.localStorage.getItem(INTERACTION_STORAGE_KEY) === 'true',
  )

  useEffect(() => {
    const reducedMotion = matchMedia('(prefers-reduced-motion)')

    if (window?.localStorage.getItem(INTERACTION_STORAGE_KEY) === null && reducedMotion.matches) {
      window?.localStorage?.setItem(INTERACTION_STORAGE_KEY, 'true')
      setDisableInteractions(true)
    }
  }, [])

  const toggleMyTool = useCallback(() => {
    window?.localStorage?.setItem(INTERACTION_STORAGE_KEY, `${!disableInteractions}`)
    setDisableInteractions(!disableInteractions)
      // Refreshes the page to cause the interaction to stop/start
      window.location.reload()
}, [disableInteractions, setDisableInteractions])

  return (
    <IconButton
      key={TOOL_ID}
      aria-label="Disable Interactions"
      onClick={toggleMyTool}
      defaultChecked={disableInteractions}
      aria-pressed={disableInteractions}
    >
      {disableInteractions ? <PlayIcon /> : <StopIcon />}
      Interactions
    </IconButton>
  )
}

How the pieces work

User preference is persisted with window.localStorage. On first load, the addon checks whether a preference already exists and, when one does, uses it.

const [disableInteractions, setDisableInteractions] = React.useState(
       window?.localStorage.getItem(INTERACTION_STORAGE_KEY) === 'true',
  )

A useEffect hook then checks for the prefers-reduced-motion setting and forces interactions off whenever the user has not already stored a preference in Storybook.

useEffect(() => {
    const reducedMotion = matchMedia('(prefers-reduced-motion)')

    if (window?.localStorage.getItem(INTERACTION_STORAGE_KEY) === null && reducedMotion.matches) {
      window?.localStorage?.setItem(INTERACTION_STORAGE_KEY, 'true')
      setDisableInteractions(true)
    }
  }, [])

Clicking the toggle writes the new preference and refreshes the page so the change takes effect.

const toggleMyTool = useCallback(() => {
    window?.localStorage?.setItem(INTERACTION_STORAGE_KEY, `${!disableInteractions}`)
    setDisableInteractions(!disableInteractions)
      // Refreshes the page to cause the interaction to stop/start
      window.location.reload()
  }, [disableInteractions, setDisableInteractions])

Registering the addon

In your .storybook/manager file, register the addon:

addons.register(ADDON_ID, () => {
  addons.add(TOOL_ID, {
    title: 'toggle interaction',
    type: types.TOOL as any,
    match: ({ viewMode, tabId }) => viewMode === 'story' && !tabId,
    render: () => <InteractionToggle />,
  })
})

Registration puts the toggle button in the Storybook toolbar, where users can change their Interactions preference.

Gating the interactions

Last, add a function that decides whether interactions are allowed to play, and call it from your interaction stories:

import {INTERACTION_STORAGE_KEY} from './.storybook/src/InteractionToggle'

export const shouldInteractionPlay = () => {
  const disableInteractions = window?.localStorage?.getItem(INTERACTION_STORAGE_KEY)
  return disableInteractions === 'false' || disableInteractions === null
}

 export const SomeComponentStory = {
  render: SomeComponent,
  play: async ({context}) => {
    if (shouldInteractionPlay()) {
...
    }
  })
 }

Result

With the addon in place, motion-sensitive users get interactions disabled by default, and every user — regardless of their OS settings — can turn interactions on or off from the toolbar. The accessibility gap closes without giving up the Interactions addon's testing capability.