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
...
What's wrong with this?
- Flat structure β everything dumped in one folder
-
Redundant prefixes β
Button,Button,Button... - Poor encapsulation β hard to tell what belongs to what
- Scaling issues β at 50+ components it becomes a nightmare
-
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
π₯ 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'
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>
)
}
Vue Example: components/button/
index.ts β exports the component and types:
export { default as Button } from './index.vue'
export * from './types'
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>
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;
}
}
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
}
π³ 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.
components/sections/index.ts β exports sections:
export * from './contact'
export * from './hero'
export * from './faq'
export * from './footer'
export * from './navbar'
Same pattern for SCSS:
components/sections/_index.scss:
@import 'contact', 'hero', 'faq', 'footer', 'navbar';
This means in your main stylesheet, you only need:
@import 'components/sections';
π 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
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>
)
}
β 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'
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β typebutton/indexand 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
π¨ 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
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
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
Import
import { Button } from '@/components/button'
// or from barrel:
import { Button } from '@/components'
π 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
- Nucleify demo
- Nucleify on GitHub β check out the full implementation
- Atomic Design β a methodology that pairs perfectly with this pattern
- Barrel Exports Pattern β more on export aggregation
Got questions? Leave a comment below! π
If you like this convention, give a β on Nucleify's GitHub π
Top comments (2)
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.
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.