Using Storybook For UI Development
The iframe below will not render until the "X-Frame-Options: deny" setting has been removed -- hopefully soon
We use shadcn as the baseline for our component development.
We use Storybook for component hosting and documentation.
Our Storybook site is hosted at https://storybook.charles-schwab.vercel.app/
Our shadcn Integration
We follow shadcn's philosophy of copy-paste components rather than installing a component library. This gives us full control over styling and behavior while maintaining consistency.
What We Use from shadcn
1. The cn() Utility Function
Located at /packages/ui/src/utilities/shad/index.ts, this is the core utility for merging Tailwind classes:
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
/**
* Takes in a clsx compliant set of classes and joins them into a singular string.
* Intelligently merges Tailwind classes, resolving conflicts.
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Usage in Components:
import { cn } from '../../utilities/shad';
<div className={cn(
'base-classes',
variantClasses,
conditionalClass && 'conditional-classes',
className // User-provided overrides
)} />
2. Radix UI Primitives
We use Radix UI headless components as the foundation (same as shadcn):
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toast": "^1.2.11"
These provide:
- Accessibility - WAI-ARIA compliant out of the box
- Keyboard Navigation - Full keyboard support
- Focus Management - Proper focus trapping and restoration
- Unstyled - Complete styling control
3. Component Architecture Pattern
We adapt shadcn components to our needs. Example: Tabs component at /packages/ui/src/components/Shadcn/:
Shadcn/
├── Tabs.client.tsx # Radix UI wrapper with our styling
└── TabsWrapper.tsx # High-level wrapper for our use cases
Tabs.client.tsx - Base shadcn-style component:
'use client';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from '@schwab/ui/cn';
import { cva, type VariantProps } from 'class-variance-authority';
const Tabs = React.forwardRef<
React.ComponentRef<typeof TabsPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Root
{...props}
ref={ref}
className={cn(TabsVariants({ className }))}
>
{children}
</TabsPrimitive.Root>
));
const TabsVariants = cva('base-classes', {
variants: {
variant: {
default: 'bg-metal-700',
accessibleBlue: 'bg-blue-700',
},
},
});
TabsWrapper.tsx - Application-specific wrapper:
import { Tabs, TabsList, TabsTrigger, TabsContent } from './Tabs.client';
export function TabsWrapper({
tabListAriaLabel, // Required for accessibility
tabsInfo,
}: Props) {
return (
<Tabs defaultValue={tabsInfo[0]?.value} activationMode="manual">
<TabsList aria-label={tabListAriaLabel} loop={true}>
{tabsInfo.map((tab) => (
<TabsTrigger key={tab.value} value={tab.value}>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{/* Content... */}
</Tabs>
);
}
How Our Approach Differs from Pure shadcn
1. Monorepo Structure
- shadcn: Components live in your app's
components/uifolder - Ours: Components in
/packages/ui/src/components/for cross-app reuse
2. Schema-Driven Props
- shadcn: TypeScript interfaces for props
- Ours: Zod schemas for runtime validation + type generation
3. CVA Integration
- shadcn: Basic CVA usage
- Ours: Centralized
baseCVAClassesfor design system consistency
4. Server/Client Split
- shadcn: Client components with
'use client' - Ours: Separate
.server.tsxand.client.tsxfiles for NextJS optimization
5. Path Aliases
- shadcn: Relative imports
../../components/ui/button - Ours: Absolute aliases
#components/Button,#utilities/shad
When to Use shadcn-Style Components
✅ Use for:
- Interactive UI primitives (Tabs, Dialogs, Popovers, Accordions)
- Components requiring accessibility features
- Reusable form controls
- Complex interaction patterns
❌ Don't use for:
- Simple presentational components (use direct Tailwind)
- Content-specific components (Article, Story, etc.)
- Layout components (Container, Panel)
Adding New shadcn Components
If you need a shadcn component we don't have:
-
Check Radix UI availability - Verify the primitive exists
-
Install Radix package (if needed):
cd /home/nextjs-web/packages/ui
pnpm add @radix-ui/react-[component-name] -
Create in Shadcn folder:
cd /home/nextjs-web/packages/ui/src/components/Shadcn
touch YourComponent.client.tsx -
Follow the pattern:
- Use
'use client'directive - Wrap Radix primitive with forwardRef
- Apply CVA for variants
- Use
cn()for class merging - Export component and its sub-components
- Use
-
Add export to package.json:
"exports": {
"./YourComponent": "./src/components/Shadcn/YourComponent.client.tsx"
} -
Create Storybook stories - Follow standard story creation process
Best Practices for shadcn Components
Accessibility First:
<TabsList
aria-label="Navigation tabs" // Always provide labels
loop={true} // Enable keyboard wrapping
/>
<TabsTrigger
value="tab1"
activationMode="manual" // Prevents auto-activation on focus
>
Use CVA for Variants:
const componentVariants = cva(
'base-classes', // Always-applied classes
{
variants: {
variant: {
default: 'default-styles',
custom: 'custom-styles',
},
},
defaultVariants: {
variant: 'default',
},
},
);
Preserve Radix Props:
// Always spread Radix props to preserve functionality
<TabsPrimitive.Root {...props} ref={ref} className={cn(...)}>
Type Safety:
interface TabsList extends
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>,
VariantProps<typeof TabsListVariants> {}
Available shadcn-Style Components
Currently implemented in /packages/ui/src/components/Shadcn/:
- Tabs - Tab navigation with multiple variants
- Accessible keyboard navigation
- Support for horizontal layouts
- Customizable trigger and list styling
More components can be added following the same pattern as needed.
Why use Storybook?
- Develop and test in isolation - Build components without needing the full application context
- QA in isolation - Test individual components thoroughly before integration
- Documentation - Auto-generated documentation and visual component library
- Single Source of Truth - Centralized component showcase and specifications
- Accessibility - Built-in accessibility testing and validation tools
Proposed Workflow
1. Determine if component should be present in Storybook
For the majority of components moving forward, the component should be developed in the Storybook environment as opposed to in the context of a page or site.
Components that belong in Storybook:
- Reusable UI components
- Form elements and inputs
- Layout components
- Interactive components with various states
- Components with multiple variants or themes
Components that may not need Storybook:
- Page-specific components with no reuse potential
- Simple one-off wrappers
- Components tightly coupled to specific business logic
2. Develop component with an eye towards adaptability and maintenance
Components should be usable/adaptable to work within a multitude of situations. Developer should ask:
- What other use cases are applicable to this component?
- Are requested changes (if any) going to hamper the component's ability to be extended in the future?
- What other components have a dependency on this component? How will this affect those components?
Design System Thinking: In all cases, be aware that site owners, etc. tend to think of components on a prescriptive basis i.e. "this page needs a specific functionality." Our role as developers within a design system is to translate those needs into code that works for a variety of situations, even if that means modifying the context of the request.
3. Documentation during development
Developers should update documentation as they work. Storybook provides a number of tools for documentation but the most useful is being able to auto-generate documentation using comments in TSX files.
Required Documentation:
- All components should have a basic description of their purpose and use
- All props should have documentation through interface commenting
- Usage examples and common patterns
- Do's and Don'ts for component usage
Example Documentation Pattern:
/**
* Button component for user actions and navigation
*
* Use this component for primary and secondary actions throughout the application.
* Supports multiple variants, sizes, and states.
*
* @example
* <Button variant="primary" size="large" onClick={handleClick}>
* Click me
* </Button>
*/
export interface ButtonProps {
/** The visual style variant of the button */
variant?: 'primary' | 'secondary' | 'ghost';
/** The size of the button */
size?: 'small' | 'medium' | 'large';
/** Whether the button is disabled */
disabled?: boolean;
/** Click handler function */
onClick?: () => void;
/** Button content */
children: React.ReactNode;
}
4. Develop/update component and seek approval from UX/Designer/Owner
Before creating a PR, use the Vercel preview URL to seek approval and suggestions from concerned parties. In many cases, additional questions will arise after the initial round of development is complete.
Review Process:
- Deploy component to Storybook preview environment
- Share preview URL with stakeholders
- Gather feedback and iterate as needed
- Document any design decisions or compromises
- Get final approval before proceeding to PR
5. Test in Storybook
Storybook provides a number of tools for testing your code:
Available Testing Tools:
- Accessibility testing - Built-in a11y addon for WCAG compliance
- Unit testing - Jest integration for component logic testing
- Interaction testing (TBD) - Simulated user interactions
- Visual regression testing (TBD) - Screenshot comparison testing
Recommended Testing Approach:
Consider using Chromatic, a paid service for visual regression testing that is tightly integrated with Storybook. This provides:
- Automated visual diffing
- Cross-browser testing
- Component history tracking
- Integration with PR workflows
6. Test in Site
Using Vercel preview, test if the component is accomplishing its purpose within the actual application context.
Integration Testing Checklist:
- Component renders correctly in various page contexts
- Props are passed correctly from parent components
- Component behaves as expected with real data
- No styling conflicts with other components
- Performance is acceptable in production-like environment
7. Create PR
After completing development, testing, and stakeholder review:
- Create pull request with comprehensive description
- Include links to Storybook stories for reviewers
- Document any breaking changes or migration notes
- Request reviews from appropriate team members
- Address feedback and iterate as needed
How to Add a UI Component to Storybook
Follow this step-by-step guide to add a new component to our Storybook.
Prerequisites
Before you begin, ensure:
- Your component will be located in
/home/nextjs-web/packages/ui/src/components/[ComponentName]/ - You have a Zod schema defined (or will create one) in
/home/nextjs-web/packages/schema/src/ui/[ComponentName]/[ComponentName]Schema.ts - Storybook is running locally (
pnpm devfrom/home/nextjs-web/apps/storybook)
Our Component Architecture Overview
Our component library in /home/nextjs-web/packages/ui/src/components/ contains 78+ components organized by functionality:
Layout Components: Container, Panel, Mosaic, Deck, Grid structures
Content Components: Card, Tile, ArticleTile, DeckTile
Media Components: Image, Video, Audio, Podcast, PDF
Navigation: Meganav, NavContainer, NavItem, Link, SearchBar
Typography: Heading, RichText, PullQuote, Footnote
Interactive: Button, Modal, Dialog, Popover, Sheet, Toast
Data Display: Table, HighChart, GoogleMaps
Specialized: Logo, Icon, Spinner, Skeleton, Lockup
Content Management: ContentRenderEngine, Resource, Collection
Each component follows a consistent pattern with separation between client and server logic.
Step 1: Component File Structure
Our components follow one of three architectural patterns depending on their complexity and requirements:
Pattern 1: Full Server/Client Split (Most Common)
Used for complex components with both server and client logic.
packages/ui/src/components/Card/
├── Card.tsx # Wrapper that decides server/client routing
├── Card.server.tsx # Server-side rendering logic
├── Card.stories.tsx # Storybook stories
└── __tests__/
└── Card.test.tsx
Example: Card.tsx (Wrapper)
import { TCardProps } from '@schwab/schema/ui/Card/CardSchema';
import CardServer from './Card.server';
/**
* A React Functional Component that leverages props to generate and return JSX.
* @param {TCardProps} props
* @returns {ReturnType<typeof CardServer>}
*/
export default function Card(props: TCardProps): ReturnType<typeof CardServer> {
return <CardServer {...props} />;
}
Example: Card.server.tsx (Implementation)
import { TCardProps } from '@schwab/schema/ui/Card/CardSchema';
import { cva } from 'class-variance-authority';
import { type JSX } from 'react';
import Image from '#components/Image';
import Lockup from '#components/Lockup';
import { baseCVAClasses } from '../../utilities/cva/common';
import { cn } from '../../utilities/shad';
/**
* A React Functional Component that leverages props to generate and return JSX.
* @param {TCardProps} props
* @returns {JSX.Element}
*/
export default function CardServer({
borderColor,
palette,
image,
mediaBleed = false,
hasPriorityImage,
body,
disclosure,
eyebrow,
heading,
headingLink,
headingLevel = 3,
headingStyle = 'bcn-heading--m-bold',
iconComponent,
iconImage,
links,
textAlign,
children,
componentName = 'Card',
className,
}: TCardProps): JSX.Element {
return (
<div
data-component={componentName}
className={cn(cardVariants({ borderColor, palette }), className)}
>
{image && <CardImage image={image} hasPriorityImage={hasPriorityImage} mediaBleed={mediaBleed} />}
<CardBody
body={body}
disclosure={disclosure}
heading={heading}
headingLevel={headingLevel}
headingLink={headingLink}
headingStyle={headingStyle}
iconImage={iconImage}
iconComponent={iconComponent}
eyebrow={eyebrow}
links={links}
textAlign={textAlign}
>
{children}
</CardBody>
</div>
);
}
/**
* Card Variants using Class Variance Authority (CVA)
*/
const cardVariants = cva(
['@container/card', 'rounded-lg', 'overflow-hidden', 'flex', 'flex-col', 'h-full'],
{
variants: {
palette: baseCVAClasses.palette,
borderColor: baseCVAClasses.border,
},
},
);
Pattern 2: Server + Client Split
Used when components need both server and client-specific logic.
packages/ui/src/components/Image/
├── Image.tsx # Main wrapper
├── Image.server.tsx # Server-side logic
├── Image.client.tsx # Client-side logic (for interactivity)
└── __tests__/
└── Image.test.tsx
Example: Image.tsx
import type { TImageProps } from '@schwab/schema/ui/Image/ImageSchema';
import ImageServer from './Image.server';
/**
* A React Functional Component that leverages props to generate and return JSX.
* @param {TImageProps} props
* @returns {ReturnType<typeof ImageServer>}
*/
export default function Image(props: TImageProps): ReturnType<typeof ImageServer> {
return <ImageServer {...props} />;
}
Pattern 3: Simple Single File
Used for simple components without server/client complexity.
packages/ui/src/components/Spinner/
└── Spinner.tsx # Single file with component
Example: Spinner.tsx
import { JSX } from 'react';
import IconSpinner from '#icons/IconSpinner';
/**
* A React Functional Component that leverages props to generate and return JSX.
* @param {React.HTMLAttributes<HTMLDivElement>} props
* @returns {JSX.Element}
*/
function Spinner({ className }: React.HTMLAttributes<HTMLDivElement>): JSX.Element {
return (
<div className={`flex items-center justify-center h-20 w-full ${className}`}>
<IconSpinner className="animate-spin" />
<span className="sr-only">Loading...</span>
</div>
);
}
export { Spinner };
Key Architectural Patterns
1. Component Aliases: Use path aliases for imports:
import Image from '#components/Image';
import Lockup from '#components/Lockup';
import IconSpinner from '#icons/IconSpinner';
2. CVA for Styling: Use Class Variance Authority with our shared base classes:
import { baseCVAClasses } from '../../utilities/cva/common';
const componentVariants = cva(
['base', 'classes'], // Base classes
{
variants: {
palette: baseCVAClasses.palette, // Reuse color palettes
borderColor: baseCVAClasses.border, // Reuse border colors
},
},
);
3. Utility Functions:
Use the cn() utility for conditional classes:
import { cn } from '../../utilities/shad';
<div className={cn(componentVariants({ palette, size }), className)} />
The cn() utility is our shadcn-inspired utility that:
- Merges multiple class strings using
clsx - Resolves Tailwind class conflicts using
twMerge - Handles conditional classes elegantly
- Allows user overrides through
classNameprop
Example usage:
// Basic merging
cn('px-4 py-2', 'bg-blue-500') // 'px-4 py-2 bg-blue-500'
// Conflict resolution (last wins)
cn('px-4', 'px-6') // 'px-6'
// Conditional classes
cn('base-classes', isActive && 'active-classes', className)
// With CVA
cn(buttonVariants({ variant, size }), disabled && 'opacity-50', className)
4. Sub-components: Break complex components into sub-components within the same file:
function CardContainer({ children, palette }: Props): JSX.Element {
return <div className={cn(cardVariants({ palette }))}>{children}</div>;
}
function CardBody({ body, children }: Props): JSX.Element {
return <div className="p-6">{body || children}</div>;
}
5. Data Attributes:
Always add a data-component attribute for debugging:
<div data-component="Card" className={...}>
Available Base CVA Classes
Our design system provides reusable CVA variant classes in /packages/ui/src/utilities/cva/common.ts:
Color Palettes (baseCVAClasses.palette)
Available background color schemes with appropriate text colors:
palette: baseCVAClasses.palette
Available palettes:
white- White background, dark textblue- Blue 700 background, white textblueClickable- Blue with hover statesblue-95- Light blue backgroundsapphire- Sapphire 700 backgroundmetal- Metal 800 background, white textmetal-70- Metal 400 backgroundmetal-95- Light metal backgroundcoastal-40- Coastal 700 with underlined linkscoastal-95- Light coastal backgroundteal,teal-95- Teal variantstangerine-95- Light tangerine backgroundnone- No background styling
Border Colors (baseCVAClasses.border)
Consistent border styling:
borderColor: baseCVAClasses.border
Available borders:
blue,sapphire,metal,metal-70,coastal,white,teal,teal-60,tangerine
Heading Styles (baseCVAClasses.headingStyle)
Typography styles for headings:
headingStyle: baseCVAClasses.headingStyle
Modern heading styles:
bcn-heading--xl,bcn-heading--xl-bold- Extra large (text-4xl/text-7xl)bcn-heading--l,bcn-heading--l-bold- Large (text-3xl/text-4xl)bcn-heading--m,bcn-heading--m-bold- Medium (text-2xl/text-3xl) - Default for most componentsbcn-heading--s,bcn-heading--s-bold- Small (text-2xl)bcn-heading--xs-bold- Extra small (text-xl)
Article heading styles:
bcn-article-heading--3xl,bcn-article-heading--3xl-bold- (text-9xl/text-12xl)bcn-article-heading--2xl,bcn-article-heading--2xl-bold- (text-8xl/text-11xl)bcn-article-heading--xl,bcn-article-heading--xl-bold- (text-7xl/text-10xl)bcn-article-heading--l,bcn-article-heading--l-bold- (text-6xl/text-9xl)
Background Focal Points (baseCVAClasses.backgroundFocalPoint)
For background image positioning:
backgroundFocalPoint: baseCVAClasses.backgroundFocalPoint
Available positions:
left_top,center_top,right_topleft_center,center_center,right_centerleft_bottom,center_bottom,right_bottom
Body Styles (baseCVAClasses.bodyStyle)
Typography for body text:
bodyStyle: baseCVAClasses.bodyStyle
Available styles:
bcn-body--s- Small body text (text-sm)- Various deprecated styles for backward compatibility
Example: Using Base CVA Classes
import { baseCVAClasses } from '../../utilities/cva/common';
import { cva } from 'class-variance-authority';
const myComponentVariants = cva(
['rounded-lg', 'p-6'], // Base classes
{
variants: {
palette: baseCVAClasses.palette, // Reuse all color palettes
borderColor: baseCVAClasses.border, // Reuse all border colors
headingStyle: baseCVAClasses.headingStyle, // Reuse heading typography
// Add custom variants
size: {
small: ['p-3', 'text-sm'],
large: ['p-8', 'text-lg'],
},
},
defaultVariants: {
palette: 'white',
size: 'small',
},
},
);
Step 2: Create the Schema (if needed)
Define your component's props using Zod in the schema package. The schema serves as the single source of truth for component props and automatically generates TypeScript types and Storybook controls.
Location: /home/nextjs-web/packages/schema/src/ui/YourComponent/YourComponentSchema.ts
// packages/schema/src/ui/YourComponent/YourComponentSchema.ts
import { z } from 'zod';
/**
* Schema for YourComponent props.
* This schema is used for:
* - TypeScript type generation
* - Runtime validation
* - Storybook controls generation
* - API validation
*/
export const YourComponentSchema = z.object({
/**
* Component variant style
* Controls the visual appearance of the component
*/
variant: z.enum(['primary', 'secondary', 'ghost']).optional().default('primary'),
/**
* Component size
* Affects padding, font size, and overall dimensions
*/
size: z.enum(['small', 'medium', 'large']).optional().default('medium'),
/**
* Whether the component is disabled
* Disables interaction and applies disabled styling
*/
disabled: z.boolean().optional().default(false),
/**
* Additional CSS classes to apply
* Use for custom styling or layout adjustments
*/
className: z.string().optional(),
/**
* Component content
* Can be text, elements, or other React components
*/
children: z.any(),
});
/**
* TypeScript type inferred from the schema
* Use this type for component props
*/
export type TYourComponentProps = z.infer<typeof YourComponentSchema>;
Important Guidelines:
- JSDoc Comments: Every schema property MUST have a JSDoc comment - these become the Storybook control descriptions
- Naming Convention: Use
Tprefix for types (e.g.,TYourComponentProps) - Schema Export: Export both the schema and the inferred type
- Defaults: Use
.default()for optional props that should have a default value - Enums: Use
z.enum()for props with fixed options - these become select controls in Storybook
Step 3: Create the Stories File
Create a .stories.tsx file in your component directory:
// packages/ui/src/components/YourComponent/YourComponent.stories.tsx
import { YourComponentSchema, TYourComponentProps } from '@schwab/schema/ui/YourComponent/YourComponentSchema';
import { transformZodToArgTypes } from '@schwab/utilities/functions/transformZodToArgTypes';
import type { Meta, StoryObj } from '@storybook/nextjs';
import YourComponent from './YourComponent';
const meta: Meta<TYourComponentProps> = {
title: 'NextGen-Tailwind/YourComponent', // Category/ComponentName
component: YourComponent,
parameters: {
layout: 'padded', // Options: 'centered', 'padded', 'fullscreen'
docs: {
description: {
component: 'A brief description of your component and its primary use case.'
}
}
},
decorators: [
(Story) => (
<div className="max-w-md"> // Optional: Constrain component width
<Story />
</div>
),
],
argTypes: transformZodToArgTypes(
YourComponentSchema.omit({
// Omit internal/complex props from Storybook controls
className: true,
}).shape,
),
};
export default meta;
type Story = StoryObj<TYourComponentProps>;
// Default/primary story - this becomes the "Default" in Storybook
export const Default: Story = {
name: 'Default State',
args: {
variant: 'primary',
size: 'medium',
children: 'Your Component',
},
};
// Additional variant stories
export const Secondary: Story = {
name: 'Secondary Variant',
args: {
variant: 'secondary',
size: 'medium',
children: 'Secondary Component',
},
};
// Story with custom render function
export const WithCustomRender: Story = {
name: 'With Custom Context',
args: {
variant: 'primary',
size: 'large',
children: 'Custom Rendered Component',
},
render: (args) => {
return (
<>
<p className="text-xs mb-6">
<b>Note:</b> This story demonstrates custom rendering with additional context.
</p>
<YourComponent {...args} />
</>
);
},
};
Step 3b: Optional - Add MDX Documentation
For complex components, you can add an .mdx file for enhanced documentation:
packages/ui/src/components/Panel/
├── Panel.tsx
├── Panel.server.tsx
├── Panel.stories.tsx
└── Panel.mdx # Enhanced documentation
Example: Panel.mdx
import { Meta, Controls, Primary, Stories } from '@storybook/addon-docs/blocks';
import * as PanelStories from './Panel.stories';
<Meta of={PanelStories} />
# Panel
## Description
Panel extends Container. It is intended to show children components in a single or row layout without wrapping in desktop view.
## Usage
- Children elements should be wrapped in a DIV element in order for layout to be applied correctly.
- Use for horizontal layouts of related content
- Provides consistent spacing and alignment
## Accessibility Guidelines
- Ensure proper heading hierarchy within panel content
- Use semantic HTML for child elements
- Maintain color contrast ratios
## Known Issues
None
<Controls />
<Stories />
This creates a rich documentation page with:
- Component description
- Usage guidelines
- Accessibility notes
- Interactive controls
- All stories displayed below
Step 4: Key Configuration Options
Story Title Organization
The title property determines the component's location in Storybook's sidebar:
title: 'NextGen-Tailwind/ComponentName' // Appears in NextGen-Tailwind section
title: 'NextGen-Tailwind/Forms/Input' // Creates nested folders
title: 'Deprecated/OldComponent' // Appears in Deprecated section
Our standard categories (defined in .storybook/preview.tsx):
NextGen-Tailwind- Modern Tailwind CSS componentsDocumentation- Documentation pagesMFE- Micro-frontend componentsDeprecated- Legacy components
Layout Options
Control the story canvas layout with the layout parameter:
parameters: {
layout: 'centered', // Center component (good for small components)
layout: 'padded', // Add padding (default for most components)
layout: 'fullscreen', // No padding (good for page-level components)
}
Using transformZodToArgTypes
This utility automatically generates Storybook controls from your Zod schema:
argTypes: transformZodToArgTypes(
YourComponentSchema.omit({
// Omit props that shouldn't be controlled in Storybook
className: true,
children: true,
internalProp: true,
}).shape,
)
Benefits:
- Automatic control types (select, boolean, text, etc.)
- Auto-generated descriptions from JSDoc comments
- Type-safe prop controls
- Single source of truth for props
Decorators
Decorators wrap your stories with additional markup or context:
decorators: [
(Story) => (
<div className="max-w-md p-4 bg-gray-100">
<Story />
</div>
),
]
Common use cases:
- Constrain component width
- Add background colors
- Provide layout context
- Add theme providers
Step 5: Story Naming Best Practices
// ✅ Good: Descriptive, clear purpose
export const Default: Story = { name: 'Default Button' };
export const PrimaryLarge: Story = { name: 'Primary Large' };
export const WithIcon: Story = { name: 'With Leading Icon' };
export const DisabledState: Story = { name: 'Disabled State' };
// ❌ Bad: Vague or technical
export const Story1: Story = { name: 'Test' };
export const Example: Story = { name: 'Example' };
Step 6: Testing Your Stories Locally
-
Start Storybook development server:
cd /home/nextjs-web/apps/storybook
pnpm dev -
Access Storybook: Open http://localhost:6006 in your browser
-
Verify your component:
- Navigate to your component in the sidebar
- Test all story variants
- Verify controls work correctly
- Check the "Docs" tab for auto-generated documentation
- Test accessibility with the a11y addon (bottom panel)
-
Check for issues:
- No console errors
- All props render correctly
- Controls update the component
- Documentation is complete and accurate
Step 7: Advanced Story Patterns
Multiple Stories with Shared Args
const BaseArgs = {
variant: 'primary',
size: 'medium',
};
export const Default: Story = {
args: BaseArgs,
};
export const Large: Story = {
args: {
...BaseArgs,
size: 'large',
},
};
Stories with Complex Children
export const WithComplexContent: Story = {
args: {
variant: 'primary',
},
render: (args) => (
<YourComponent {...args}>
<div className="flex items-center gap-2">
<Icon name="check" />
<span>Complex Content</span>
</div>
</YourComponent>
),
};
Stories with NextJS-specific Features
export const BankContext: Story = {
args: {
variant: 'primary',
},
parameters: {
nextjs: {
navigation: {
pathname: '/bank', // Simulate bank page context
},
},
},
};
Step 8: Storybook Configuration Files
Our Storybook is configured with these key files:
.storybook/main.ts
- Defines where stories are located
- Configures addons
- Sets up framework options
- Manages static assets
stories: [
'../../../packages/ui/src/components/**/*.mdx',
'../../../packages/ui/src/components/**/*.stories.@(js|jsx|mjs|ts|tsx)',
'../docs/**/*.mdx',
]
.storybook/preview.tsx
- Configures global story parameters
- Sets up decorators (like fonts)
- Defines story sorting order
- Configures global toolbar options
Step 9: Embedding Stories in Documentation
You can embed your Storybook stories in other documentation using iframes:
<!-- Embed a specific story -->
<iframe
src="https://storybook.charles-schwab.vercel.app/iframe.html?id=nextgen-tailwind-yourcomponent--default&viewMode=story"
width="100%"
height="400">
</iframe>
<!-- Embed full component docs -->
<iframe
src="https://storybook.charles-schwab.vercel.app/iframe.html?id=nextgen-tailwind-yourcomponent--docs&viewMode=docs"
width="100%"
height="600">
</iframe>
How to find your story ID:
- Navigate to your story in Storybook
- Look at the URL:
?path=/story/<story-id> - Story ID format:
category-componentname--storyname(all lowercase, kebab-case)
URL Parameters:
viewMode=story- Show just the componentviewMode=docs- Show documentation pageargs=propName:value- Override propsnav=0- Hide navigation
Step 10: Common Patterns from Existing Components
Card Component Pattern
// Omit complex props, provide realistic defaults
argTypes: transformZodToArgTypes(
CardSchema.omit({
role: true,
badgeType: true,
video: true,
}).shape,
)
Logo Component Pattern
// Responsive story with explanatory note
export const Responsive: Story = {
name: 'Responsive Logo',
args: {
className: 'size-12 lg:size-24',
type: 'responsive',
},
render: (args) => (
<>
<p className="text-xs mb-6">
<b>Note:</b> Change screen width to see responsive behavior.
</p>
<Logo {...args} />
</>
),
};
Mosaic Component Pattern
// Dynamic child components
render: (args) => (
<Mosaic {...args}>
{createChildComponents(args.itemCount)}
</Mosaic>
)
Storybook Configuration Best Practices
Addons Configuration
Our Storybook uses these essential addons (configured in .storybook/main.ts):
@storybook/addon-a11y- Accessibility testing and WCAG validation@storybook/addon-docs- Auto-generated documentation from stories@storybook/addon-essentials- Core Storybook functionality (via NextJS framework)@storybook/nextjs- NextJS-specific integration
Global Configuration
Configured in .storybook/preview.tsx:
parameters: {
options: {
storySort: {
method: 'alphabetical',
order: ['NextGen-Tailwind', 'Documentation', 'MFE', '*', 'Deprecated'],
},
},
controls: {
sort: 'requiredFirst', // Required props appear first
disableSaveFromUI: true, // Prevent saving from UI
},
}
Custom Toolbar Features
We have a custom "Main Column" toolbar that shows brand standard column guides:
globalTypes: {
showMainColumn: {
name: 'Main column',
description: 'Shows guides for the main content column',
toolbar: {
icon: 'component',
items: [
{ value: true, title: 'Show Main Column' },
{ value: false, title: 'Hide Main Column' },
],
},
},
}
Running Storybook
Development Mode
cd /home/nextjs-web/apps/storybook
pnpm dev
Starts Storybook on http://localhost:6006
Build for Production
cd /home/nextjs-web/apps/storybook
pnpm build
Outputs to storybook-static/ directory
Production Deployment
Our Storybook is automatically deployed to Vercel:
- Production: https://storybook.charles-schwab.vercel.app/
- Preview builds: Generated for each pull request
Real-World Examples
Example 1: Card Component
See /home/nextjs-web/packages/ui/src/components/Card/Card.stories.tsx for a complete example showing:
- Multiple variants (with image, with icon, text-only)
- Custom render functions
- Omitting complex props from controls
- Using realistic test data
Example 2: Logo Component
See /home/nextjs-web/packages/ui/src/components/Logo/Logo.stories.tsx for:
- Context-dependent rendering (bank vs. default)
- Responsive stories with explanatory notes
- NextJS navigation parameter usage
Example 3: Mosaic Component
See /home/nextjs-web/packages/ui/src/components/Mosaic/Mosaic.stories.tsx for:
- Dynamic child component generation
- Multiple layout variations
- Complex component composition
Component Creation Workflow Guide
Complete Workflow for Creating a New Component
Follow these steps to create a production-ready component from scratch:
Step 1: Planning & Design Review
- Review design specs and component requirements
- Identify reusability opportunities
- Determine which architectural pattern to use:
- shadcn-style (in
/Shadcn/folder) - Interactive primitives with Radix UI - Full Server/Client split - Complex components with server rendering
- Simple single file - Basic presentational components
- shadcn-style (in
- Check if similar components exist that could be extended
- Check if a Radix UI primitive exists for interactive components
- Get stakeholder alignment on component scope
Step 2: Create Directory Structure
# Navigate to components directory
cd /home/nextjs-web/packages/ui/src/components
# Create component directory
mkdir YourComponent
cd YourComponent
# Create test directory
mkdir __tests__
Step 3: Create Zod Schema
# Navigate to schema directory
cd /home/nextjs-web/packages/schema/src/ui
# Create component schema directory
mkdir YourComponent
cd YourComponent
# Create schema file
touch YourComponentSchema.ts
Edit the schema with proper JSDoc comments and type definitions.
Step 4: Implement Component Files
Create files in this order:
YourComponent.tsx- Main wrapperYourComponent.server.tsx- ImplementationYourComponent.client.tsx- Client logic (if needed)
Use existing components as reference (Card, Panel, Lockup).
Step 5: Create Stories
cd /home/nextjs-web/packages/ui/src/components/YourComponent
touch YourComponent.stories.tsx
Create at least 3 stories showing different use cases.
Step 6: Start Storybook and Iterate
cd /home/nextjs-web/apps/storybook
pnpm dev
Navigate to http://localhost:6006 and test your component:
- Verify all variants render correctly
- Test controls in Storybook
- Check accessibility panel
- Test responsive behavior
- Verify in different color palettes
Step 7: Create Tests
cd /home/nextjs-web/packages/ui/src/components/YourComponent/__tests__
touch YourComponent.test.tsx
Write unit tests for component logic and rendering.
Step 8: Documentation
If component is complex, create MDX documentation:
cd /home/nextjs-web/packages/ui/src/components/YourComponent
touch YourComponent.mdx
Step 9: Integration Testing
Test component in actual application context:
- Import component into a page
- Test with real data
- Verify no styling conflicts
- Check performance
Step 10: Code Review & Deploy
- Run type checking:
pnpm type-check - Run tests:
pnpm test - Build Storybook:
pnpm build - Create PR with preview links
- Address review feedback
- Merge when approved
Quick Reference: File Templates
Minimal Component Template
// YourComponent.tsx
import { TYourComponentProps } from '@schwab/schema/ui/YourComponent/YourComponentSchema';
import { JSX } from 'react';
import YourComponentServer from './YourComponent.server';
export default function YourComponent(props: TYourComponentProps): JSX.Element {
return <YourComponentServer {...props} />;
}
Minimal Server Component Template
// YourComponent.server.tsx
import { TYourComponentProps } from '@schwab/schema/ui/YourComponent/YourComponentSchema';
import { cva } from 'class-variance-authority';
import { type JSX } from 'react';
import { baseCVAClasses } from '../../utilities/cva/common';
import { cn } from '../../utilities/shad';
export default function YourComponentServer({
palette = 'white',
className,
children,
}: TYourComponentProps): JSX.Element {
return (
<div
data-component="YourComponent"
className={cn(variants({ palette }), className)}
>
{children}
</div>
);
}
const variants = cva(['rounded-lg'], {
variants: {
palette: baseCVAClasses.palette,
},
defaultVariants: {
palette: 'white',
},
});
Minimal Schema Template
// YourComponentSchema.ts
import { z } from 'zod';
/**
* Schema for YourComponent props
*/
export const YourComponentSchema = z.object({
/** Color palette for component background and text */
palette: z.enum(['white', 'blue', 'metal']).optional().default('white'),
/** Additional CSS classes */
className: z.string().optional(),
/** Component children */
children: z.any(),
});
export type TYourComponentProps = z.infer<typeof YourComponentSchema>;