Often, when you start the development of an application, you can get in trouble trying to understand how to organize your components.
In this post I'm going to explain to you how to do it (or how you could do it). It's the approach we adopted in MotorK for both the Design System and Single Page Applications. It's called: Atomic Design.
Table of Contents
Atomic Design
Break entire interfaces down into fundamental building blocks and work up from there. That’s the basic gist of atomic design.
The Atomic Design principle is to split your UI parts into "small" and reusable components in order to have a better reusability.
Like chemistry, you can organize your components in atoms, molecules and organisms.
In addition there are also templates and pages, but I won't talk about them because I want to keep the focus on small applications' architectures.
So, let's start talking about atoms, molecules and organisms.
Atoms
Atoms are the smallest components of your application. Basically, they can be texts, buttons, form inputs and so on.
The golden rule is: if you can't split a component into smaller components then it must be an atom.
Molecules
Molecules are combinations of atoms bonded together. For example, if you have Text
and Input
atoms, you can combine them into a InputField
(or whatever name you want) molecule .
Organisms
Organisms are combinations of molecules: if you mix two or more molecules you get an organism.
A real example
Let's try to craft an application using the Atomic Design. The final goal is to create two different forms:
- ContactForm
- SignupForm
N.B. I'm going to use Vue, but you can use whatever language/framework you prefer.
Folder structure
First of all we have to create our folder structure in which store our components.
So, let's create a src
directory which will contain all the JS files and, inside it, a components
folder. After that, we need to create atoms
, molecules
and organisms
folders inside components
.
The result should be something like this:
App.vue
it's our entry point.
Since we have our folder structure, we can proceed creating our components starting from atoms.
We'll set the Ad namespace in each component.
Our atoms components
Let's create the following atoms components:
- Button
- Input
- Text
- Textarea
Button
<template>
<button class="a-button" :type="type">{{label}}</button>
</template>
<script>
const _buttonTypes = ["submit", "button"];
export default {
name: "AdButton",
props: {
type: {
type: String,
required: true,
default: "button",
validator: (value) => _buttonTypes.includes(value),
},
label: {
type: String,
required: true,
},
},
};
</script>
Input
<template>
<input
v-model="value"
:type="type"
:id="id"
:name="name"
:placeholder="placeholder"
class="a-input"
/>
</template>
<script>
const _inputTypes = ["text", "email", "password", "checkbox"];
export default {
name: "AdInput",
data() {
return {
value: "",
};
},
props: {
id: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
placeholder: {
type: String,
required: false,
default: null,
},
type: {
type: String,
required: true,
default: "text",
validator: (value) => _inputTypes.includes(value),
},
},
};
</script>
Text
<template>
<component :is="tag" :for="getForProp" class="a-text">
{{content}}
</component>
</template>
<script>
const _tagTypes = ["h1", "h2", "h3", "p", "span", "label"];
export default {
name: "AdText",
props: {
tag: {
type: String,
required: true,
default: "span",
validator: (value) => _tagTypes.includes(value),
},
content: {
type: String,
required: true,
},
for: {
type: String,
required: false,
default: null,
},
},
computed: {
// Rendered only if the tag is a label
getForProp() {
return ["label"].includes(this.tag) ? this.for : null;
},
},
};
</script>
Textarea
<template>
<textarea
v-model="value"
:id="id"
:name="name"
:placeholder="placeholder"
class="a-textarea"
></textarea>
</template>
<script>
export default {
name: "AdTextarea",
data() {
return {
value: "",
};
},
props: {
id: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
placeholder: {
type: String,
required: false,
default: null,
},
},
};
</script>
Our molecules components
And then the following molecules:
- CheckboxField
- InputField
- TextareaField
CheckboxField
<template>
<div class="m-checkbox-field">
<ad-input :id="id" :name="name" type="checkbox"></ad-input>
<ad-text tag="label" :for="id" :content="label"></ad-text>
</div>
</template>
<script>
import AdText from "../atoms/Text";
import AdInput from "../atoms/Input";
export default {
name: "CheckboxField",
components: {
AdText,
AdInput,
},
props: {
id: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
},
};
</script>
InputField
<template>
<div class="m-input-field">
<ad-text tag="label" :for="id" :content="label"></ad-text>
<ad-input
:id="id"
:name="name"
:placeholder="placeholder"
:type="inputType"
></ad-input>
</div>
</template>
<script>
import AdText from "../atoms/Text";
import AdInput from "../atoms/Input";
const _inputTypes = ["text", "email", "password"];
export default {
name: "InputField",
components: {
AdText,
AdInput,
},
props: {
id: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
placeholder: {
type: String,
required: false,
default: null,
},
inputType: {
type: String,
required: false,
default: "text",
validator: (value) => _inputTypes.includes(value),
},
},
};
</script>
TextareaField
<template>
<div class="m-textarea-field">
<ad-text tag="label" :for="id" :content="label"></ad-text>
<ad-textarea
:id="id"
:name="name"
:placeholder="placeholder"
type="text"
></ad-textarea>
</div>
</template>
<script>
import AdText from "../atoms/Text";
import AdTextarea from "../atoms/Textarea";
export default {
name: "TextareaField",
components: {
AdText,
AdTextarea,
},
props: {
id: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
placeholder: {
type: String,
required: false,
default: null,
},
},
};
</script>
Our organisms components
And finally we can develop our two organisms:
- ContactForm
- SignupForm
ContactForm
<template>
<form method="POST" class="o-contact-form" autocomplete="off">
<ad-text tag="h1" content="Contact us!"></ad-text>
<ad-input-field
id="name"
name="name"
label="Insert your name"
placeholder="Name"
></ad-input-field>
<ad-input-field
id="surname"
name="surname"
label="Insert your surname"
placeholder="Surname"
></ad-input-field>
<ad-input-field
id="email"
name="email"
label="Email"
input-type="email"
placeholder="Insert your e-mail"
></ad-input-field>
<ad-textarea-field
id="textarea"
name="textarea"
label="Leave a message"
placeholder="This post is amazing!"
></ad-textarea-field>
<ad-checkbox-field
id="checkbox"
name="checkbox"
label="Privacy policy"
></ad-checkbox-field>
<ad-button type="submit" label="Submit your request"></ad-button>
</form>
</template>
<script>
import AdText from "../atoms/Text";
import AdButton from "../atoms/Button";
import AdInputField from "../molecules/InputField";
import AdCheckboxField from "../molecules/CheckboxField";
import AdTextareaField from "../molecules/TextareaField";
export default {
name: "AdContactForm",
components: {
AdText,
AdButton,
AdInputField,
AdCheckboxField,
AdTextareaField,
},
};
</script>
SignupForm
<template>
<form method="POST" class="o-signup-form" autocomplete="off">
<ad-text tag="h1" content="Sign up!"></ad-text>
<ad-input-field
id="name"
name="name"
label="Insert your name"
placeholder="Name"
></ad-input-field>
<ad-input-field
id="surname"
name="surname"
label="Insert your surname"
placeholder="Surname"
></ad-input-field>
<ad-input-field
id="username"
name="username"
label="Insert your username"
placeholder="Username"
></ad-input-field>
<ad-input-field
id="email"
name="email"
label="Email"
input-type="email"
placeholder="Insert your e-mail"
></ad-input-field>
<ad-input-field
id="password"
name="password"
label="Password"
input-type="password"
placeholder="Insert your password here"
></ad-input-field>
<ad-input-field
id="confirm-password"
name="confirm-password"
label="Confirm password"
input-type="password"
placeholder="Confirm your password"
></ad-input-field>
<ad-checkbox-field
id="amazing-checkbox"
name="amazing_checkbox"
label="Privacy policy"
></ad-checkbox-field>
<ad-button type="submit" label="Join us!"></ad-button>
</form>
</template>
<script>
import AdText from "../atoms/Text";
import AdButton from "../atoms/Button";
import AdInputField from "../molecules/InputField";
import AdCheckboxField from "../molecules/CheckboxField";
import AdTextareaField from "../molecules/TextareaField";
export default {
name: "AdSignupForm",
components: {
AdText,
AdButton,
AdInputField,
AdCheckboxField,
AdTextareaField,
},
};
</script>
Let's consume our organisms
Now that we have our organisms, it's time to use them!
Open the App.vue
file and import the forms:
<template>
<div id="app">
<!-- You shouldn't use them together in the same page -->
<ad-contact-form></ad-contact-form>
<ad-signup-form></ad-signup-form>
</div>
</template>
<script>
import AdSignupForm from "./components/organisms/SignupForm";
import AdContactForm from "./components/organisms/ContactForm";
export default {
name: "App",
components: {
AdSignupForm,
AdContactForm,
},
};
</script>
The <ad-contact-form />
renders this:
and the <ad-signup-form />
renders this:
But why?
Maybe, at this point, you're asking yourself: "Ok, I got how it works... But why should I use the Atomic Design?"
I'm not the source of the truth, but I tell you why I like this approach.
Essentially for three reasons:
- Better organization
- Better design
- No boundaries
Better organization
As you have seen so far, this methodology can help you to organize your files and your components in a understandable and predictable way: you know where to put your components and how to organize them. And, following this pattern, your development process will become quicker.
Better design
With Better design I don't mean a better UI design but a better architectural design. If you start thinking of components as atoms, molecules and organisms, during the bootstrap of your application, you are enforced to project your software following this pattern putting your focus on the reusing of your components.
No boundaries
Being the Atomic Design a methodology, it is not strictly bound to particular technologies: you can apply it to different languages and frameworks: PHP, CSS, Laravel, Symfony, React, Angular, Vue and so on.
Follow me on
If you liked the post, you might offer me a ☕️ on PayPal. 🙂
Top comments (13)
Atomic design didn't work for me actually when using it with React. It makes finding the components harder than just using a flat, or feature-based structure. Let me give an example:
We have a
Button
component. It's a dumb component doing nothing, so we probably put this into Atoms. Now, I want to have anIconButton
, which usesButton
, so we'll put it to Molecules. Next, let's say we have aSubscriptionButton
, which opens a newsletter subscription modal - this is a lot of logic, so we'll put it to Organisms. Now, it was not obvious for me or my co-workers (and especially newcomers) where should we look for the specific component. It very quickly became a mess.IMO a lot clearer is a structure based on features. Basing on the earlier example,
Button
andIconButtons
would be just Components (where all reusable, dumb components land), andSubscriptionButton
would be somewhere in aSubscriptions
folder, along with things likeSubscriptionModal
,SubscriptionModalCloseButton
etc.Hi Jakub,
thank you for your comment: I really appreciate it!
I'm not saying that the Atomic Design is the panacea to all evils (it has some limits of course). What I wrote is that it can help you to think in reusable components way.
I think this approach is not correct: the
SubscriptionButton
shouldn't have any kind of logic, but it should have atrigger
prop to be used for launching events (in your case opening a Modal).The
SubscriptionModal
should be a different component and, using React, the best scenario could be something like:I would put the logic part in the component which wrap both
SubscriptionButton
andSubscriptionModal
.What do you think? 🙂
I agree! I think that I didn't use a proper example for this 😄 I used Atomic Design in a project a few years ago and I forgot already about the specific cases where we struggled with using it.
I don't say it's a bad pattern, it just didn't work for me for some reason 😄
Oh, forgot to mention: your article is great!
While I am a fan of applying atomic design to components, I have to disagree with you about it helping with code organisation - at least in the manner you propose.
At first it does help, when the application is small. However, down the road - when the application is larger and you get back to a feature after 6 months - not having the related components together creates mental overhead.
For our next project we will keep using atomic design, but we're looking into better ways to organise our components. We'll probably go back to feature folders and depending on the size of the feature the components will be either prefixed or put in subfolders according to their atomic type.
An added benefit is that it's a step closer to breaking a feature out into a separate npm package.
Is there a particular reason for making a text component?
35 lines of code just to render a
<p>
or<label>
seems incredibly overcomplicated.Hi Janus,
thank you for your comment: I really appreciate it!
I can get your perplexity about the "overengineering" of the
Text
component.Let me try to clarify your doubts.
The
Text
component in this context has too much implementation, it's true. Just two things:Having a component like this can bring some advantages.
Consistency
You have to think the this component is meant to be used and used and used... If you need to use it several times, then you must a have a consinstent component with the same size, the same font-family, the same color and so on. It's really hard to have this kind of consistency without using a component.
Let me give you one example:
This means that, when you need to write something, to have texts which are consinstent you have to apply the same classes. Everytime. And this is not a smart approach neither stable, because it's very buggy (try to imagine what could happen writing the
weight-medium
class 50 times).Automatic updates
Futhermore, with a reusable component, if you want to change a class or a default value, you just need to modify the component itself and the changes will be automatically reflected everywhere.
Having reusable components is very useful, especially in design systems.
Anyway, I want to give you a suggestion: do not put your focus on the
Text
's implementation; put it on the usage. The implementation is made only once, but the usage might be done n times.I'm sorry, but I don't think your example makes sense.
Of course, reusable components are useful. Very much so.
But you are talking about making an entire component only for text elements.
In your post, you describe making a molecule that combines a label with an input. That's fine.
But you gain nothing from having the label as a generic text component with
type=label
over just writing<label class="input-field__label"/>
.Everything you said about changing the label in one place still applies.
And if you want to have consistent
<p>'s
you put a standard style for paragraphs in your stylesheet.Your long example of five different classes implies that a
<span>
could have other classes. That would be exactly the same with a text componenttype=span
.So again, it's not actually simpler.
But of course, you should write your code however you see fit :-)
And that day you'll have to search and replace it in the whole code. ;)
How would you bind the events which are concerning the input fields? Would you bubble up all the events?
Or let's say I want to dispatch an action to my reducer. I should do this from my smart component while the Button (dumb component) is a child 3 levels bellow. So, when I click this button, should I have an onClick event that will go from the atom and then to the molecule and then to the organism and then to the page? Or how would you solve this?
What if one day You will decide to use some Bootstrap component instead of simple input? Or some other component library like Vuetify? Having this seperation gives You a freedom to do it very fast and easy. This is the point of having own abstraction.
By the way iconButton could be considered as molecule which cosists of button atom and icon atom ;)
Naming stuff well is hard and if You can put some system in place for that it can payoff in a large long term project. It is very easy to go wrong with component spliting. I have seen it many times. So if you think that things will never change think twice.
Hello, first of all thank you for this beautiful article. It is a method I use frequently. But there are a few questions in my mind I will be glad if you answer my questions. When we divide the components as you mentioned, it would be more correct to write the css of the element in the related component. Is it in our global css file or in direct component? Like style-scoped. And if we do it this way, for example, if I will use the button component in three places on the home page, will it be duplicated three times? Thank you.
Thank you for sharing the good post
In app.vue, how are we going to create 2 way binding between the input data inside the component with data declared in app.vue?