DEV Community

Nucleify
Nucleify

Posted on

Why You Should Use index Files: The Folder-as-Component Pattern

Have you ever browsed through a React or Vue project and stumbled upon dozens of files like Button.tsx, Button.vue, ButtonStyles.scss, ButtonTypes.ts, useButton.ts? And then realized half of them belong to a completely different module?

In this article, I'll show you an alternative approach – Folder-as-Component – which I use in Nucleify, a modular full-stack framework. This pattern works equally well in React, Vue, Nuxt, Next.js, or any component-based architecture.


🎯 The Problem: Naming Chaos

A typical frontend project often looks like this:

components/
β”œβ”€β”€ Button.tsx
β”œβ”€β”€ Button.vue
β”œβ”€β”€ ButtonProps.ts
β”œβ”€β”€ ButtonStyles.scss
β”œβ”€β”€ Card.tsx
β”œβ”€β”€ CardProps.ts
β”œβ”€β”€ CardStyles.scss
β”œβ”€β”€ Navbar.tsx
β”œβ”€β”€ NavbarLinks.tsx
β”œβ”€β”€ NavbarDrawer.tsx
β”œβ”€β”€ NavbarStyles.scss
...
Enter fullscreen mode Exit fullscreen mode

What's wrong with this?

  1. Flat structure – everything dumped in one folder
  2. Redundant prefixes – Button, Button, Button...
  3. Poor encapsulation – hard to tell what belongs to what
  4. Scaling issues – at 50+ components it becomes a nightmare
  5. IDE tab hell – 10 open tabs all starting with Button*

πŸ’‘ The Solution: Folder-as-Component

Instead of naming files after components, I name folders after components, while files inside always have the same names:

components/
β”œβ”€β”€ button/
β”‚   β”œβ”€β”€ index.ts          # exports component + types
β”‚   β”œβ”€β”€ index.tsx         # React component
β”‚   β”œβ”€β”€ index.vue         # or Vue component
β”‚   β”œβ”€β”€ _index.scss       # styles (underscore = SCSS partial)
β”‚   └── types/
β”‚       β”œβ”€β”€ index.ts      # barrel export
β”‚       └── interfaces.ts
β”œβ”€β”€ card/
β”‚   β”œβ”€β”€ index.ts
β”‚   β”œβ”€β”€ index.tsx
β”‚   └── _index.scss
└── navbar/
    β”œβ”€β”€ index.ts
    β”œβ”€β”€ index.tsx
    β”œβ”€β”€ _index.scss
    └── components/       # nested components
        β”œβ”€β”€ index.ts
        β”œβ”€β”€ _index.scss
        β”œβ”€β”€ Drawer/
        β”‚   β”œβ”€β”€ index.ts
        β”‚   β”œβ”€β”€ index.tsx
        β”‚   └── _index.scss
        └── Links/
            β”œβ”€β”€ index.ts
            β”œβ”€β”€ index.tsx
            └── _index.scss
Enter fullscreen mode Exit fullscreen mode

πŸ”₯ Real-World Examples

Let's see how a button component is structured in both React and Vue:

React Example: components/button/

index.ts – exports the component and types:

export { Button } from './index.tsx'
export * from './types'
Enter fullscreen mode Exit fullscreen mode

index.tsx – the React component:

import type { ButtonProps } from '.'
import clsx from 'clsx'
import './index.scss'

export const Button = ({ 
  variant, 
  size, 
  icon, 
  label, 
  children,
  className,
  ...props 
}: ButtonProps) => {
  return (
    <button
      className={clsx(
        'button',
        variant && `${variant}-button`,
        size && `${size}-button`,
        className
      )}
      {...props}
    >
      {icon && <Icon name={icon} />}
      {label && <span>{label}</span>}
      {children}
    </button>
  )
}
Enter fullscreen mode Exit fullscreen mode

Vue Example: components/button/

index.ts – exports the component and types:

export { default as Button } from './index.vue'
export * from './types'
Enter fullscreen mode Exit fullscreen mode

index.vue – the Vue component:

<template>
  <button
    :class="[
      'button',
      variant && `${variant}-button`,
      size && `${size}-button`
    ]"
  >
    <Icon v-if="icon" :name="icon" />
    <span v-if="label">{{ label }}</span>
    <slot />
  </button>
</template>

<script setup lang="ts">
import type { ButtonProps } from '.'  // Import from the same folder!

const props = defineProps<ButtonProps>()
</script>

<style lang="scss">
@import 'index';  // Loads _index.scss
</style>
Enter fullscreen mode Exit fullscreen mode

Shared: _index.scss

.button {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  transition: all 0.3s ease;

  &.primary-button {
    background: var(--primary);
    color: white;
  }

  &.secondary-button {
    background: transparent;
    border: 1px solid var(--primary);
  }

  &.sm-button {
    padding: 0.25rem 0.5rem;
  }
  &.md-button {
    padding: 0.5rem 1rem;
  }
  &.lg-button {
    padding: 0.75rem 1.5rem;
  }
}
Enter fullscreen mode Exit fullscreen mode

Shared: types/interfaces.ts

import type { ButtonHTMLAttributes } from 'react' // or from vue

export type ButtonVariant = 'primary' | 'secondary' | 'ghost'
export type ButtonSize = 'sm' | 'md' | 'lg'

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: ButtonVariant
  size?: ButtonSize
  label?: string
  icon?: string
}
Enter fullscreen mode Exit fullscreen mode

🌳 Barrel Exports – Module Aggregation

The magic happens in index.ts files at higher levels:

components/atoms/index.ts – exports all atoms:

export * from './avatar'
export * from './badge'
export * from './button'
export * from './checkbox'
export * from './heading'
export * from './icon'
export * from './image'
// ... etc.
Enter fullscreen mode Exit fullscreen mode

components/sections/index.ts – exports sections:

export * from './contact'
export * from './hero'
export * from './faq'
export * from './footer'
export * from './navbar'
Enter fullscreen mode Exit fullscreen mode

Same pattern for SCSS:

components/sections/_index.scss:

@import 'contact', 'hero', 'faq', 'footer', 'navbar';
Enter fullscreen mode Exit fullscreen mode

This means in your main stylesheet, you only need:

@import 'components/sections';
Enter fullscreen mode Exit fullscreen mode

πŸ“ Nested Components

A Navbar typically has its own subcomponents. Here's the structure:

navbar/
β”œβ”€β”€ index.ts
β”œβ”€β”€ index.tsx          # or index.vue
β”œβ”€β”€ _index.scss
└── components/
    β”œβ”€β”€ index.ts          # export * from './Drawer'; export * from './Links'
    β”œβ”€β”€ _index.scss       # @import 'Drawer', 'Links';
    β”œβ”€β”€ Drawer/
    β”‚   β”œβ”€β”€ index.ts
    β”‚   β”œβ”€β”€ index.tsx
    β”‚   └── _index.scss
    └── Links/
        β”œβ”€β”€ index.ts
        β”œβ”€β”€ index.tsx
        β”œβ”€β”€ _index.scss
        └── links.ts      # link data
Enter fullscreen mode Exit fullscreen mode

navbar/index.tsx – clean and readable imports:

import { NavbarDrawer, NavbarLinks } from './components'
import './index.scss'

export const Navbar = () => {
  return (
    <nav className="navbar">
      <Logo />
      <NavbarLinks />
      <NavbarDrawer />
    </nav>
  )
}
Enter fullscreen mode Exit fullscreen mode

βœ… Benefits of the Folder-as-Component Pattern

1. Intuitive Imports

// Instead of:
import Button from '@/components/Button.tsx'
import { ButtonProps } from '@/components/ButtonTypes'

// You have:
import { Button, ButtonProps } from '@/components/button'
// or from barrel export:
import { Button } from '@/components'
Enter fullscreen mode Exit fullscreen mode

2. Encapsulation

Everything related to a component is in one place. Want to delete Button? Just delete the button/ folder.

3. Scalability

Even with 100+ components, you maintain order – each component is its own "micro-package".

4. IDE-Friendly

  • Tabs show the folder name, not the file name
  • Quick Ctrl+P / Cmd+P β†’ type button/index and you know exactly what it is
  • Fewer naming conflicts

5. Consistency

Every developer knows where to look:

  • Component β†’ index.tsx / index.vue
  • Exports β†’ index.ts
  • Styles β†’ _index.scss
  • Types β†’ types/interfaces.ts

6. Easy Refactoring

Move the entire folder without changing internal imports.

7. Tree-shaking Friendly

Bundlers can easily analyze what's being used.

8. Framework Agnostic

The same structure works in React, Vue, Svelte, Angular – only the component file extension changes.


βš™οΈ File Naming Conventions

index.ts vs index.tsx vs index.vue

File Purpose
index.ts Barrel exports (re-exports component + types)
index.tsx React/Preact component with JSX
index.vue Vue Single File Component
index.svelte Svelte component

_index.scss Convention

The underscore (_) in SCSS denotes a partial – a file that doesn't compile on its own but is imported by other files.

// In a component:
@import 'index';  // Loads _index.scss from the same folder

// In an aggregator:
@import 'button', 'card', 'navbar';  // Loads _index.scss from each folder
Enter fullscreen mode Exit fullscreen mode

🎨 What Does It Look Like in Practice?

In a real project, the entire architecture is built on this pattern:

src/
β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ atoms/
β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   β”œβ”€β”€ _index.scss
β”‚   β”‚   β”œβ”€β”€ button/
β”‚   β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ index.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ _index.scss
β”‚   β”‚   β”‚   └── types/
β”‚   β”‚   β”‚       β”œβ”€β”€ index.ts
β”‚   β”‚   β”‚       └── interfaces.ts
β”‚   β”‚   β”œβ”€β”€ icon/
β”‚   β”‚   └── input/
β”‚   β”œβ”€β”€ molecules/
β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   β”œβ”€β”€ search-bar/
β”‚   β”‚   └── form-field/
β”‚   └── organisms/
β”‚       β”œβ”€β”€ index.ts
β”‚       β”œβ”€β”€ navbar/
β”‚       β”œβ”€β”€ footer/
β”‚       └── sidebar/
β”œβ”€β”€ hooks/
β”‚   β”œβ”€β”€ index.ts
β”‚   β”œβ”€β”€ use-auth.ts
β”‚   └── use-theme.ts
└── utils/
    β”œβ”€β”€ index.ts
    β”œβ”€β”€ format-date.ts
    └── cn.ts
Enter fullscreen mode Exit fullscreen mode

Every component follows the same structure. New developers immediately know where to find things.


πŸš€ Quick Start

Want to implement this in your project? Here's a minimal structure:

React

src/
└── components/
    └── button/
        β”œβ”€β”€ index.ts        # export { Button } from './index.tsx'; export * from './types'
        β”œβ”€β”€ index.tsx       # export const Button = () => <button>...</button>
        β”œβ”€β”€ _index.scss
        └── types/
            β”œβ”€β”€ index.ts
            └── interfaces.ts
Enter fullscreen mode Exit fullscreen mode

Vue

src/
└── components/
    └── button/
        β”œβ”€β”€ index.ts        # export { default as Button } from './index.vue'; export * from './types'
        β”œβ”€β”€ index.vue       # <template>...</template>
        β”œβ”€β”€ _index.scss
        └── types/
            β”œβ”€β”€ index.ts
            └── interfaces.ts
Enter fullscreen mode Exit fullscreen mode

Import

import { Button } from '@/components/button'
// or from barrel:
import { Button } from '@/components'
Enter fullscreen mode Exit fullscreen mode

πŸ“š Summary

Aspect Traditional Folder-as-Component
Structure Flat Hierarchical
File naming ComponentName.* index.*
Encapsulation Weak Strong
Scalability ❌ βœ…
Imports Long paths Short, via barrel
Refactoring Difficult Easy
Framework Specific Agnostic

πŸ”— Links


Got questions? Leave a comment below! πŸ‘‡

If you like this convention, give a ⭐ on Nucleify's GitHub πŸš€

Top comments (3)

Collapse
Β 
brooks-rockett profile image
Brooks Rockett β€’

This seems like a cool approach, and I recognize the benefits especially for refactoring and scaling.

I just have this bad habit of always wanting to "CTRL+P" to find a file by name and it's bad enough with a number of index.ts files in different subfolders of /pages - this seems like it would turn that up a few notches.

Collapse
Β 
nucleify profile image
Nucleify β€’

It's not that big problem in my opinion, when you write "button" or other component it shows you files based on folder names. So it's really just matter of habits.

Collapse
Β 
brooks-rockett profile image
Brooks Rockett β€’

Makes sense! Thanks for sharing!