Storybook Controls & Args: Interactive UI Testing
Storybook Controls is an add-on that automatically generates interactive form fields for your component's props, letting you test different configurations without editing code or reloading the page. As you change a control's value—say, toggling a boolean or typing into a text field—Storybook hot-reloads the story and shows the updated component immediately. This capability makes Controls essential for rapid component testing, exploring edge cases, and validating responsive behavior across prop combinations in real time.
How Do Controls Infer Input Types from Args?
Storybook's Controls add-on inspects your component's props and automatically generates matching UI controls. String args get text inputs, boolean args get toggles, and union types get dropdowns. Here's a Form component demonstrating type-based control inference:
import type { Meta, StoryObj } from '@storybook/react';
import { Form } from './Form';
interface FormProps {
title: string;
email: string;
password: string;
rememberMe: boolean;
theme: 'light' | 'dark';
submitButtonText: string;
disabled: boolean;
}
const meta = {
component: Form,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Form>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
title: 'Sign In',
email: '[email protected]',
password: '',
rememberMe: false,
theme: 'light',
submitButtonText: 'Sign In',
disabled: false,
},
};
Storybook automatically creates controls:
title,email,password,submitButtonText: text input fields.rememberMe,disabled: boolean toggle switches.theme: a dropdown/select with options['light', 'dark']inferred from the union type.
You can now drag the sidebar's Controls panel to test the form in every state: disabled, with different themes, with filled or empty fields.
Customizing Controls with argTypes
While auto-inferred controls work for basic types, you often need custom UI for domain-specific props. Use argTypes to specify control type, range, options, and descriptions:
const meta = {
component: Form,
argTypes: {
title: {
control: 'text',
description: 'The form title displayed above the email and password fields.',
},
email: {
control: 'email',
description: 'User email address.',
},
password: {
control: { type: 'text', mask: true },
description: 'User password (masked in UI).',
},
theme: {
control: 'radio',
options: ['light', 'dark', 'auto'],
description: 'The color scheme for the form.',
},
submitButtonText: {
control: { type: 'select', options: ['Sign In', 'Log In', 'Continue'] },
},
disabled: {
control: 'boolean',
description: 'Disables all form inputs.',
},
},
} satisfies Meta<typeof Form>;
Now:
emailrenders as an email-specific input (browser validation cues included).passwordis masked (shows dots instead of typed characters).themerenders as radio buttons instead of a dropdown (faster switching between 3 options).submitButtonTextis a select dropdown with predefined options instead of free-form text.
Testing Numeric Ranges with Sliders
For numeric props like font sizes, timeouts, or grid columns, use a number control with min, max, and step:
interface CardProps {
title: string;
padding: number; // in pixels
borderRadius: number;
opacity: number; // 0–1
columns: number; // grid layout
}
const meta = {
component: Card,
argTypes: {
padding: {
control: { type: 'range', min: 0, max: 64, step: 4 },
description: 'Internal padding in pixels.',
},
borderRadius: {
control: { type: 'range', min: 0, max: 32, step: 2 },
},
opacity: {
control: { type: 'range', min: 0, max: 1, step: 0.1 },
},
columns: {
control: { type: 'number', min: 1, max: 12 },
},
},
} satisfies Meta<typeof Card>;
export const Default: Story = {
args: {
title: 'Card Title',
padding: 16,
borderRadius: 8,
opacity: 1,
columns: 3,
},
};
The range control displays a slider, so you can drag to test values visually. The number control is an input field. Both support min, max, and step to constrain valid inputs.
Color and Date Pickers
For color and date-time props, Storybook includes specialized controls:
interface BannerProps {
backgroundColor: string; // hex color
textColor: string;
scheduleDate: string; // ISO date
expiresAt: string; // ISO datetime
}
const meta = {
component: Banner,
argTypes: {
backgroundColor: {
control: 'color',
description: 'Banner background color.',
},
textColor: {
control: 'color',
},
scheduleDate: {
control: { type: 'date' },
},
expiresAt: {
control: { type: 'date', includeTime: true },
},
},
} satisfies Meta<typeof Banner>;
export const Default: Story = {
args: {
backgroundColor: '#3498db',
textColor: '#ffffff',
scheduleDate: '2026-06-15',
expiresAt: '2026-06-15T23:59:00',
},
};
Click the color control to open a color picker; click the date control to open a date/time picker—no manual ISO formatting needed.
Documenting Edge Cases with Predefined Control Sets
For complex components, create separate stories showcasing edge cases and predefined control combinations. This pattern documents expected behavior and helps QA teams validate limits:
export const MaxPadding: Story = {
args: {
title: 'Max Padding Test',
padding: 64,
borderRadius: 32,
opacity: 1,
columns: 12,
},
parameters: {
docs: {
description: {
story: 'Tests the component at maximum padding and radius values.',
},
},
},
};
export const MinPadding: Story = {
args: {
title: 'Min Padding Test',
padding: 0,
borderRadius: 0,
opacity: 0.1,
columns: 1,
},
};
export const Responsive: Story = {
args: {
...Default.args,
},
parameters: {
viewport: {
defaultViewport: 'mobile1',
},
docs: {
description: {
story: 'Renders at mobile width (375px) to test responsive behavior.',
},
},
},
};
Each story documents a specific scenario. The parameters object includes documentation visible in the Docs tab, so QA teams understand what each story tests.
Using Actions to Log Callback Invocations
When your component accepts callbacks (like onClick or onSubmit), use the action control to log invocations in the Actions tab:
interface ButtonProps {
label: string;
onClick?: () => void;
onHover?: () => void;
}
const meta = {
component: Button,
argTypes: {
onClick: { action: 'clicked' },
onHover: { action: 'hovered' },
},
} satisfies Meta<typeof Button>;
export const Interactive: Story = {
args: {
label: 'Click me',
},
};
When you click the button in the story, Storybook logs an action entry with a timestamp in the Actions tab (right-side panel). This verifies callbacks are wired correctly without writing Jest or Cypress tests.
Key Takeaways
- Auto-inferred controls: Storybook generates controls from TypeScript types—strings become text inputs, booleans become toggles, unions become dropdowns.
- Custom argTypes override inference: Explicitly define
argTypesto specify control types (color, date, range, radio, select) and add descriptions visible in the Docs tab. - Range sliders for numeric props: Use
control: { type: 'range', min, max, step }for font sizes, spacing, and opacity to test smoothly across a continuous range. - Specialized pickers: Use
color,date, anddate with timecontrols for domain-specific inputs. - Actions log callbacks: Set
action: 'event-name'on callback args to log invocations in the Actions tab, verifying event handling without unit tests. - Edge-case stories document limits: Create separate stories for minimum, maximum, and responsive breakpoint scenarios with parameters documentation.
Frequently Asked Questions
Can I create dynamic controls that depend on other args?
Not directly in argTypes, but you can conditionally render UI in your story decorator or wrapper. For example: if (args.variant === 'complex') { showAdvancedControls() }. Alternatively, use story-level decorators to wrap the component with logic that shows/hides controls based on args.
How do I hide a prop from controls if my component has internal-only props?
Set table: { disable: true } in the argTypes entry for that prop. For example: internalState: { table: { disable: true } } removes it from the Controls panel and the Docs prop table.
What if my component accepts complex objects as props?
Storybook's controls don't auto-generate for deeply nested objects. Instead, define simpler args that map to object props, or use control: false and pass the object manually in args. For example: config: { control: false } disables control generation for an object prop.
Can I share controls between multiple stories?
Yes. Define a base args object and extend it in each story: const defaultArgs = { ... }; export const Variant1 = { args: { ...defaultArgs, variant: 'v1' } }. This pattern reduces duplication and makes bulk edits easier.