Skip to content

Storybook Component Workflow

Created: October 12, 2025
Purpose: Complete workflow for developing, documenting, and consuming components in our monorepo
Status: Production-ready reference guide

Version notice (2025-10-13): Tailwind is standardized at v3.4.10 across apps and Storybook. Use CommonJS tailwind.config.js with darkMode: 'class' and content: { relative: true, files: [...] }. PostCSS should use { tailwindcss: {}, autoprefixer: {} } and the CSS entry must include @tailwind base; @tailwind components; @tailwind utilities; (do not use @import "tailwindcss" or @source). Storybook’s PostCSS must explicitly point to its Tailwind config. See docs/tailwind-parity-checklist.md.


  1. Overview
  2. Workflow: Component → Storybook → App
  3. Creating a New Component
  4. Adding Components to Storybook
  5. Using Components in Apps
  6. Best Practices
  7. Troubleshooting
  8. Quick Start Checklist
  9. PR Workflow & CI
  10. Static Publish (Optional)

Our monorepo has a three-tier component architecture:

Component Libraries (packages/ui-*)
Storybook (packages/storybook)
Applications (apps/*)
packages/
├── ui/ # Legacy/base components (being phased out)
├── ui-web/ # Generic cross-division web components
├── ui-stay-match/ # Stay Match division-specific components
├── ui-guestroom/ # Guestroom division-specific components
├── ui-bonjour-services/ # Bonjour Services division-specific components
└── ... (other divisions)
  1. Develop in isolation - Build components in Storybook first
  2. Document as you build - Stories = documentation + examples
  3. Test interactively - Use Storybook controls and actions
  4. Import with confidence - Well-tested components ready for production

Use this when adding a new component end-to-end.

  1. Create component in the right package
  • Generic web → packages/ui-web
  • Division-specific → packages/ui-{division} (e.g., ui-stay-match)
  1. Export it from the package index
  • Update packages/<pkg>/src/index.ts
  1. Add Storybook story
  • Create packages/storybook/stories/<Component>.stories.tsx
  • Use argTypes and multiple variants
  1. Run Storybook and verify
  • Start: yarn nx storybook storybook
  • Check: dark mode, controls, actions, responsive, a11y
  1. Use in an app and build
  • Import in app code
  • Build: yarn nx build <app-name>
  1. Open a PR
  • Include screenshots/GIF from Storybook
  • Link to story path (e.g., ui-web/Card)

graph LR
A[Create Component] --> B[Write TypeScript/JSX]
B --> C[Export from Package]
C --> D[Create Story]
D --> E[Test in Storybook]
E --> F{Works?}
F -->|No| B
F -->|Yes| G[Import in App]
G --> H[Use in Production]
  1. Create Component in appropriate package
  2. Export from package index
  3. Create Story in Storybook
  4. Develop & Test in Storybook
  5. Import into app
  6. Deploy to production

Decision tree:

Is it web-only?
├─ Yes: Is it division-specific?
│ ├─ Yes → packages/ui-{division}/
│ └─ No → packages/ui-web/
└─ No: Is it cross-platform?
└─ Yes → Create in appropriate package with .native.tsx variant

Examples:

  • Generic Button for all web apps → packages/ui-web/
  • Stay Match Hero component → packages/ui-stay-match/
  • Guestroom booking form → packages/ui-guestroom/
Terminal window
# Example: Creating a Card component in ui-web
cd /Users/work-station/company/cloudalt-frontend
# Navigate to the package
cd packages/ui-web/src/components
# Create component directory
mkdir Card
cd Card
# Create files
touch Card.tsx
touch Card.test.tsx # Optional but recommended
touch index.ts
touch README.md # Optional but helpful

Template: Card.tsx

/**
* Card Component
*
* A container component with optional header, footer, and actions.
*
* Features:
* - Multiple variants (elevated, outlined, filled)
* - Optional header and footer
* - Clickable with hover effects
* - Dark mode support
*/
import React from 'react';
import { tv, type VariantProps } from 'tailwind-variants';
import { twMerge } from 'tailwind-merge';
const card = tv({
base: 'rounded-lg transition-all duration-200',
variants: {
variant: {
elevated: 'bg-white dark:bg-gray-800 shadow-md hover:shadow-lg',
outlined: 'bg-transparent border-2 border-gray-300 dark:border-gray-600',
filled: 'bg-gray-100 dark:bg-gray-700',
},
padding: {
none: 'p-0',
sm: 'p-4',
md: 'p-6',
lg: 'p-8',
},
clickable: {
true: 'cursor-pointer hover:scale-[1.02]',
false: '',
},
},
defaultVariants: {
variant: 'elevated',
padding: 'md',
clickable: false,
},
});
export type CardVariants = VariantProps<typeof card>;
export interface CardProps extends CardVariants {
/** Card content */
children: React.ReactNode;
/** Optional header content */
header?: React.ReactNode;
/** Optional footer content */
footer?: React.ReactNode;
/** Click handler (makes card clickable) */
onClick?: () => void;
/** Custom class names */
className?: string;
}
export const Card: React.FC<CardProps> = ({
children,
header,
footer,
variant,
padding,
clickable,
onClick,
className,
}) => {
const isClickable = clickable || !!onClick;
return (
<div
className={card({ variant, padding, clickable: isClickable, className: twMerge(className) })}
onClick={onClick}
role={isClickable ? 'button' : undefined}
tabIndex={isClickable ? 0 : undefined}
>
{header && (
<div className="border-b border-gray-200 dark:border-gray-700 pb-4 mb-4">
{header}
</div>
)}
<div>{children}</div>
{footer && (
<div className="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4">
{footer}
</div>
)}
</div>
);
};

Export: index.ts

export { Card } from './Card';
export type { CardProps, CardVariants } from './Card';

Edit packages/ui-web/src/index.ts:

// Existing exports
export * from './components/Hero';
export * from './components/Button';
// Add new component
export * from './components/Card';
export * from './types';
Terminal window
cd /Users/work-station/company/cloudalt-frontend
yarn nx build ui-web

Terminal window
cd /Users/work-station/company/cloudalt-frontend/packages/storybook/stories
# Option 1: Top-level story
touch Card.stories.tsx
# Option 2: Organize in subdirectory
mkdir -p Components
touch Components/Card.stories.tsx

Template: Card.stories.tsx

import type { Meta, StoryObj } from '@storybook/react';
import { Card } from '@cloudalt-frontend/ui-web';
import React from 'react';
const meta: Meta<typeof Card> = {
title: 'ui-web/Card',
component: Card,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
onClick: { action: 'clicked' },
variant: {
control: 'select',
options: ['elevated', 'outlined', 'filled'],
},
padding: {
control: 'select',
options: ['none', 'sm', 'md', 'lg'],
},
},
};
export default meta;
type Story = StoryObj<typeof Card>;
// Basic story
export const Default: Story = {
args: {
children: (
<div>
<h3 className="text-xl font-bold mb-2">Card Title</h3>
<p className="text-gray-600 dark:text-gray-400">
This is a basic card component with some content.
</p>
</div>
),
},
};
// With header and footer
export const WithHeaderAndFooter: Story = {
args: {
header: <h2 className="text-lg font-semibold">Card Header</h2>,
children: (
<p className="text-gray-600 dark:text-gray-400">
Card content goes here. This card has both a header and footer.
</p>
),
footer: (
<div className="flex gap-2">
<button className="px-4 py-2 bg-primary-500 text-white rounded">
Action
</button>
<button className="px-4 py-2 bg-gray-200 text-gray-700 rounded">
Cancel
</button>
</div>
),
},
};
// Variants
export const Elevated: Story = {
args: {
variant: 'elevated',
children: <p>Elevated card with shadow</p>,
},
};
export const Outlined: Story = {
args: {
variant: 'outlined',
children: <p>Outlined card with border</p>,
},
};
export const Filled: Story = {
args: {
variant: 'filled',
children: <p>Filled card with background</p>,
},
};
// Clickable
export const Clickable: Story = {
args: {
clickable: true,
children: (
<div>
<h3 className="text-xl font-bold mb-2">Clickable Card</h3>
<p className="text-gray-600 dark:text-gray-400">
Click me! I have hover effects.
</p>
</div>
),
},
};
// Padding variants
export const NoPadding: Story = {
args: {
padding: 'none',
children: (
<img
src="https://via.placeholder.com/400x300"
alt="Card content"
className="w-full"
/>
),
},
};
Terminal window
cd /Users/work-station/company/cloudalt-frontend
yarn nx storybook storybook

Access: http://localhost:6006/

Note: The Storybook project name is storybook (see packages/storybook/project.json). Nx targets available:

  • yarn nx storybook storybook (dev)
  • yarn nx build-storybook storybook (static export)

Checklist:

  • Component renders without errors
  • All variants display correctly
  • Controls panel updates props in real-time
  • Actions panel logs events (clicks, etc.)
  • Dark mode toggle works
  • Responsive in different viewport sizes
  • TypeScript types are correct (no red squiggles)

If issues are found:

  1. Fix component in packages/ui-web/src/components/Card/Card.tsx
  2. Storybook will hot-reload automatically
  3. Re-test
  4. Repeat until perfect

Terminal window
# Check the package exports
cat packages/ui-web/src/index.ts
# Should include:
# export * from './components/Card';

Example: Using Card in apps/stay_match/pinkguest/web/src/App.tsx

import React from 'react';
import { Card, Button } from '@cloudalt-frontend/ui-web';
function App() {
return (
<div className="p-8">
<Card
variant="elevated"
header={<h1 className="text-2xl font-bold">Welcome to PinkGuest</h1>}
footer={
<Button color="primary" onClick={() => console.log('Get Started!')}>
Get Started
</Button>
}
>
<p className="text-gray-600 dark:text-gray-400">
Find your perfect stay with PinkGuest - the LGBTQ+ friendly accommodation platform.
</p>
</Card>
</div>
);
}
export default App;
Terminal window
cd /Users/work-station/company/cloudalt-frontend
# Development server
yarn nx serve pinkguest-web
# Or production build
yarn nx build pinkguest-web

Once verified, deploy as normal through your CI/CD pipeline.


  • Use TypeScript - Full type safety for props
  • Use tailwind-variants - Type-safe variants
  • Support dark mode - Include dark: classes
  • Document thoroughly - JSDoc comments on all exports
  • Test edge cases - Empty states, loading states, errors
  • Follow naming conventions - PascalCase for components
  • Export types - Export both component and prop types
  • Use semantic HTML - Proper accessibility
  • Inline large JSX - Extract to separate components
  • Hardcode values - Use props or design tokens
  • Skip TypeScript - No any types
  • Forget dark mode - Must work in both themes
  • Over-engineer - Keep it simple and focused
  • Create dependencies - Components should be independent
  • Create multiple stories - Show all variants
  • Use meaningful names - Default, WithHeader, Clickable
  • Add argTypes - Enable interactive controls
  • Add actions - Log events for testing
  • Test edge cases - Empty, loading, error states
  • Use real-ish data - Not just “Lorem ipsum”
  • Document props - Use JSDoc or MDX docs
  • One story only - Show multiple use cases
  • Skip controls - Make stories interactive
  • Use production data - Mock data only
  • Forget dark mode - Test in both themes
  • Ignore accessibility - Use a11y addon

When to use:

  • Component is useful across multiple divisions
  • No division-specific branding
  • Reusable patterns (buttons, inputs, cards)

Examples:

  • Button, Input, Select, Checkbox
  • Card, Modal, Dialog, Drawer
  • Layout components (Container, Grid, Flex)

When to use:

  • Component has division-specific branding
  • Uses division-specific assets
  • Custom business logic for that division

Examples:

  • Stay Match Hero with specific imagery
  • Guestroom booking flow components
  • Bonjour Services dashboard widgets

Problem: Import error in story file

Solution:

Terminal window
# 1. Verify component is exported
cat packages/ui-web/src/index.ts
# 2. Rebuild the package
yarn nx build ui-web
# 3. Restart Storybook
# Kill the process (Ctrl+C)
yarn nx storybook storybook

Problem: TypeScript errors or no autocomplete

Solution:

Terminal window
# Rebuild TypeScript declarations
yarn nx build ui-web
# Restart VS Code TypeScript server
# CMD+Shift+P → "TypeScript: Restart TS Server"

Problem: Tailwind classes not working

Checklist:

  • Is component path in tailwind.config.js content array?
  • Is Tailwind CSS imported in preview.ts?
  • Did you use standard Tailwind classes (not custom)?
  • Is dark mode class on parent element?

Fix for missing content path:

packages/storybook/tailwind.config.js
export default {
presets: [require('../config/tailwind-preset.js')],
content: [
'./stories/**/*.{js,jsx,ts,tsx,mdx}',
'../ui-web/src/**/*.{js,jsx,ts,tsx}',
'../ui-stay-match/src/**/*.{js,jsx,ts,tsx}',
// Add your new package here
'../ui-guestroom/src/**/*.{js,jsx,ts,tsx}',
],
};

Tip: if you add a new UI package (e.g., ui-pride-city), append its src glob to the content array to ensure Tailwind picks up its classes in Storybook.

Problem: Component renders with error in Storybook

Debug steps:

  1. Check browser console for errors
  2. Verify all required props are provided in story args
  3. Check component implementation for bugs
  4. Verify imports are correct
  5. Try simplifying the story to minimal example

Problem: Changes not reflected in Storybook

Solutions:

Terminal window
# Clear Storybook cache
rm -rf node_modules/.cache/storybook
# Clear Nx cache
yarn nx reset
# Restart Storybook
yarn nx storybook storybook

Problem: Can’t import component in app

Checklist:

  • Component exported from package index?
  • Package built successfully?
  • App has package as dependency? (Should be automatic in monorepo)
  • TypeScript can find the types?

Verify:

Terminal window
# Check if package builds
yarn nx build ui-web
# Check if app can build
yarn nx build pinkguest-web

Terminal window
# Navigate to workspace root
cd /Users/work-station/company/cloudalt-frontend
# Create component (example)
mkdir -p packages/ui-web/src/components/NewComponent
touch packages/ui-web/src/components/NewComponent/{index.ts,NewComponent.tsx}
# Build package
yarn nx build ui-web
Terminal window
# Start Storybook dev server
yarn nx storybook storybook
# Build static Storybook
yarn nx build-storybook storybook
# Clear cache
rm -rf node_modules/.cache/storybook
Terminal window
# Run app dev server
yarn nx serve pinkguest-web
# Build app for production
yarn nx build pinkguest-web
# Test app
yarn nx test pinkguest-web
Terminal window
# See affected projects
yarn nx affected:apps
# Run command on affected projects
yarn nx affected --target=build
# Graph dependencies
yarn nx graph

Follow this lightweight process for component changes:

  1. Branching
  • Create a feature branch from your current base (e.g., feat/<component-name>)
  1. Local verification
  • Run Storybook and validate all variants
  • Add or update stories as needed
  1. Commit hygiene
  • Commit component files + stories together
  • Include screenshots/GIFs in PR description
  1. CI expectations
  • Static build of Storybook: yarn nx build-storybook storybook
  • App build (at least one consumer app): yarn nx build <app-name>
  1. Review
  • Link to Storybook story paths in PR (e.g., ui-web/Card)
  • Address feedback with follow-up commits
  1. Merge & cleanup
  • Squash & merge if appropriate
  • Delete feature branch after merge

Generate a static Storybook for sharing or hosting:

dist/storybook
yarn nx build-storybook storybook

You can host dist/storybook on any static hosting (e.g., GitHub Pages, Netlify, Vercel, S3). Add a CI step to publish on merges to a main branch if desired.


Step 1: Create component

Terminal window
cd /Users/work-station/company/cloudalt-frontend
mkdir -p packages/ui-web/src/components/Alert
cd packages/ui-web/src/components/Alert

Step 2: Write component (Alert.tsx)

import React from 'react';
import { tv, type VariantProps } from 'tailwind-variants';
const alert = tv({
base: 'p-4 rounded-lg border',
variants: {
variant: {
info: 'bg-blue-50 border-blue-200 text-blue-800 dark:bg-blue-900 dark:border-blue-700 dark:text-blue-200',
success: 'bg-green-50 border-green-200 text-green-800 dark:bg-green-900 dark:border-green-700 dark:text-green-200',
warning: 'bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900 dark:border-yellow-700 dark:text-yellow-200',
error: 'bg-red-50 border-red-200 text-red-800 dark:bg-red-900 dark:border-red-700 dark:text-red-200',
},
},
defaultVariants: {
variant: 'info',
},
});
export type AlertVariants = VariantProps<typeof alert>;
export interface AlertProps extends AlertVariants {
children: React.ReactNode;
title?: string;
onClose?: () => void;
}
export const Alert: React.FC<AlertProps> = ({ children, title, variant, onClose }) => {
return (
<div className={alert({ variant })}>
{title && <div className="font-bold mb-1">{title}</div>}
<div>{children}</div>
{onClose && (
<button onClick={onClose} className="ml-auto"></button>
)}
</div>
);
};

Step 3: Export component

packages/ui-web/src/components/Alert/index.ts
export { Alert } from './Alert';
export type { AlertProps, AlertVariants } from './Alert';
// packages/ui-web/src/index.ts
export * from './components/Alert';

Step 4: Create story

packages/storybook/stories/Alert.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Alert } from '@cloudalt-frontend/ui-web';
const meta: Meta<typeof Alert> = {
title: 'ui-web/Alert',
component: Alert,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof Alert>;
export const Info: Story = {
args: {
variant: 'info',
title: 'Information',
children: 'This is an info alert.',
},
};
export const Success: Story = {
args: {
variant: 'success',
title: 'Success!',
children: 'Your changes have been saved.',
},
};
export const Warning: Story = {
args: {
variant: 'warning',
title: 'Warning',
children: 'Please review before proceeding.',
},
};
export const Error: Story = {
args: {
variant: 'error',
title: 'Error',
children: 'Something went wrong.',
},
};

Step 5: Test in Storybook

Terminal window
yarn nx storybook storybook
# Open http://localhost:6006/
# Navigate to ui-web/Alert
# Test all variants

Step 6: Use in app

apps/stay_match/pinkguest/web/src/pages/Dashboard.tsx
import { Alert } from '@cloudalt-frontend/ui-web';
function Dashboard() {
return (
<div className="p-8">
<Alert variant="success" title="Welcome back!">
You have 3 new messages.
</Alert>
</div>
);
}

Done!


This workflow ensures:

  • Consistent development - Same process for all components
  • Quality components - Tested in isolation before production
  • Living documentation - Storybook as single source of truth
  • Fast development - Reuse proven components
  • Type safety - Full TypeScript support
  • Dark mode support - Built-in from the start

Remember: Component → Storybook → App. Always develop in isolation first!


Document: STORYBOOK_WORKFLOW.md
Last Updated: October 12, 2025
Maintained by: CloudAlt Engineering Team