Skip to content

Component Library

The ui-component-library provides shared React components following the Atomic Design pattern.

Overview

The component library is organized hierarchically:

graph TD
    A[Atoms<br/>Button, Input, Icon] --> B[Molecules<br/>SearchBar, FormField]
    B --> C[Organisms<br/>Header, Card, Form]
    C --> D[Templates<br/>PageLayout]
    D --> E[Pages<br/>App-specific]

    style A fill:#e1f5ff
    style B fill:#b3e5fc
    style C fill:#81d4fa
    style D fill:#4fc3f7
    style E fill:#29b6f6

Installation

Components are already available in the monorepo:

import { Button, Input } from '@ui-component-library';

Available Components

Atoms

Basic building blocks that can't be broken down further.

Button

import { Button } from '@ui-component-library';

export function Example() {
  return (
    <>
      <Button variant="contained">Primary</Button>
      <Button variant="outlined">Secondary</Button>
      <Button variant="text">Text</Button>
    </>
  );
}

Props:

  • variant: 'contained' | 'outlined' | 'text'
  • color: 'primary' | 'secondary'
  • size: 'small' | 'medium' | 'large'
  • disabled: boolean
  • onClick: () => void

Input

import { Input } from '@ui-component-library';

export function Example() {
  return (
    <Input
      label="Email"
      type="email"
      placeholder="user@example.com"
      required
    />
  );
}

Molecules

Simple combinations of atoms.

import { SearchBar } from '@ui-component-library';

export function Example() {
  const handleSearch = (query: string) => {
    console.log('Searching for:', query);
  };

  return <SearchBar onSearch={handleSearch} placeholder="Search..." />;
}

Organisms

Complex components composed of molecules and atoms.

!!! info "Coming Soon" Organism components are being developed. Check Storybook for the latest additions.

Viewing in Storybook

All components are documented in Storybook:

npx nx storybook ui-component-library

Visit http://localhost:4400 to:

  • Browse all components
  • View component props
  • Test component variants
  • Check accessibility
  • Copy code examples

Creating New Components

1. Create Component Directory

mkdir -p libs/ui-component-library/src/lib/atoms/Avatar

2. Create Component Files

Avatar.tsx
import { FC } from 'react';

export interface AvatarProps {
  src?: string;
  alt: string;
  size?: 'small' | 'medium' | 'large';
}

export const Avatar: FC<AvatarProps> = ({ src, alt, size = 'medium' }) => {
  return (
    <img
      src={src || '/default-avatar.png'}
      alt={alt}
      className={`avatar avatar-${size}`}
    />
  );
};

3. Create Test

Avatar.spec.tsx
import { render, screen } from '@testing-library/react';
import { Avatar } from './Avatar';

describe('Avatar', () => {
  it('renders with alt text', () => {
    render(<Avatar alt="User avatar" />);
    expect(screen.getByAltText('User avatar')).toBeInTheDocument();
  });

  it('uses provided src', () => {
    render(<Avatar src="/user.jpg" alt="User" />);
    const img = screen.getByAltText('User') as HTMLImageElement;
    expect(img.src).toContain('/user.jpg');
  });

  it('applies size className', () => {
    render(<Avatar alt="User" size="large" />);
    const img = screen.getByAltText('User');
    expect(img).toHaveClass('avatar-large');
  });
});

4. Create Story

Avatar.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Avatar } from './Avatar';

const meta: Meta<typeof Avatar> = {
    title: 'Atoms/Avatar',
    component: Avatar,
    parameters: {
        layout: 'centered',
    },
    tags: ['autodocs'],
    argTypes: {
        size: {
            control: 'select',
            options: ['small', 'medium', 'large'],
        },
    },
};

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
    args: {
        alt: 'User avatar',
        size: 'medium',
    },
};

export const Small: Story = {
    args: {
        alt: 'User avatar',
        size: 'small',
    },
};

export const Large: Story = {
    args: {
        alt: 'User avatar',
        size: 'large',
    },
};

export const WithImage: Story = {
    args: {
        src: 'https://via.placeholder.com/150',
        alt: 'User avatar',
        size: 'medium',
    },
};

5. Export Component

Add to libs/ui-component-library/src/index.ts:

export * from './lib/atoms/Avatar/Avatar';

6. Test and Document

# Run tests
npx nx test ui-component-library

# View in Storybook
npx nx storybook ui-component-library

# Lint
npx nx lint ui-component-library

Component Guidelines

Naming Conventions

  • PascalCase for component names
  • camelCase for props
  • Descriptive names (e.g., UserProfileCard, not UPC)

Props Interface

Always export props interface:

export interface ButtonProps {
    variant?: 'contained' | 'outlined' | 'text';
    children: React.ReactNode;
    onClick?: () => void;
}

export const Button: FC<ButtonProps> = ({
    variant = 'contained',
    children,
    onClick,
}) => {
    // ...
};

Default Props

Use default parameters:

// ✅ Good
export const Button: FC<ButtonProps> = ({
    variant = 'contained',
    size = 'medium',
}) => {
    // ...
};

// ❌ Avoid (React 18.3+ deprecated defaultProps)
Button.defaultProps = {
    variant: 'contained',
    size: 'medium',
};

Accessibility

Ensure components are accessible:

export const Button: FC<ButtonProps> = ({ children, disabled, ...props }) => {
  return (
    <button
      {...props}
      disabled={disabled}
      aria-disabled={disabled}
      role="button"
    >
      {children}
    </button>
  );
};

TypeScript Strict Mode

All components must work with strict TypeScript:

// ✅ Good - explicit types
export const Avatar: FC<AvatarProps> = ({ src, alt }) => {
  return <img src={src} alt={alt} />;
};

// ❌ Bad - implicit any
export const Avatar = ({ src, alt }: any) => {
  return <img src={src} alt={alt} />;
};

Styled Components

Use Emotion for styling:

StyledButton.tsx
import styled from '@emotion/styled';
import { ButtonProps } from './Button';

export const StyledButton = styled.button<ButtonProps>`
    padding: ${({ size }) => {
        switch (size) {
            case 'small':
                return '4px 8px';
            case 'large':
                return '12px 24px';
            default:
                return '8px 16px';
        }
    }};

    background-color: ${({ variant, theme }) =>
        variant === 'contained' ? theme.palette.primary.main : 'transparent'};

    &:hover {
        opacity: 0.8;
    }
`;

Theme Integration

Components should use the theme:

import { useTheme } from '@util-global-theme';

export const ThemedButton: FC<ButtonProps> = ({ children }) => {
  const theme = useTheme();

  return (
    <button style={{ color: theme.palette.primary.main }}>
      {children}
    </button>
  );
};

Testing Components

See Testing Guide for comprehensive testing strategies.

Next Steps