Blog

percipio.london: Integrating atomic design in your CraftCMS workflow

Atomic design is a naming convention borrowed from chemistry and applied to elements on the web.

In the natural world, atomic elements combine together to form molecules. These molecules can combine further to form relatively complex organisms. We use this same principle in our development workflow – read on to find out more. 

Taking cues from chemistry

In the natural world, atomic elements combine together to form molecules. These molecules can combine further to form relatively complex organisms. To expound a bit further:

Atoms are the basic building blocks of all matter. Each chemical element his distinct properties and they can’t be broken down further without losing their meaning.

Molecules are groups of two or more atoms held together with chemical bonds. These combinations of atoms take on their own unique properties and become more tangible and operational than atoms.

Organisms are assemblies of molecules functioning together as a unit. These relatively complex structures can range from single-celled organisms all the way up to incredibly sophisticated organisms like human beings.

Now, we might be simplifying the incredibly rich composition of the universe ever-so-slightly, however the basic premise remains

atoms combine together to form molecules – which further combine to form organisms. 

This atomic theory means that all matter in the known universe can be broken down into a finite set of atomic elements. 

OK, High school chemistry lesson over… what has this got to do with web development? Watch the quick video below first. 

All websites, apps, intranets, hoobadyboops and whatevers are all composed of the same HTML elements.

So, if Dmitri Mendeleev can organise the universe into the periodic table, then we should be able to do it with our web application development. This gives our entire team a common language when discussing projects and allows us to think of our user interfaces as both a cohesive whole and a collection of parts at the same time.

Atoms

The atoms of our interfaces are the foundational building blocks that comprise all of our interfaces. 

These atoms include the basic elements like form labels, inputs, buttons, images and others. They can’t be broken down any further without ceasing to be functional. 

Each atom has its own unique properties, such as the ratio of a hero image or the font size of a primary heading. 

These properties influence how each atom should be applied to the broader user interface system.

Molecules

Molecules are groups of atoms bonded together that take on distinct new properties. 

They are relatively simple groups of UI elements functioning together as a unit. For example an image, heading and description can join together to create a card molecule.

Making simple UI molecules makes testing easier, encourages reusability and promotes consistency throughout the project.

Organisms

Organisms are more complex UI components, they are created by groups of molecules and/​or atoms and/​or other organisms which form distinct sections of an interface. 

While some organisms like a header might consist of different types of molecules like a primary navigation, search form and logos, others might consist of the same molecule repeated over and over again. 

For instance if you visit an eCommerce site or a news site you will most likely see listings of articles that are contained in repeatable cards or panels.

Building up from molecules to more elaborate organisms provides us with an important sense of context – they demonstrate those smaller, simpler components in action and serve as distinct patterns that can be used again and again.

Templates

Templates are page level objects that place components into a layout and articulate the underlying content structure of the design. They have the important characteristic in that they focus on the page’s underlying content structure rather than the page’s final content.

Atomic design provides us a structure to navigate between parts and the whole of our UIs, which is why it’s crucial to reiterate that atomic design is not a linear process. 

It would be foolish to design buttons and other elements in isolation, then cross our fingers and hope everything comes together to form a cohesive whole. This means, don’t interpret the stages of atomic design as Step 1: atoms; Step 2; molecules, Step 3: organisms; Step 4: templates”. 

Instead, think of the stages of atomic design as a mental model that allows us to concurrently create final UIs and their underlying design systems.

Template structure setup within Craft

So, how do we translate all of this into the Craft CMS template structure?

In our templates folder we create three main groups for our atomic design structure. As you may expect, they’re named according to our atomic design the principle _​atoms , _​molecules and _​organisms.

Keynote atomic design template structure

You’ll notice every folder is prepended with an underscore _. This ensures that these directories cannot be loaded directly via a URL

Inside of these main folders we have pluralised, sub directories (buttons, images, links…). Inside these directories you’ll find template files in the singular form of its parent directory name.

For example, inside buttons we have button--variant.

Doing so means that when we pass properties though an object we don’t need to think about variable order in the function as showcased in this simple atom render function

Atom render function: _macros/atoms.twig
{% macro atom(type, atom, options, prefix = '') %}
    {% apply spaceless %}
        {% include '_atoms/' ~ type ~ 's/' ~ prefix ~ type ~ '--' ~ atom with {
            options: options,
        } %}
    {% endapply %}
{% endmacro %}

This means that by retaining a clean and purposeful naming convention within our directory structure we can build larger, more complex web applications at scale.

Templating _​atoms

Inside of our _atoms directory we house all our atoms in our projects. As a reminder, these smaller templates are the foundational building blocks that we will need throughout our project – items that cannot be broken down any further. 

Text atoms are probably the simplest atoms you will find in projects, all they do is contain text content in different formats and styles – think headings, paragraphs etc.

As every atom that we create will share common properties, we create a twig file that contains our common properties and name this text--props.

Text properties: _atoms/texts/text--props
{#
    ### PROPERTIES ###
    ------------------
    alignment: (STR) -- the text alignment
    color: (STR) -- the color of the text
    content: (STR) -- the actual content as a string
    utilities: (STR) -- the specific TW spacing utilities to add
#}

{%- set props = {
    content: options.content ??? null,
    color: options.color ??? 'gray-900',
    utilities: options.utilities ?? null,
    alignment: options.alignment ??? 'text-left',
} -%}

{%- block text -%}
{%- endblock -%}

Now that we have our base file in place, we can extend the specific atoms from these properties, avoiding repeatability and copy/​pasting. As you can see in our text--card-title example:

Card Title: _atoms/texts/text--card-title
{%- extends '_atoms/texts/_text--props' -%}

{#
    ### PROPERTIES ###
    ------------------
    INHERITED FROM: `_atoms/texts/text--props.twig`

    OMITTED
    ---------
    alignment
    color
#}

{%- block text -%}

    {%- minify -%}
        <span class="text-gray-800 text-2xl-md md:text-2xl lg:text-3xl tracking-tightest {{ props.utilities -}}">
            {{- props.content | typogrify(true) | raw -}}
        </span>
    {%- endminify -%}

{%- endblock -%}

We slimmed down the comments in this one, as even tho they’re available through the parent template, we are not using color and alignment here.

And the beauty of this… we don’t need to! As every property has a default fallback, using the empty-coalesce operator ??? which is not built-in into Craft sadly. But as usual, Andrew Welch have us covered with his empty-coalesce plugin

Buttons

Applying this same logic to a more complex example we can take a look at a button atom. As with text, we need a button--props file to house all of our button properties.

You will notice a lot of additional properties here, since a button does a lot more than just showing some text. Buttons contain an action or url, they might have an icon and they’ll most likely require some event tracking functions too.

Button properties: _atoms/buttons/_button--props.twig
{#
    ### PROPERTIES ###
    ------------------
    ga: (OBJ) -- add a category / action / label for GTM tracking
    icon: (STR) -- the icon for the button ( optional )
    iconPosition: (STR) -- the position of the icon ( optional )
    target: (STR) -- add _blank if you want to open the button in a new window
    text: (STR) -- the text for the button
    url: (STR) -- the url for the button
    utlities: (STR) -- the button utilities
#}

{%- set props = {
    url: options.url ??? null,
    text: options.text ??? '',
    icon: options.icon ??? null,
    iconPosition: options.iconPosition ??? 'right',
    utilities: options.utilities ??? null,
    active: options.active|default(false),
    target: options.target ??? null,
    ga: options.ga ??? null,
} -%}

{%- block button -%}
{%- endblock -%}

Atoms shouldn’t be too complicated, so we try to avoid into overloading with logic statements. 

Button Primary: atoms/buttons/_button--primary.twig
{%- extends '_atoms/buttons/_button--props' -%}
{%- import '_macros/atoms' as render -%}
{%- import '_macros/functions' as parse -%}
{%- import '_macros/icons' as icon -%}

{#
    ### PROPERTIES ###
    ------------------
    INHERITED FROM: `_atoms/buttons/button--props.twig`
    ### FUNCTIONS ###
    -----------------
    ga: parse google analytics attributes
    ### LUTS ###
    ------------
    icon.fa_icon_lut: font-awesome class LUT
    ### COMPONENTS ###
    ------------------
    ## ATOMS
    render.icon: `_atoms/icons/icon--fa`
#}

{%- block button -%}

    {%- set classes = {
        '_default': 'inline-flex text-center rounded-full',
        'animation': 'transition-colors duration-200 ease-in-out',
        'color': 'text-white bg-blue-500',
        'focus' : 'focus:outline focus:outline-4 focus:outline-blue-500/30',
        'font': 'font-primary text-sm font-semibold',
        'hover': 'hover:bg-blue-700',
        'padding': 'py-3 px-7'
    }-%}

    {%- minify -%}

        <a
            {{ props.url ? 'href=' ~ props.url : '' }}
            {{ parse.ga(props.ga) }}
            {{ props.target ? 'target=' ~ props.target : '' }}
            aria-label="{{- props.text -}}"
            class="{{ classes._default }} {{ classes.animation }} {{ classes.color }} {{ classes.focus }} {{ classes.font }} {{ classes.hover }} {{ classes.padding }} {{ props.utilities }}"
        >
            <span class="sr-only">
                {{- props.target == '_self' ? 'Go to: ' : 'Go to external page: ' -}}
            </span>

            <span>
                {{- props.text -}}
            </span>

            {%- if props.icon -%}

                {{- render.atom('icon', 'fa', {
                    icon: icon.fa_icon_lut(props.icon),
                    size: "xs",
                    parentcss: 'block mt-px ' ~ (props.iconPosition == 'right' ? 'ml-2' : 'mr-2')
                }) -}}

            {%- endif -%}

        </a>

    {%- endminify -%}

{%- endblock -%}

Here we have our object that groups all the tailwind utility classes in a readable format. Since we also have the possibility to use icons in buttons we can even include another atom into this atom

A button with an icon is still too simple to label it as a molecule. Doing so would break consistency as buttons would live under molecules and under atoms – sometimes a little sane sacrifice needs to be made.

But why not include the icon inside of the button atom? The reason here is simple, an icon can also be part of other molecules and/​or atoms – if we add it to our button as another atom, we can also include that same atom wherever else it may be needed. 

Templating _​molecules

As we now know, molecules are composed out of different atoms.

Let’s run through the primary card molecule card--primary-large that we use here on our Percipio Site, as this is a perfect example of how simple atoms combined can create something a lot more visual.

Card primary large: _molecules/cards/card--primary-large
{%- extends '_molecules/cards/_card--props' -%}
{%- import '_macros/atoms' as render -%}

{#
    ### PROPERTIES ###
    ------------------
    INHERITED FROM: `_molecules/cards/_card--props.twig`
    ### COMPONENTS ###
    ------------------
    ## ATOMS
    image--card-large: `_atoms/images/image--card-large`
    text--card-subtitle: `_atoms/texts/text--card-subtitle`
    text--card-title: `_atoms/texts/text--card-title`
#}

{%- block card -%}
    <article class="lg:col-span-2 relative bg-white shadow-2xl rounded-4xl p-6 pb-9 overflow-hidden transition-shadow ease-in-out hover:shadow-zinc-500 {{ props.utilities -}}">

        <div class="relative rounded-[20px] overflow-hidden -mt-4 -mx-4 md:m-0">
            {{- render.atom('image', 'card-large', {
                    image: content.cardImage.collect().first(),
                    sizes: ['(max-width:768px) 100vw', '(min-width:768px) 66.6vw'],
                    alt: content.title,
                    placeholders: placeholders,
                    ratio: 'aspect-16/9 lg:aspect-[4/5]'
                })
            -}}
        </div>

        <header class="space-y-4 mt-8">
            <h3>
                {{- render.atom('text', 'card-title', {
                    content: content.title ??? ''
                }) -}}
            </h3>

            {%- if content.subtitle ??? null -%}
                <h4>
                    {{- render.atom('text', 'card-subtitle', {
                        content: content.subtitle
                    }) -}}
                </h4>
            {%- endif -%}
        </header>

        <div class="bg-{{- props.color }} h-3 absolute bottom-0 left-0 w-full"></div>

        {{- render.atom('link', 'card', {
                url: content.url,
                text: content.title,
                ga: {
                    category: 'Primary Card Large',
                    action: 'click',
                    label: content.title ??? null,
                },
            })
        -}}
    </article>
{%- endblock -%}

As you can see, we simply import our macro as a render function and through the simple structure of this card we include the needed atoms where they belong.

But what if we need a change our image atom or card-title atom? Perhaps by changing the ratio or the font-size… no problem! We can edit specific atom that controls the ratio or font-size and all the card titles and image ratios for each and every card has been updated, including other places we need the exact same atoms.

By changing one line of code in one file, we ensure that everywhere we use this element in the project will be updated. This eliminates headaches tracking down multiple templates and the fear of deploying and noticing that you forgot to update an element on a long forgotten template!

As you can see with our button molecule we are starting to add a lot of functionality to a simple link.

As a rule, element queries or data fetches are not done at molecule level – this always happens on the top layer, namely our organisms. This is important, because it separates the atoms and molecules from functional organisms. 

Templating _​organisms

Eventually we end up at our more complex organisms, these can combine the molecules and atoms or even other organisms. These bigger organisms will also pass down the variables and the data that we fetch from our Craft CMS entries.

Most of the time, our grids contain cards for an overview page or categorised page where we show a subset of our entries. 

Grid projects: _organisms/grids/grid--projects
{%- extends '_organisms/grids/_grid--props' -%}

{#
    ### PROPERTIES ###
    ------------------
    INHERITED FROM: `_organisms/grids/grid--props.twig`
    EXTENDED
    --------
    ODD (READONLY): determines card size on the odd loop
    EVEN (READONLY): determines card size on the even loop
    ### COMPONENTS ###
    ------------------
    ## MOLECULES
    card--primary-large: `_molecules/cards/card--primary-large`
    card--primary-small: `_molecules/cards/card--primary-small`
#}

{%- block grid -%}

    {%- set props = props|merge({
        ODD: props.startSize == 'large' ? 'small' : 'large',
        EVEN: props.startSize == 'large' ? 'large' : 'small',
    }) -%}

    {%- minify -%}
        <section class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 py-12 md:py-32 container max-w-screen-xl mx-auto">

            {%- include '_molecules/cards/card--primary-' ~ props.startSize ignore missing with {
                content: props.firstCard,
                color: 'blue-500',
                utilities: ''
            } -%}

            {%- for cards in props.chunks -%}

                {%- set size = loop.index is odd ? props.ODD : props.EVEN -%}

                {%- for card in cards -%}

                    {%- include '_molecules/cards/card--primary-' ~ size ignore missing with {
                        content: card,
                        color: loop.index0 is odd ? 'blue-500' : 'pink-500',
                        utilities: ''
                    } -%}

                {%- endfor -%}

            {%- endfor -%}

        </section>
    {%- endminify -%}
{%- endblock -%}

This is our grid organism where we expect an array of cards to be given, this will be passed in through another organism which we will go through soon enough. As you can see this organism contains a little loop logic to make sure we can alternate between large and small cards in the grid.

We also make use of the ignore missing keyword in every include we do which ensures we don’t receive an internal server error in a production environment if there is an include to a non-existing file. 

On the includes we also make use of the with parameter, which can be used to pass variables into the child template. There we expect a content variable into our card molecule that then will read all available options. 

In this passed on variable we have all the Craft methods available if we would need some, without putting extra stress on fetching information again and avoiding to add more queries.

Putting it all together _templates

Templates bring together our atoms, molecules and organisms, they’re the final step in building out and displaying operational web application pages. 

Page router: _organisms/page.twig
{% extends '_boilerplate/_layouts/generic-page-layout.twig' %}

{#
    ### COMPONENTS ###
    ------------------
    ## ORGANISMS
    view--detail-blog: `_organisms/views/view--detail-blog`
    view--detail-projects: `_organisms/views/view--detail-projects`
    view--detail-services: `_organisms/views/view--detail-services`
    view--detail-team: `_organisms/views/view--detail-team`
    view--detail-vacancies: `_organisms/views/view--detail-vacancies`
    view--overview-blog: `_organisms/views/view--overview-blog.twig`
    view--overview-projects: `_organisms/views/view--overview-projects.twig`
    view--overview-services: `_organisms/views/view--overview-services.twig`
    view--overview-team: `_organisms/views/view--overview-team.twig`
    view--overview-plugins: `_organisms/views/view--overview-plugins.twig`
    view--overview-vacancies: `_organisms/views/view--overview-vacancies.twig`
    view--page-contact: `_organisms/views/view--page-contact`
    view--page-content: `_organisms/views/view--page-content`
    view--page-home: `_organisms/views/view--page-home`
    view--page-landing: `_organisms/views/view--page-landing`
#}

{% block headLinks %}
    {{ parent() }}
{% endblock headLinks %}

{%- set component = null -%}

{% block content %}

    {%- if entry ??? null %}

        {%- switch entry.type -%}

            {%- case 'blog' -%}
                {%- set component = '_organisms/views/view--detail-blog' -%}

            {%- case 'blogOverview' -%}
                {%- set component = '_organisms/views/view--overview-blog' -%}

            {%- case 'contactPage' -%}
                {%- set component = '_organisms/views/view--page-contact' -%}

            {%- case 'contentPage' -%}
                {%- set component = '_organisms/views/view--page-content' -%}

            {%- case 'homepage' -%}
                {%- set component = '_organisms/views/view--page-home' -%}

            {%- case 'landingPage' -%}
                {%- set component = '_organisms/views/view--page-landing' -%}

            {%- case 'projects' -%}
                {%- set component = '_organisms/views/view--detail-projects' -%}

            {%- case 'projectsOverview' -%}
                {%- set component = '_organisms/views/view--overview-projects' -%}

            {%- case 'services' -%}
                {%- set component = '_organisms/views/view--detail-services' -%}

            {%- case 'servicesOverview' -%}
                {%- set component = '_organisms/views/view--overview-services' -%}

            {%- case 'teamMember' -%}
                {%- set component = '_organisms/views/view--detail-team' -%}

            {%- case 'teamOverview' -%}
                {%- set component = '_organisms/views/view--overview-team' -%}

            {%- case 'plugin' -%}
                {%- set component = '_organisms/views/view--detail-plugins' -%}

            {%- case 'pluginsOverview' -%}
                {%- set component = '_organisms/views/view--overview-plugins' -%}

            {%- case 'vacancies' -%}
                {%- set component = '_organisms/views/view--detail-vacancies' -%}

            {%- case 'vacanciesOverview' -%}
                {%- set component = '_organisms/views/view--overview-vacancies' -%}

        {%- endswitch -%}

        {%- if component -%}
            {%- include component ignore missing with {
                content: entry,
            } -%}
        {%- endif -%}

    {%- endif -%}

{% endblock %}

{# -- Any JavaScript that should be included before </body> -- #}
{% block bodyJs %}
    {{ parent() }}
{% endblock bodyJs %}

In our template structure you’ll find a page.twig file as main file that contains our our builder – a simple loop over our block types and based on the type and/​or content that the block contains, we load the correct molecules or organisms that are needed to be parsed.

Templating _​pages

To keep everything simple in the CP each and every section, is it a channel, structure or single, all will point to the exact same entry point namely _organisms/page.twig

Builder: _organisms/builders/builder--page
{%- do craft.eagerBeaver.eagerLoadElements(entry,
    [
        'pageBuilder',
        'pageBuilder.contentCta',
        'pageBuilder.contentCta:image',
        'pageBuilder.highlightCards:cards',
        'pageBuilder.highlightCards:cards.authors',
        'pageBuilder.highlightCards:cards.blogCategories',
        'pageBuilder.highlightCards:cards.cardImage',
    ])
-%}

{#
    ### COMPONENTS ###
    ------------------
    ## MOLECULES
    header--primary: `_molecules/headers/header--primary`
    ## ORGANISMS
    cta--image: `_organisms/ctas/cta--image`
    cta--text: `_organisms/ctas/cta--text`
    grid--blog: `_organisms/grids/grid--blog`
    grid--projects: `_organisms/grids/grid--projects`
#}

{%- minify -%}

    {%- for block in content.pageBuilder.collect() -%}

        {%- switch block.type -%}

            {%- case 'contentCta' -%}

                {%- set background = swatch(block.background) -%}

                {# check which cta should be loaded #}
                {%- set component = block.image.count() == 0 ? '_organisms/ctas/cta--text' : '_organisms/ctas/cta--image' -%}

                {%- include component ignore missing with {
                    heading: block.heading ??? '',
                    content: block.body ??? '',
                    button: block.target ??? null,
                    background: background,
                    image: block.image ??? null
                } -%}

            {%- case 'highlightCards' -%}

                {%- set component = block.cards.collect().first().section.handle == 'blog' ? '_organisms/grids/grid--blogs' : '_organisms/grids/grid--projects' -%}

                {%- include component ignore missing with {
                    cards: block.cards,
                } -%}

            {%- case 'sectionHeader' -%}

                {%- set background = swatch(block.background) -%}

                {# check which header should be loaded #}
                {%- set component = (block.target.getUrl() ??? null) ? '_molecules/headers/header--button' : '_molecules/headers/header--text' -%}

                {%- include component ignore missing with {
                    heading: block.heading ??? '',
                    description: block.description ??? '',
                    button: block.target ??? null,
                    background: background,
                } -%}

        {%- endswitch -%}

    {%- endfor -%}

{%- endminify -%}

This is our base file that will take every entry request, check if an entry actually exists and then based on the entry type ( handle ) will serve up the view we request.

This makes everything easy in the backend, no more doubting or thinking to which template we should actually point.

Summary

Since we work with a structure where a single file change impacts every template an atom, molecule or organisms is used, we reduce a lot of maintenance overheads as we don’t need to check in the templates where every button lives. 

A simple class change will affect the entire site so the design system stays consistent throughout. Of course the code comments are also a big help if you need to come back to a project after a while

As complex as this might seem, atomic design actually makes it easier for new developer onboarding. Since they can work on smaller items that match their knowledge and experience. If they still need training for more complicated components they can start working on simple atoms and finally work their way up throughout the rest.

Resources

Want to know more?

We regularly talk about atomic design at conferences, meet-ups and direct with development agencies.

Go to: Email us to find out more