How to Use the Storybook Actions Addon for Logging User Interactions

The Storybook Actions addon captures user events such as clicks, changes, and submits by creating handler functions that serialize arguments and emit them to the Actions panel for real-time debugging.

The Actions addon in the storybookjs/storybook repository transforms any user interaction into a structured log entry visible in Storybook's Actions panel. By leveraging Storybook's channel system (storybook/actions/action-event), the addon bridges the preview iframe and manager UI to display serialized event data. This guide explains the architecture, configuration options, and implementation patterns based on the source code in the next branch.

How the Actions Addon Works

Core Architecture Components

The addon consists of two runtime contexts: the preview side where handlers execute, and the manager side where logs render. Key files in code/core/src/actions/ define this behavior:

  • manager.tsx – Registers the addon panel using addons.register under the storybook/actions namespace
  • constants.ts – Defines EVENT_ID (storybook/actions/action-event), PANEL_ID, and CLEAR_ID for channel communication
  • runtime/action.ts – Implements the action() factory that creates logging handlers
  • runtime/actions.ts – Provides the actions() helper for generating multiple named handlers
  • runtime/configureActions.ts – Stores mutable default configuration (depth, limit, clearOnStoryChange)
  • decorator.ts – Contains the deprecated withActions decorator for delegated event listening
  • containers/ActionLogger/index.tsx – Manager-side container that listens for channel events and manages state

Event Flow from Handler to Panel

When a user interacts with a component, the data flows through four stages:

  1. Handler Execution – The action() function in code/core/src/actions/runtime/action.ts returns a callback that serializes arguments using the configured depth limit
  2. Channel Emission – The handler calls addons.getChannel().emit(EVENT_ID, payload) with the action name, serialized args, and display options
  3. State Aggregation – The ActionLogger container receives the payload via api.on(EVENT_ID, ...) and coalesces identical calls while respecting the limit parameter
  4. UI Rendering – The component in code/core/src/actions/components/ActionLogger/index.tsx renders the action name, argument tree, and call count in a scrollable table

Setting Up Action Handlers

The action() Function for Single Events

Import action from @storybook/addon-actions to create a named handler for individual events. This is the most common pattern for logging specific callbacks:

import { action } from '@storybook/addon-actions';

const onSubmit = action('form-submit');

export const Form = () => (
  <form onSubmit={onSubmit}>
    <input name="email" placeholder="Email" />
    <button type="submit">Send</button>
  </form>
);

When the form submits, the form-submit entry appears in the Actions panel with serialized event data.

The actions() Helper for Multiple Events

For components with multiple callbacks, use the actions() utility from code/core/src/actions/runtime/actions.ts to generate a map of handlers:

import { actions } from '@storybook/addon-actions';

export const Counter = () => {
  const { increment, decrement } = actions('increment', 'decrement');

  return (
    <div>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
};

Each string argument creates a distinct handler that logs under that name.

Configuring the Actions Addon

Global Configuration with configureActions()

Modify serialization behavior globally by calling configureActions() in your .storybook/preview.ts file. This mutates the shared config object used by all handlers:

import { configureActions } from '@storybook/addon-actions';

configureActions({
  depth: 3,          // Maximum object nesting level displayed
  limit: 20,         // Maximum actions stored in the panel
  clearOnStoryChange: true, // Reset logs when switching stories
});

The configuration is defined in code/core/src/actions/runtime/configureActions.ts and applies to every action() call in the project.

Controlling Serialization Depth

Limit deep object logging to keep the Actions panel readable:

import { action, configureActions } from '@storybook/addon-actions';

configureActions({ depth: 2 });

export const ComplexObject = () => {
  const log = action('object-click');

  return (
    <button
      onClick={() =>
        log({ 
          user: { 
            name: 'Alice', 
            profile: { 
              age: 30, 
              address: { city: 'NY' } 
            } 
          } 
        })
      }
    >
      Log Object
    </button>
  );
};

Objects nested deeper than level 2 display as [Object] to prevent UI clutter.

Delegated Event Handling

Using the withActions Decorator (Deprecated)

The withActions decorator in code/core/src/actions/decorator.ts provides delegated event listening without manual handler wiring. Although deprecated and scheduled for removal in Storybook v10, it demonstrates advanced event delegation patterns:

import { withActions } from '@storybook/addon-actions';

export default {
  title: 'Button/Delegated',
  decorators: [withActions],
  parameters: {
    actions: { 
      handles: ['click .trackable', 'change input'] 
    },
  },
};

export const Delegated = () => (
  <div>
    <button className="trackable">Tracked Click</button>
    <button>Ignored Click</button>
    <input type="text" placeholder="Tracked Input" />
  </div>
);

The decorator attaches a single listener to the Storybook root element and forwards matching events to the action logger based on the CSS selectors in parameters.handles.

Summary

  • The Actions addon creates handler functions via action() and actions() that serialize arguments and emit storybook/actions/action-event through Storybook's channel system
  • Configuration is managed through configureActions() in code/core/src/actions/runtime/configureActions.ts, controlling depth, limit, and clearOnStoryChange behavior
  • Manager-side rendering occurs in code/core/src/actions/containers/ActionLogger/index.tsx, which aggregates identical calls and respects display limits
  • Delegated handling via withActions is deprecated but available for legacy use cases requiring CSS selector-based event capture
  • All handlers serialize data according to the global config before transmission to the Actions panel

Frequently Asked Questions

How do I prevent the Actions panel from showing duplicate events?

The ActionLogger component automatically coalesces identical consecutive calls and displays a call count badge. To clear the log manually, click the Clear button in the Actions panel, which emits the CLEAR_ID event defined in code/core/src/actions/constants.ts. Setting clearOnStoryChange: true in configureActions() resets the log automatically when navigating between stories.

What is the difference between action() and actions() in Storybook?

action() (from code/core/src/actions/runtime/action.ts) creates a single named handler function, ideal for one-off events like form submissions. actions() (from code/core/src/actions/runtime/actions.ts) accepts multiple string arguments and returns an object mapping those names to individual handlers, useful for components with several callbacks. Both use the same underlying serialization and channel emission logic.

Why are my logged objects showing as [Object] instead of expanded data?

This occurs when the object depth exceeds the configured depth limit. Call configureActions({ depth: 5 }) or higher in your preview configuration to expand deeper nesting. The default depth is conservative to prevent performance issues when logging large event objects or DOM nodes.

Is the withActions decorator still supported in Storybook?

The withActions decorator is deprecated as of recent Storybook versions and will be removed in v10. While it still functions for delegated event handling via parameters.handles, the recommended approach is to use explicit action() handlers assigned directly to component props for clearer data flow and better TypeScript support.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →