Introduction
Clear writing requires clear thinking. The same is valid for coding. Throwing all components into one folder may work when starting a personal project. But as projects grow, especially with larger teams, this approach leads to problems:
- Duplicated code
- Oversized, multipurpose components
- Difficult-to-test code
Atomic Design offers a solution. Letโs examine how to apply it to a Nuxt project.
What is Atomic Design
Brad Frost developed Atomic Design as a methodology for creating design systems. It is structured into five levels inspired by chemistry:
- Atoms: Basic building blocks (e.g. form labels, inputs, buttons)
- Molecules: Simple groups of UI elements (e.g. search forms)
- Organisms: Complex components made of molecules/atoms (e.g. headers)
- Templates: Page-level layouts
- Pages: Specific instances of templates with content
Tip: ๐ For a better exploration of Atomic Design principles, I recommend reading Brad Frost's blog post: Atomic Web Design
For Nuxt, we can adapt these definitions:
- Atoms: Pure, single-purpose components
- Molecules: Combinations of atoms with minimal logic
- Organisms: Larger, self-contained, reusable components
- Templates: Nuxt layouts defining page structure
- Pages: Components handling data and API calls
Organisms vs Molecules: What's the Difference?
Molecules and organisms can be confusing. Here's a simple way to think about them:
-
Molecules are small and simple. They're like LEGO bricks that snap together.
Examples:- A search bar (input + button)
- A login form (username input + password input + submit button)
- A star rating (5 star icons + rating number)
-
Organisms are bigger and more complex. They're like pre-built LEGO sets.
Examples:- A full website header (logo + navigation menu + search bar)
- A product card (image + title + price + add to cart button)
- A comment section (comment form + list of comments)
Remember: Molecules are parts of organisms, but organisms can work independently.
Code Example: Before and After
Consider this non-Atomic Design todo app component:
<template>
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-4 text-gray-800 dark:text-gray-200">Todo App</h1>
<!-- Add Todo Form -->
<form @submit.prevent="addTodo" class="mb-4">
<input
v-model="newTodo"
type="text"
placeholder="Enter a new todo"
class="border p-2 mr-2 bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded"
/>
<button type="submit" class="bg-blue-500 hover:bg-blue-600 text-white p-2 rounded transition duration-300">
Add Todo
</button>
</form>
<!-- Todo List -->
<ul class="space-y-2">
<li
v-for="todo in todos"
:key="todo.id"
class="flex justify-between items-center p-3 bg-gray-100 dark:bg-gray-700 rounded shadow-sm"
>
<span class="text-gray-800 dark:text-gray-200">{{ todo.text }}</span>
<button
@click="deleteTodo(todo.id)"
class="bg-red-500 hover:bg-red-600 text-white p-1 rounded transition duration-300"
>
Delete
</button>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
interface Todo {
id: number
text: string
}
const newTodo = ref('')
const todos = ref<Todo[]>([])
const fetchTodos = async () => {
// Simulating API call
todos.value = [
{ id: 1, text: 'Learn Vue.js' },
{ id: 2, text: 'Build a Todo App' },
{ id: 3, text: 'Study Atomic Design' }
]
}
const addTodo = async () => {
if (newTodo.value.trim()) {
// Simulating API call
const newTodoItem: Todo = {
id: Date.now(),
text: newTodo.value
}
todos.value.push(newTodoItem)
newTodo.value = ''
}
}
const deleteTodo = async (id: number) => {
// Simulating API call
todos.value = todos.value.filter(todo => todo.id !== id)
}
onMounted(fetchTodos)
</script>
This approach leads to large, difficult-to-maintain components. Letโs refactor using Atomic Design:
This will be the refactored structure
๐ Template (Layout)
โ
โโโโ ๐ Page (TodoApp)
โ
โโโโ ๐ฆ Organism (TodoList)
โ
โโโโ ๐งช Molecule (TodoForm)
โ โ
โ โโโโ โ๏ธ Atom (BaseInput)
โ โโโโ โ๏ธ Atom (BaseButton)
โ
โโโโ ๐งช Molecule (TodoItems)
โ
โโโโ ๐งช Molecule (TodoItem) [multiple instances]
โ
โโโโ โ๏ธ Atom (BaseText)
โโโโ โ๏ธ Atom (BaseButton)
Refactored Components
Tempalte Default
<template>
<div class="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors duration-300">
<header class="bg-white dark:bg-gray-800 shadow">
<nav class="container mx-auto px-4 py-4 flex justify-between items-center">
<NuxtLink to="/" class="text-xl font-bold">Todo App</NuxtLink>
<ThemeToggle />
</nav>
</header>
<main class="container mx-auto px-4 py-8">
<slot />
</main>
</div>
</template>
<script setup lang="ts">
import ThemeToggle from '~/components/ThemeToggle.vue'
</script>
Pages
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import TodoList from '../components/organisms/TodoList'
interface Todo {
id: number
text: string
}
const todos = ref<Todo[]>([])
const fetchTodos = async () => {
// Simulating API call
todos.value = [
{ id: 1, text: 'Learn Vue.js' },
{ id: 2, text: 'Build a Todo App' },
{ id: 3, text: 'Study Atomic Design' }
]
}
const addTodo = async (text: string) => {
// Simulating API call
const newTodoItem: Todo = {
id: Date.now(),
text
}
todos.value.push(newTodoItem)
}
const deleteTodo = async (id: number) => {
// Simulating API call
todos.value = todos.value.filter(todo => todo.id !== id)
}
onMounted(fetchTodos)
</script>
<template>
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-4 text-gray-800 dark:text-gray-200">Todo App</h1>
<TodoList
:todos="todos"
@add-todo="addTodo"
@delete-todo="deleteTodo"
/>
</div>
</template>
Organism (TodoList)
<script setup lang="ts">
import TodoForm from '../molecules/TodoForm.vue'
import TodoItem from '../molecules/TodoItem.vue'
interface Todo {
id: number
text: string
}
defineProps<{
todos: Todo[]
}>()
defineEmits<{
(e: 'add-todo', value: string): void
(e: 'delete-todo', id: number): void
}>()
</script>
<template>
<div>
<TodoForm @add-todo="$emit('add-todo', $event)" />
<ul class="space-y-2">
<TodoItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
@delete-todo="$emit('delete-todo', $event)"
/>
</ul>
</div>
</template>
Molecules (TodoForm and TodoItem)
TodoForm.vue:
<script setup lang="ts">
import TodoForm from '../molecules/TodoForm.vue'
import TodoItem from '../molecules/TodoItem.vue'
interface Todo {
id: number
text: string
}
defineProps<{
todos: Todo[]
}>()
defineEmits<{
(e: 'add-todo', value: string): void
(e: 'delete-todo', id: number): void
}>()
</script>
<template>
<div>
<TodoForm @add-todo="$emit('add-todo', $event)" />
<ul class="space-y-2">
<TodoItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
@delete-todo="$emit('delete-todo', $event)"
/>
</ul>
</div>
</template>
TodoItem.vue:
<script setup lang="ts">
import { ref } from 'vue'
import BaseInput from '../atoms/BaseInput.vue'
import BaseButton from '../atoms/BaseButton.vue'
const newTodo = ref('')
const emit = defineEmits<{
(e: 'add-todo', value: string): void
}>()
const addTodo = () => {
if (newTodo.value.trim()) {
emit('add-todo', newTodo.value)
newTodo.value = ''
}
}
</script>
<template>
<form @submit.prevent="addTodo" class="mb-4">
<BaseInput v-model="newTodo" placeholder="Enter a new todo" />
<BaseButton type="submit">Add Todo</BaseButton>
</form>
</template>
Atoms (BaseButton, BaseInput, BaseText)
BaseButton.vue:
<script setup lang="ts">
defineProps<{
variant?: 'primary' | 'danger'
}>()
</script>
<template>
<button
:class="[
'p-2 rounded transition duration-300',
variant === 'danger'
? 'bg-red-500 hover:bg-red-600 text-white'
: 'bg-blue-500 hover:bg-blue-600 text-white'
]"
>
<slot></slot>
</button>
</template>
BaseInput.vue:
<script setup lang="ts">
defineProps<{
modelValue: string
placeholder?: string
}>()
defineEmits<{
(e: 'update:modelValue', value: string): void
}>()
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
type="text"
:placeholder="placeholder"
class="border p-2 mr-2 bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded"
/>
</template>
Info: Want to check out the full example yourself? click me
Component Level | Job | Examples |
---|---|---|
Atoms | Pure, single-purpose components | BaseButton BaseInput BaseIcon BaseText |
Molecules | Combinations of atoms with minimal logic | SearchBar LoginForm StarRating Tooltip |
Organisms | Larger, self-contained, reusable components. Can perform side effects and complex operations. | TheHeader ProductCard CommentSection NavigationMenu |
Templates | Nuxt layouts defining page structure | DefaultLayout BlogLayout DashboardLayout AuthLayout |
Pages | Components handling data and API calls | HomePage UserProfile ProductList CheckoutPage |
Summary
Atomic Design offers one path to a more apparent code structure. It works well as a starting point for many projects. But as complexity grows, other architectures may serve you better. Want to explore more options? Read my post on How to structure vue Projects. It covers approaches beyond Atomic Design when your project outgrows its initial structure.
Top comments (0)