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

How to organize your components using the Atomic Design

sanfra1407 profile image Giuseppe Sanfrancesco ใƒปUpdated on ใƒป7 min read

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>

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 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>

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 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>

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. ๐Ÿ™‚


Discussion

pic
Editor guide
Collapse
sarneeh 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 Sanfrancesco Author

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.

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>

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 } />
      </>
   )
}

What do you think? ๐Ÿ™‚

Collapse
sarneeh 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
floede profile image
Janus Hasseriis

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 Sanfrancesco Author

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
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
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
adinancenci profile image
Adinan Cenci

Why would write a component just to wrap a single html element adding no new functionality?

This is insanity

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.

Collapse
adinancenci profile image
Adinan Cenci

Then that day I will have to revise the code, native html or component. Way to overcomplicate things

Thread Thread
sanfra1407 profile image
Giuseppe Sanfrancesco Author

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

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?