Practical Component Design Guide with React × Tailwind CSS × Emotion: The Optimal Approach to Design Systems, State Management, and Reusability

  • react
    react
  • tailwindcss
    tailwindcss
  • emotion
    emotion
  • storybook
    storybook
  • figma
    figma
  • nextjs
    nextjs
Published on 2024/11/22

Introduction

Component design is an essential element of modern frontend development. Well-designed components not only improve development efficiency but also greatly contribute to maintainability and scalability.

In this article, we will explain best practices for component design while leveraging a tech stack that includes React, Tailwind CSS, Emotion, Storybook, Figma, and Next.js. Based on the following points, we will introduce guidelines for driving development grounded in a design system.

  • Creating component design guidelines (naming conventions based on a design system)
  • Component state management (proper use of props, state, and context)
  • Abstraction to improve component reusability
  • Establishing style guidelines (guidelines to maintain UI consistency)
  • Applying theme settings (style management based on themes)
  • Incorporating accessibility support (checking accessibility for each component)
  • Component version control (introducing proper version management)
  • Creating component documentation (detailed usage guides for consumers)

By properly organizing component design, you can not only improve the development speed of your project but also enhance UI consistency and usability. Use the content introduced here as a reference to practice efficient and maintainable component design.

Component Design Guidelines (Naming Conventions Based on a Design System)

We will introduce naming conventions and best practices for designing highly consistent and reusable components based on a design system. By leveraging Tailwind CSS or Emotion, you can enable flexible styling.

Basic Policy for Naming Conventions

Use Atomic Design to classify components at an appropriate level of granularity.

  • Atoms
    The smallest unit of UI components, which do not have functionality on their own and are used in combination with other components.

    • Examples
      • Button.tsx
      • Input.tsx
      • Icon.tsx
      • Typography.tsx
  • Molecules
    Components that combine multiple Atoms to provide a single piece of functionality.

    • Examples
      • SearchBar.tsx (Input + Button)
      • UserCard.tsx (Avatar + Text)
      • FormField.tsx (Label + Input)
  • Organisms
    Components that combine Molecules and become major building blocks of a page.

    • Examples
      • Header.tsx (Logo + NavigationMenu + SearchBar)
      • Sidebar.tsx (UserProfile + NavigationLinks)
      • ProductList.tsx (a collection of ProductCard)

Example Implementation of Components Using Tailwind CSS

Button component

// components/atoms/Button.tsx
import { cn } from "@/utils/cn"; // Function to merge Tailwind utilities

type ButtonProps = {
  variant?: "primary" | "secondary";
  children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

export const Button = ({ variant = "primary", children, className, ...props }: ButtonProps) => {
  return (
    <button
      className={cn(
        "px-4 py-2 rounded-md text-white font-medium transition",
        variant === "primary" && "bg-blue-500 hover:bg-blue-600",
        variant === "secondary" && "bg-gray-500 hover:bg-gray-600",
        className
      )}
      {...props}
    >
      {children}
    </button>
  );
};

Key points

  • Dynamically combine className with the cn() function
  • Switch styles with the variant property
  • Inherit standard button attributes with ButtonHTMLAttributes<HTMLButtonElement>

Function to combine Tailwind utilities

utils.ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

UserCard component

// components/molecules/UserCard.tsx
import { Button } from "../atoms/Button";

type UserCardProps = {
  name: string;
  avatarUrl: string;
};

export const UserCard = ({ name, avatarUrl }: UserCardProps) => {
  return (
    <div className="flex items-center p-4 border border-gray-300 rounded-lg shadow-sm">
      <img src={avatarUrl} alt={name} className="w-12 h-12 rounded-full" />
      <div className="ml-3">
        <h3 className="text-lg font-semibold">{name}</h3>
        <Button variant="primary" className="mt-2">Follow</Button>
      </div>
    </div>
  );
};

Key points

  • Use border border-gray-300 rounded-lg shadow-sm to set the card border and shadow
  • Use w-12 h-12 rounded-full to make the avatar image circular

Example Implementation of Components Using Emotion

Button component

import { css } from "@emotion/react";

type ButtonProps = {
  variant?: "primary" | "secondary";
  children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

const buttonBaseStyle = css`
  padding: 8px 16px;
  border-radius: 4px;
  font-size: 16px;
  font-weight: 500;
  color: white;
  transition: background-color 0.2s;
  border: none;
  cursor: pointer;
`;

const variantStyles = {
  primary: css`
    background-color: #007bff;
    &:hover {
      background-color: #0056b3;
    }
  `,
  secondary: css`
    background-color: #6c757d;
    &:hover {
      background-color: #5a6268;
    }
  `,
};

export const Button = ({ variant = "primary", children, ...props }: ButtonProps) => {
  return (
    <button css={[buttonBaseStyle, variantStyles[variant]]} {...props}>
      {children}
    </button>
  );
};

Key points

  • Use @emotion/react and define styles with the css prop
  • Dynamically change styles according to variant

Component State Management (Proper Use of props, state, and context)

State management in components is extremely important in React design. By understanding the characteristics of each approach and using them appropriately, you can improve code readability and maintainability.

When to Use props

Use props when passing data from a parent component to a child component.
The basic rule is that “passed data is not modified (immutable).”

Appropriate use cases

  • Passing data required for UI display
  • Passing parent component state or functions to children
type ButtonProps = {
  label: string;
  onClick: () => void;
};

const Button = ({ label, onClick }: ButtonProps) => {
  return <button onClick={onClick}>{label}</button>;
};

// Parent component
const App = () => {
  const handleClick = () => {
    console.log("Button was clicked");
  };

  return <Button label="Click Me" onClick={handleClick} />;
};

Key points

  • label is display data → appropriate to pass via props
  • onClick is an event handler → pass via props and use inside the child component

Cases where you should use props
✅ When passing data from parent to child
✅ When passing data that does not need to be changed
✅ When passing functions to child components

When to Use state

Use state to manage data that changes inside a component.

Appropriate use cases

  • Data that changes based on user actions
  • Data that only affects the inside of the component
const Modal = () => {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Open Modal</button>
      {isOpen && (
        <div>
          <p>Modal is open</p>
          <button onClick={() => setIsOpen(false)}>Close</button>
        </div>
      )}
    </div>
  );
};

Key points

  • isOpen is a value that changes based on user actions → appropriate to manage with state
  • Use setIsOpen to update state and change the UI

Cases where you should use state
✅ Data that changes inside a component
✅ Data that changes based on user actions
✅ Temporary data (such as form input values)

When to Use context

Use context to manage data shared across multiple components.
It is used to avoid “prop drilling.”

Appropriate use cases

  • Themes or authentication information shared across the entire app
  • Data shared across multiple components
const ThemeContext = createContext("light");

const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

const ThemedComponent = () => {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <div>
      <p>Current theme: {theme}</p>
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        Switch theme
      </button>
    </div>
  );
};

// Use ThemeProvider for the entire app
const App = () => {
  return (
    <ThemeProvider>
      <ThemedComponent />
    </ThemeProvider>
  );
};

Key points

  • By using context, you can get and update the theme state without going through props
  • Placing ThemeProvider at the top level of App applies theme settings globally

Cases where you should use context
✅ Data to be managed globally (authentication info, theme, etc.)
✅ Data shared across multiple components
✅ When you want to avoid prop drilling

Criteria for Choosing State Management

State management method When to use Concrete use cases
props When passing data from parent to child Button labels, list data, event handlers
state For data that changes inside a component Form input values, modal open/close state
context For data to be managed globally Theme, authentication info, language settings

Abstraction to Improve Component Reusability

To improve component reusability, it is important to design components with appropriate abstraction so they can be reused in different situations. Below are detailed points to enhance reusability.

Design a Generic API

By making the component API (prop design) generic, you can use it in various scenarios.

✅ Concrete example: Button component
By giving it props like variant and size, you can reuse the same component with different styles.

type ButtonProps = {
  variant?: "primary" | "secondary" | "outline";
  size?: "small" | "medium" | "large";
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

const Button: React.FC<ButtonProps> = ({ variant = "primary", size = "medium", ...props }) => {
  const baseStyle = "px-4 py-2 font-semibold rounded";
  const variantStyles = {
    primary: "bg-blue-500 text-white",
    secondary: "bg-gray-500 text-white",
    outline: "border border-gray-500 text-gray-500",
  };
  const sizeStyles = {
    small: "text-sm px-2 py-1",
    medium: "text-base px-4 py-2",
    large: "text-lg px-6 py-3",
  };

  return (
    <button className={`${baseStyle} ${variantStyles[variant]} ${sizeStyles[size]}`} {...props} />
  );
};

Key points

  • You can get different designs just by specifying variant and size
  • By extending React.ButtonHTMLAttributes<HTMLButtonElement>, you can also pass standard button props (such as onClick)
  • Easy to reuse anywhere

Use the Pattern of Accepting Child Components

By allowing flexible insertion of elements inside a component, it becomes easier to reuse in various situations.

✅ Concrete example: Card component

type CardProps = {
  title: string;
  children: React.ReactNode;
};

const Card: React.FC<CardProps> = ({ title, children }) => {
  return (
    <div className="border p-4 shadow rounded-lg">
      <h2 className="text-xl font-bold">{title}</h2>
      <div className="mt-2">{children}</div>
    </div>
  );
};

Usage

<Card title="Profile">
  <p>Name: John Doe</p>
  <p>Age: 30</p>
</Card>

Key points

  • By using children, you can freely customize the content
  • Only title is a fixed part, making it manageable with a simple API
type CardProps = {
  header?: React.ReactNode;
  footer?: React.ReactNode;
  children: React.ReactNode;
};

const Card: React.FC<CardProps> = ({ header, footer, children }) => {
  return (
    <div className="border p-4 shadow rounded-lg">
      {header && <div className="border-b pb-2">{header}</div>}
      <div className="mt-2">{children}</div>
      {footer && <div className="border-t pt-2">{footer}</div>}
    </div>
  );
};

Key points

  • For more flexible usage, add slots like header and footer

Use Custom Hooks

By extracting component logic into custom hooks, you can reuse the same logic in other components.

✅ Concrete example: useFetch for fetching API data

import { useState, useEffect } from "react";

const useFetch = <T,>(url: string) => {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then((res) => {
        if (!res.ok) {
          throw new Error("Failed to fetch data");
        }
        return res.json();
      })
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);

  return { data, loading, error };
};

Usage

const UsersList = () => {
  const { data: users, loading, error } = useFetch<User[]>(
    "https://jsonplaceholder.typicode.com/users"
  );

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {users?.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};

Key points

  • By using generics in useFetch<T>(), you can use it with various data types
  • Manage loading and error state and keep component implementation simple
  • Easy to flexibly respond to API changes

Use the Compound Components Pattern

This is a way to design a more flexible API by grouping multiple components together.

✅ Concrete example: Tabs component

const Tabs = ({ children }: { children: React.ReactNode }) => {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <div>
      <div className="flex space-x-2 border-b">
        {React.Children.map(children, (child, index) =>
          React.isValidElement(child) ? (
            <button
              className={`px-4 py-2 ${index === activeIndex ? "border-b-2 border-blue-500" : ""}`}
              onClick={() => setActiveIndex(index)}
            >
              {child.props.label}
            </button>
          ) : null
        )}
      </div>
      <div className="p-4">{React.Children.toArray(children)[activeIndex]}</div>
    </div>
  );
};

const Tab = ({ children }: { children: React.ReactNode }) => <>{children}</>;

Usage

<Tabs>
  <Tab label="Home">Home content</Tab>
  <Tab label="Profile">Profile content</Tab>
  <Tab label="Settings">Settings</Tab>
</Tabs>

Key points

  • By nesting <Tab> inside <Tabs>, the API becomes intuitive
  • By giving label as a prop, you can explicitly specify tab titles
  • Easy to customize the appearance of components

Establishing Style Guidelines (Creating Guidelines to Maintain UI Consistency)

Style guidelines are important documents for maintaining design consistency across the project and preventing misunderstandings among developers. By focusing on the following points, you can enhance UI consistency and improve development efficiency.

Unified Theme

To maintain UI consistency, clearly define design elements such as color palette, fonts, and spacing.

  1. Color palette
    • Define primary, secondary, and accent colors
    • Consider color schemes for light mode and dark mode
    • Clarify the usage of colors (buttons, backgrounds, text)

Example: Custom theme in Tailwind

tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: "#1E40AF", // Blue-based main color
        secondary: "#9333EA", // Accent color
        background: "#F9FAFB", // Background color
        text: "#111827", // Text color
      },
    },
  },
};
  1. Fonts
  • Specify base font and heading font
  • Standardize font size, line height, and letter spacing
  • Consider readability (use Google Fonts or system fonts)

Example: Font settings in Tailwind

tailwind.config.js
module.exports = {
  theme: {
    extend: {
      fontFamily: {
        sans: ['Inter', 'Helvetica', 'Arial', 'sans-serif'],
        heading: ['Poppins', 'sans-serif'],
      },
    },
  },
};
  1. Spacing
  • Standardize spacing between components
  • Adopt an 8px grid system (4px increments are also possible)

Example: Spacing settings in Tailwind

tailwind.config.js
module.exports = {
  theme: {
    extend: {
      spacing: {
        '4': '16px',
        '8': '32px',
        '12': '48px',
      },
    },
  },
};

Integration with a Design System

To maintain design consistency and enable smooth collaboration between developers and designers, it is important to use a design system.

  • Figma
    • Create a component library so designers and developers can use common UI parts
    • Clarify styles (colors, fonts, icons, spacing)
  • Storybook
    • Document components so developers can correctly apply styles
    • Instantly confirm UI changes

Storybook usage image
Image from Gyazo

Unifying Style Management Methods

To perform consistent style management across the team, choose an appropriate method.

Use Tailwind CSS utility classes

  • Class-based styling avoids scope conflicts
  • Use @apply to share styles at the component level

Example: Unifying button components with Tailwind

styles/globals.css
@layer components {
  .btn-primary {
    @apply px-4 py-2 bg-primary text-white rounded-md hover:bg-opacity-80;
  }
}
components/Button.tsx
const Button = ({ children }: { children: React.ReactNode }) => {
  return <button className="btn-primary">{children}</button>;
};

Key points

  • Use @apply to integrate utility classes into CSS
  • Make the btn-primary class reusable in components
  • Apply unified styles across multiple components

Applying Theme Settings

By centrally managing component styles, you can build a scalable UI while ensuring design consistency.

  • Benefits of theme management
    • Design consistency
      • Applying consistent styles to all components improves UX.
    • Improved maintainability
      • You can update designs easily by changing styles in one place.
    • Easy application of dark mode and brand colors
      • By using theme settings, you can smoothly switch between different color schemes.

Example Implementation (Tailwind CSS + Theme Provider)

By using Tailwind CSS theme settings and next-themes, you can easily apply dark mode and custom themes.

  1. Setting up ThemeProvider
    Use next-themes to manage theme state and apply it across the page.
import { ThemeProvider } from 'next-themes';

export default function MyApp({ Component, pageProps }) {
  return (
    <ThemeProvider attribute="class" defaultTheme="light">
      <Component {...pageProps} />
    </ThemeProvider>
  );
}
  1. Tailwind CSS configuration
    Customize Tailwind theme settings so they can be applied per component.
// tailwind.config.js
module.exports = {
  darkMode: 'class', // Apply dark mode with class-based approach
  theme: {
    extend: {
      colors: {
        primary: '#1e3a8a', // Brand color
        secondary: '#9333ea',
      },
    },
  },
};
  1. Applying themes in components
    Use the useTheme hook to allow theme switching.
import { useTheme } from 'next-themes';

export default function ThemeSwitcher() {
  const { theme, setTheme } = useTheme();

  return (
    <button
      className="p-2 bg-primary text-white rounded"
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
    >
      {theme === 'dark' ? 'Light Mode' : 'Dark Mode'}
    </button>
  );
}

This allows you to easily manage components with extensible dark mode support.

Accessibility-Conscious Design

When designing components, it is important to consider accessibility (a11y).

Why Improve Accessibility

  • Makes it easier to use for all users (such as visually impaired users and keyboard-only users)
  • Positively affects SEO (search engine optimization)
  • Makes it easier to meet legal requirements (such as WCAG compliance)

Basic Accessibility Points

  • Use aria-* attributes appropriately
  • Consider keyboard operation
  • Ensure sufficient contrast ratio

Concrete Accessibility Design Points

  1. Proper use of aria-* attributes
    By properly setting aria-* attributes, users who use screen readers can more easily understand the content.

Main aria-* attributes

  • aria-label
    Give an appropriate name when buttons or links have no visual label.
<button aria-label="Close">×</button>
  • aria-labelledby
    Reference the id of another element and use it as a label.
<h2 id="section-title">User settings</h2>
<p aria-labelledby="section-title">You can change user settings in this section.</p>
  • aria-describedby
    Specify an element that provides supplementary information.
<input type="text" id="username" aria-describedby="username-desc" />
<p id="username-desc">Please enter username (alphanumeric characters only).</p>
  • Proper specification of the role attribute
    Assign appropriate roles such as role="dialog" to improve screen reader behavior.
<div role="dialog" aria-labelledby="modal-title">
  <h2 id="modal-title">Settings</h2>
  <p>You can change account settings here.</p>
</div>
  1. Consider keyboard operation
    Support keyboard operation so that users who cannot use a mouse can still operate the UI.
  • Focusable elements
    button, a, input, textarea, select, etc. are focusable by default.
    Specify tabIndex for custom elements.
<div tabIndex={0}>Focusable element</div>
  • Add keyboard operation with the keydown event
    For example, close a modal with the Escape key.
const handleKeyDown = (event: KeyboardEvent) => {
  if (event.key === "Escape") {
    closeModal();
  }
};

useEffect(() => {
  document.addEventListener("keydown", handleKeyDown);
  return () => document.removeEventListener("keydown", handleKeyDown);
}, []);
  1. Ensure contrast ratio
    Ensure sufficient contrast for users with color vision deficiencies or low vision.

Recommended contrast ratios

  • Normal text: 4.5:1 or higher
  • Large text (18px, bold or larger): 3:1 or higher

NG example: Low contrast

button {
  background-color: lightgray;
  color: white;
}

OK example: Improved contrast ratio

button {
  background-color: blue;
  color: white;
}

Contrast checking during development

  • Chrome DevTools
  • WebAIM Contrast Checker

https://webaim.org/resources/contrastchecker/

  1. Form accessibility
    Properly associate labels.
  • Associate label elements with input
<label htmlFor="email">Email Address</label>
<input type="email" id="email" />
  • Properly convey error messages
    Use aria-live to notify error messages in real time.
<p id="error-message" aria-live="polite">
  Email AddressPlease enter.
</p>

Role of aria-live

  • Used so that screen readers read out dynamically changing content (such as error messages or loading states) at appropriate times.
  • For example, applying it to form error messages or updated search results allows you to convey visual changes via audio.
  1. Modal accessibility
    When a modal opens, trap focus inside the modal to support proper operation.

Example: Implementing a focus trap with useEffect

import { useEffect, useRef } from "react";

const Modal = ({ isOpen, onClose }) => {
  const modalRef = useRef(null);

  useEffect(() => {
    if (!isOpen) return;

    const focusableElements = modalRef.current?.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );

    if (!focusableElements.length) return;

    const firstElement = focusableElements[0];
    const lastElement = focusableElements[focusableElements.length - 1];

    const handleTabKey = (event) => {
      if (event.key === "Tab") {
        if (event.shiftKey && document.activeElement === firstElement) {
          lastElement.focus();
          event.preventDefault();
        } else if (!event.shiftKey && document.activeElement === lastElement) {
          firstElement.focus();
          event.preventDefault();
        }
      }
    };

    document.addEventListener("keydown", handleTabKey);
    return () => document.removeEventListener("keydown", handleTabKey);
  }, [isOpen]);

  if (!isOpen) return null;

  return (
    <div
      ref={modalRef}
      role="dialog"
      aria-modal="true"
      style={{
        position: "fixed",
        top: "50%",
        left: "50%",
        transform: "translate(-50%, -50%)",
        background: "white",
        padding: "20px",
        boxShadow: "0px 4px 6px rgba(0,0,0,0.1)",
      }}
    >
      <h2>Modal Title</h2>
      <p>Modal content.</p>
      <input type="text" placeholder="Input field" />
      <button onClick={onClose}>Close</button>
    </div>
  );
};

export default Modal;
  • What is a focus trap?
    A focus trap is a mechanism that keeps focus movement with the Tab key confined inside the modal while it is open.

  • Why is a focus trap necessary?
    Normally, pressing the Tab key moves focus between focusable elements.
    However, when a modal is open, pressing Tab may move focus outside the modal.

  • Example

    • The modal opens
    • You press the Tab key
    • Focus moves outside the modal (for example, to a navigation link)
    • Even though the modal is open, focus jumps elsewhere, making it hard to operate
  • When you implement a focus trap

    • Even if you press the Tab key, you cannot move focus out of the focusable elements inside the modal.
    • This allows smooth handling of the modal using only the keyboard.

Component Version Control

By properly managing component versions, you can safely add features and fix bugs while maintaining project stability. Here we will explain concrete management methods and practical approaches.

Importance of Version Control

Proper component version control provides the following benefits:

  • Easier to understand the impact range of changes
  • Easier to unify understanding among team members
  • Easier maintenance of legacy code
  • Easier reuse of components across multiple projects

Version control is especially indispensable when developing and providing a component library.

Introducing Semantic Versioning (SemVer)

By introducing Semantic Versioning (SemVer) as the basis for version control, you can clearly classify types of changes.

SemVer rules

MAJOR.MINOR.PATCH
  • MAJOR (breaking changes): When you make changes that are not backward compatible with the existing API
  • MINOR (feature additions): When you add new features while maintaining compatibility with the existing API
  • PATCH (bug fixes): When you fix bugs without changing the behavior of the existing API
package.json
{
  "version": "2.1.4",
  "dependencies": {
    "@my-ui/button": "^2.1.0"
  }
}

In the above case

  • 2.1.4 → major version 2, minor version 1, patch version 4
  • ^2.1.0 for @my-ui/button → automatically updates up to 2.1.x

Managing Change History

By recording changes per version in CHANGELOG.md, it becomes easier for developers and users to understand what has changed.

Example of how to write CHANGELOG.md

# Changelog

## [2.1.0] - 2025-03-06
### Added
- Add new `Card` component
- `Button` to component `loading` Add property

## [2.0.0] - 2025-02-20
### Breaking Changes
- `Button` component `size` property changed to `small` / `medium` / `large`
- `Card` component `onClick` Change event arguments

It becomes easier to understand if you create categories such as Added and Breaking Changes.

Linking Versions and Release Tags

By using Git tags, you can clearly manage versions for each release.

How to add a tag

git tag v2.1.0
git push origin v2.1.0

By using tags, you can easily check out code corresponding to a specific version.

git checkout tags/v2.1.0

Version Management Workflow

Below is a concrete workflow to make version management operations smoother.

Example workflow

  • New feature addition / fix
    • Create a branch such as feature/new-button-loading
    • After the fix, merge into main
  • Version update
    • Update the version in package.json
    • Update CHANGELOG.md
    • Create a Git tag and push it to main
  • Release
    • Describe changes in GitHub Releases
    • Publish the library with npm publish (if needed)

Creating Component Documentation

It is important to enrich documentation so that consumers (development engineers) can correctly understand and efficiently use components.
Especially as the project grows, having unified documentation prevents misunderstandings among developers and improves reusability.

What to Include in Documentation

Component documentation generally includes the following:

Section Description
Introduction Briefly explains the purpose and use cases of the component
Usage Describes basic usage of the component
Props and default values Lists props, their types, and default values
Examples Shows various use cases
Styling Explains how to customize styles with Tailwind, Emotion, etc.
Accessibility Explains WCAG compliance and aria-* attributes
Changelog Describes changes and revision history

Creating Documentation with Storybook

By using Storybook, you can create documentation while visually checking component usage examples.
Because it can be operated interactively, it is easy for developers and designers to intuitively understand.

First, add Storybook to your project.

npx storybook init

Creating a story file
For example, to create a story for a Button component, prepare Button.stories.tsx as follows:

import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  argTypes: {
    onClick: { action: 'clicked' },
  },
};

export default meta;

type Story = StoryObj<typeof Button>;

export const Default: Story = {
  args: {
    label: 'Click me',
  },
};

export const Primary: Story = {
  args: {
    label: 'Click me',
    primary: true,
  },
};

Using the Docs Addon

By using Storybook’s @storybook/addon-docs, you can write documentation in Markdown or JSX.

npm install @storybook/addon-docs

Add the following to .storybook/main.js

.storybook/main.js
module.exports = {
  addons: ['@storybook/addon-docs'],
};

By adding JSDoc comments to components, they can be automatically reflected on the Docs page.

Button.tsx
/**
 * Basic button component
 * @param label - Button label
 * @param onClick - clickhandler
 */
export const Button = ({ label, onClick }: { label: string; onClick: () => void }) => {
  return <button onClick={onClick}>{label}</button>;
};

Conclusion

In this article, we explained best practices for component design using a tech stack centered on React. By designing based on a design system, you can maintain consistency in development and improve the productivity of the entire team.

Furthermore, by combining elements such as state management, reusability, style unification, accessibility support, and version control, you can build a scalable frontend architecture. In particular, by appropriately combining tools such as visualizing components with Storybook, design collaboration with Figma, and flexible style design with Emotion and Tailwind CSS, you can maximize development efficiency.

Component design is not something that ends once decided; it needs to evolve as the project grows. We hope you will use the content of this article as a reference and adopt it in a way that suits your team’s development style and product.

Xでシェア
Facebookでシェア
LinkedInでシェア

Questions about this article 📝

If you have any questions or feedback about the content, please feel free to contact us.
Go to inquiry form