DEV Community

Cover image for How to organize your components using the Atomic Design
Giuseppe
Giuseppe

Posted on • Updated on

How to organize your components using the Atomic Design

Often, when you start the development of an application, you can get in trouble trying to understand how to organize your files.

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've adopted in MotorK for both our Design System and Single Page Applications. It's called: Atomic Design.


Table of Contents

  1. Atomic Design
  2. A real example
  3. But why?

Atomic Design

Break entire interfaces down into fundamental building blocks and work up from there. That’s the basic gist of atomic design.

Brad Frost

The Atomic Design's principle is to split your UI parts into "small" 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 create an application using the Atomic Design. The final goal is to create and render 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:
Folders structure

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 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Our organisms components

And finally we can write 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Let's use our organisms

Now we have our two 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>
Enter fullscreen mode Exit fullscreen mode

The <ad-contact-form></ad-contact-form> renders this:
Contact form

and the <ad-signup-form></ad-signup-form> renders this:
Signup form


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 can tell you why I like this approach.
Essentially for three reasons:

  • Better organization
  • Better design
  • No boundaries

Better organization

As you have seen as 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 phase 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. 🙂


Oldest comments (13)

Collapse
 
jsardev profile image
Jakub Sarnowski

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 an IconButton, which uses Button, so we'll put it to Molecules. Next, let's say we have a SubscriptionButton, 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 and IconButtons would be just Components (where all reusable, dumb components land), and SubscriptionButton would be somewhere in a Subscriptions folder, along with things like SubscriptionModal, SubscriptionModalCloseButton etc.

Collapse
 
sanfra1407 profile image
Giuseppe • Edited

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.

Let's say we have a `SubscriptionButton`, which opens a newsletter 
subscription modal - this is a lot of logic, so we'll put it to Organisms.
Enter fullscreen mode Exit fullscreen mode

I think this approach is not correct: the SubscriptionButton shouldn't have any kind of logic, but it should have a trigger 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:

<SubscriptionModal>
<!-- A subscription form-->
</SubscriptionModal>
Enter fullscreen mode Exit fullscreen mode

I would put the logic part in the component which wrap both SubscriptionButton and SubscriptionModal.

const _handleModal = () => {
   this.setState( prevState => {
      return {
         subscriptionModalOpened: !prevState.subscriptionModalOpened,
      }
   } 
} )

render() {
   const { subscriptionModalOpened } = this.state;

   return (
      <>
         { subscriptionModalOpened ? 
            <SubscriptionModal closeModal={ _handleModal } /> :
            null;
         }
         <SubscriptionButton trigger={ _handleModal } />
      </>
   )
}
Enter fullscreen mode Exit fullscreen mode

What do you think? 🙂

Collapse
 
jsardev profile image
Jakub Sarnowski

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!

Collapse
 
lordsayur profile image
Omar

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?

Collapse
 
floede profile image
Janus Hasseriis • Edited

Is there a particular reason for making a text component?
35 lines of code just to render a <p> or <label> seems incredibly overcomplicated.

Collapse
 
sanfra1407 profile image
Giuseppe • Edited

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:

  • This is a simple example;
  • This is a small application.

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:

<template>
   <span class="text-component size-big weight-medium family-primary color-secondary">
      {{myAwesomeText}}
   </span>
<template>

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.

Collapse
 
floede profile image
Janus Hasseriis

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 component type=span.
So again, it's not actually simpler.

But of course, you should write your code however you see fit :-)

Collapse
 
wvandam profile image
Wouter van Dam

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.

Collapse
 
monas profile image
Laimonas

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.

 
sanfra1407 profile image
Giuseppe

And that day you'll have to search and replace it in the whole code. ;)

Collapse
 
luardo profile image
Luis Eduardo

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?

Collapse
 
enderimen profile image
Ender İMEN • Edited

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.

Collapse
 
lico profile image
SeongKuk Han

Thank you for sharing the good post