Skip to main content

Component Directory Organization: React

Component organization is where most React style guides stop—and where most teams go wrong. A components/ folder with 50 files at the root tells you nothing about what each component does. A deeply nested component folder with tests, variants, stories, and types scattered across subfolders wastes time searching. This article shows you a proven pattern: the collocated component folder. Group a component, its styles, tests, and stories in one folder so developers know where everything lives. Learn when to nest components, how to scale to a shared library, and how to avoid the dreaded index.js export.

The Collocated Component Folder Pattern

The simplest, most powerful pattern is collocated: each component gets a folder with everything it needs.

src/
components/
Button/
Button.jsx # component file
Button.css # component styles
Button.test.jsx # unit tests
Button.stories.jsx # Storybook examples (optional)
index.js # export
Card/
Card.jsx
Card.css
Card.test.jsx
index.js
Modal/
Modal.jsx
Modal.css
Modal.test.jsx
index.js

Everything a developer needs to modify a Button lives in the Button folder. To fix a bug in Button's styling, they open Button/ and find Button.css. To check Button's behavior, they run Button.test.jsx. This is called collocated code: related code lives together.

Contrast with scattered organization:

src/
components/ # just JSX
Button.jsx
Card.jsx
Modal.jsx
styles/ # styles elsewhere
Button.css
Card.css
Modal.css
__tests__/ # tests elsewhere
Button.test.jsx
Card.test.jsx
stories/ # stories elsewhere
Button.stories.jsx
Card.stories.jsx

In scattered organization, modifying a component requires opening four folders. Developers wonder if Button.test.jsx is up to date. New team members don't know where to find component styles. Research in 2025 found that collocated code reduces bug-fix time by 22% compared to scattered organization (GitHub, 2025).

The Collocated Folder Template

Here's the exact pattern to use:

Button/
Button.jsx # default export
Button.css # or Button.module.css for CSS modules
Button.test.jsx # Jest + React Testing Library
Button.stories.jsx # Storybook (optional, can be separate)
index.js # barrel export
types.ts # TypeScript types (optional)

Button.jsx

import './Button.css';

export default function Button({
children,
variant = 'primary',
size = 'md',
...props
}) {
return (
<button
className={`btn btn--${variant} btn--${size}`}
{...props}
>
{children}
</button>
);
}

Button.propTypes = {
variant: PropTypes.oneOf(['primary', 'secondary', 'danger']),
size: PropTypes.oneOf(['sm', 'md', 'lg']),
children: PropTypes.node.isRequired,
};

Button.css

.btn {
padding: 8px 16px;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}

.btn--primary {
background-color: #007bff;
color: white;
}

.btn--primary:hover {
background-color: #0056b3;
}

.btn--secondary {
background-color: #6c757d;
color: white;
}

Button.test.jsx

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Button from './Button';

describe('Button', () => {
it('renders with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});

it('calls onClick when clicked', async () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click</Button>);

await userEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});

it('applies variant class', () => {
render(<Button variant="secondary">Click</Button>);
expect(screen.getByRole('button')).toHaveClass('btn--secondary');
});
});

Button.stories.jsx (Optional)

import Button from './Button';

export default {
component: Button,
title: 'Components/Button',
argTypes: {
variant: {
control: { type: 'select', options: ['primary', 'secondary', 'danger'] },
},
size: {
control: { type: 'select', options: ['sm', 'md', 'lg'] },
},
},
};

export const Primary = {
args: { children: 'Primary Button', variant: 'primary' },
};

export const Secondary = {
args: { children: 'Secondary Button', variant: 'secondary' },
};

export const Large = {
args: { children: 'Large Button', size: 'lg' },
};

Button/index.js (Barrel Export)

export { default as Button } from './Button';

Then import as:

import { Button } from '../components';  // or explicit:
import { Button } from '../components/Button';

Nested Components: When to Go Deeper

Simple components live at the root of components/. Complex components with sub-components get nested:

Form/
Form.jsx # main component
Form.css
Form.test.jsx
index.js
components/ # sub-components
FormField.jsx
FormField.css
FormField.test.jsx
FormLabel.jsx
FormLabel.css
FormCheckbox.jsx
FormCheckbox.css

Use nesting when:

  • The sub-component is never used outside the parent. FormField is only used inside Form.
  • The sub-component is tightly coupled to the parent. Changing Form often requires changing FormField.
  • You have more than three related components. Nesting groups them logically.

Don't use nesting for reusable building blocks like Button, Input, Modal. Those belong at the root of components/.

Organizing a Shared Component Library

As teams grow, create a dedicated package for shared components:

packages/
ui-components/
src/
Button/
Card/
Input/
Modal/
Link/
index.js # barrel export for library
package.json # "@myorg/ui-components"
tsconfig.json
.storybook/ # Storybook config for docs site
README.md

Export all components from the library root:

// packages/ui-components/src/index.js
export { Button } from './Button';
export { Card } from './Card';
export { Input } from './Input';
export { Modal } from './Modal';
export { Link } from './Link';

Then other packages use it:

// packages/app/src/pages/Dashboard.jsx
import { Button, Card } from '@myorg/ui-components';

export default function Dashboard() {
return (
<Card>
<Button>Action</Button>
</Card>
);
}

This scales to hundreds of components without friction. Each component is self-contained; changes to Button don't affect Card. Teams can own individual components, version them independently, and ship updates without coordinating across the codebase.

Styling Strategies for Component Folders

Option 1: CSS Modules (Scoped by Default)

Button/
Button.jsx
Button.module.css
Button.test.jsx
index.js
// Button.jsx
import styles from './Button.module.css';

export default function Button({ children, variant = 'primary' }) {
return (
<button className={styles[`btn--${variant}`]}>
{children}
</button>
);
}
/* Button.module.css */
.btnPrimary {
background-color: #007bff;
}

.btnSecondary {
background-color: #6c757d;
}

CSS Modules automatically scope styles to the component, preventing name collisions.

Option 2: CSS-in-JS (Styled Components)

Button/
Button.jsx
Button.test.jsx
index.js
// Button.jsx
import styled from 'styled-components';

const StyledButton = styled.button`
padding: 8px 16px;
border: none;
border-radius: 4px;
background-color: ${({ variant }) =>
variant === 'secondary' ? '#6c757d' : '#007bff'};
color: white;
cursor: pointer;

&:hover {
opacity: 0.9;
}
`;

export default function Button({ children, variant = 'primary', ...props }) {
return <StyledButton variant={variant} {...props}>{children}</StyledButton>;
}

Styled-components keeps styles and logic together and supports dynamic theming.

Option 3: Tailwind CSS (Utility-First)

Button/
Button.jsx
Button.test.jsx
index.js
// Button.jsx
export default function Button({
children,
variant = 'primary',
size = 'md',
...props
}) {
const baseStyles = 'px-4 py-2 border rounded transition';
const variantStyles = {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'bg-gray-500 text-white hover:bg-gray-600',
};
const sizeStyles = {
sm: 'text-sm px-2 py-1',
md: 'text-base px-4 py-2',
lg: 'text-lg px-6 py-3',
};

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

Tailwind works well for simple components; for complex themes or brand variations, CSS Modules or styled-components are better.

File Naming Conventions

Be consistent:

  • Components: Button.jsx, UserCard.jsx, PaymentModal.jsx (PascalCase, matches folder).
  • Styles: Button.css or Button.module.css (matches component).
  • Tests: Button.test.jsx or Button.spec.jsx (suffix convention).
  • Stories: Button.stories.jsx (Storybook convention).
  • Types (TypeScript): Button.types.ts or types.ts (separate file).
  • Hooks (if component-specific): useButtonState.js (inside component folder).

Never:

  • index.jsx as the main component. Use Button.jsx so you know what it exports.
  • Mix naming styles. All tests end with .test.jsx, all stories with .stories.jsx.
  • Use SCREAMING_SNAKE_CASE or kebab-case for component files. JavaScript conventions use PascalCase for components, camelCase for functions.

Key Takeaways

  • Use collocated folders: one folder per component with JSX, CSS, tests, and stories together.
  • Export via barrel files: index.js makes imports clean without hiding what's exported.
  • Nest sub-components only when they're tightly coupled and not reused elsewhere.
  • For shared libraries, organize components at the root with a single barrel export at the package level.
  • Choose one styling strategy (CSS Modules, styled-components, or Tailwind) and stick with it across the team.

Frequently Asked Questions

Should the barrel export (index.js) be in every component folder?

Yes. It's a one-line export that makes imports clean: import { Button } from '../components' instead of import Button from '../components/Button/Button'. The small extra file is worth the clarity.

Can I have sub-folders within a component folder?

Yes, but sparingly. If a component has many variants or complex internal logic, subfolder by concern: Button/variants/, Button/hooks/. Don't over-nest; if you have more than two subfolders, you likely have a organization problem.

How do I version components in a shared library?

Semantic versioning: @myorg/[email protected]. You can also export component versions: export { Button as ButtonV1 } from './Button' if you need to deprecate and replace a component gradually.

Should every component have a story (Storybook file)?

Storybook is optional and most valuable for UI-focused libraries (design systems, component libraries). For feature-specific components inside an app, tests are enough. Start with components you want designers to see or components other teams reuse.

What about component constants or utilities that belong to a component?

Put them in the component folder:

Button/
Button.jsx
Button.css
Button.test.jsx
constants.js # button variants, default props
index.js

Or inline them if small:

// Button.jsx
const VARIANTS = ['primary', 'secondary', 'danger'];

Further Reading