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 (2)

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.