The complexity of modern websites has grown significantly over the past few years. The increased demand for high-quality, industry-standard designs further intensifies the challenges faced by frontend developers.
Today, even frontend apps need some architectural considerations to streamline the development process. In my previous article, I shared my experience implementing the clean architecture approach in front-end applications while working on my side project.
In this article, I aim to delve deeper into the atomic design approach, drawing from my experiences with the same project. I will discuss its advantages and disadvantages, and evaluate its usefulness in different scenarios.
To begin, let’s explore the concept of a design system. Design systems are comprehensive collections of reusable components, guidelines, and principles that empower teams to design and develop consistent user interfaces across multiple platforms.
They act as a single source of truth for both designers and developers, ensuring that the visual and functional aspects of a product align and adhere to the established brand identity. If you’re interested in exploring examples of design system implementations, consider examining the following:
If you want to dive deeper into the topic of design systems I recommend checking out this article. It describes this topic in detail, details that are not required for us in the scope of this work.
Building on the foundation of design systems, atomic design is a methodology that streamlines the organization and structuring of reusable components and guidelines. Conceived by Brad Frost, Atomic Design is inspired by chemistry, as it deconstructs user interfaces into their most fundamental building blocks and reassembles them into more intricate structures.
Here’s an image illustrating the analogy to chemistry:
An example of a chemical equation showing hydrogen and oxygen atoms combining together to form a water molecule.
Chemical reactions are represented by chemical equations, which often show how atomic elements combine together to form molecules. In the example above, we see how hydrogen and oxygen combine together to form water molecules.
In essence, atomic design is a natural evolution of design systems, providing a systematic approach to creating flexible and scalable components. By applying the principles of atomic design, teams can manage their design systems more efficiently, as the modular nature of this methodology makes it easier to maintain, update, and extend the components and patterns within the system.
If you’re concerned that this may sound complex, don’t worry. In the upcoming sections, I’ll demonstrate how to apply these principles using a real-life example from the app I’ve developed, making it easy to understand and implement in your own projects.
The atomic design organizes components into five distinct levels, each building upon the previous. Let’s explore these five levels in detail:
Atomic design is atoms, molecules, organisms, templates, and pages concurrently working together to create effective interface design systems.
In order to develop a well-informed perspective on atomic design for frontend development, I embarked on a journey to create an application. Over a period of six months, I gained valuable insights and experience while working on this project.
Consequently, the examples provided throughout this article draw from my hands-on experience with the application. To maintain transparency, all examples are derived from publicly accessible code.
You can explore the final result by visiting the repository or website itself.
Keep in mind, I’ll be using examples coded in React. If you’re not familiar with this language, don’t worry – I’ve aimed to illustrate the fundamental concepts of atomic design, rather than focusing on the nitty-gritty details of the code.
To get a better understanding of the components in my repository, you can find them under the following directory: /client/presentation
. In this location, I created a new directory called atoms
to maintain consistent naming with the atomic design methodology. This new directory contains all the small pieces needed to build the entire onboarding process.
The full list of atoms is as follows:
atoms
??? box
??? button
??? card
??? card-body
??? card-footer
??? container
??? divider
??? flex
??? form-control
??? form-error-message
??? form-helper-text
??? form-label
??? heading
??? icon
??? input
??? list
??? list-icon
??? list-item
??? spinner
??? tab
??? tab-list
??? tab-panel
??? tab-panels
??? tabs
??? text
These atom names may be familiar to you since they are based on the Chakra UIpackage. Most of them already contain the default matching style for my application, so there isn’t anything particularly unique to describe at this level. With that in mind, we can proceed directly to discuss the molecules
.
At this stage, the atomic design process becomes more interesting and its true power starts to reveal itself. While defining your base atoms may have been a time-consuming and monotonous task, building new components using atoms becomes much more enjoyable.
To define the molecules, I created a molecules
directory inside of my /client/presentation
directory. The full list of required molecules is as follows:
molecules
??? available-notion-database
??? full-screen-loader
??? input-control
??? onboarding-step-layout
??? onboarding-tab-list
Indeed, with just five molecules, we have enough components to accomplish our goal. It’s important to note that this is also an ideal place to include shared layouts built upon other atoms. For instance, the onboarding-step-layout
is utilized to maintain a consistent appearance throughout all five steps of the onboarding process.
The other components are as follows:
AvailableNotionDatabase component with header and base pieces of information about Notion database.
import { FC } from 'react';
import { Flex, Spinner } from '@presentation/atoms';
import { FullScreenLoaderProps } from './full-screen-loader.types';
export const FullScreenLoader: FC<FullScreenLoaderProps> = ({
children,
...restProps
}): JSX.Element => (
<Flex
alignItems="center"
bg="gray.50"
height="full"
justifyContent="center"
left={0}
position="fixed"
top={0}
width="full"
zIndex="9999"
{...restProps}
>
<Spinner />
{children}
</Flex>
);
There’s no rocket science here. This is just a combination of the already defined flex
and spinner
atoms.
input
atom with form-label
, form-control
, form-error-label
, and spinner
to show whether there is some background action happening. The component appears on the UI like this:InputControl component with the label.
OnboardingTabList component is used to show progress in the onboarding process.
Now that more pieces are ready, we can move on to defining larger blocks in our design puzzle.
This section is where I create each component responsible for displaying each step of the onboarding process.
To clarify things, I’ll just show you the list of created organisms:
organisms
??? onboarding-step-one
??? onboarding-step-two
??? onboarding-step-three
??? onboarding-step-four
??? onboarding-step-five
I believe the names are self-explanatory, and there should be no misunderstandings. To illustrate how I put everything together, I will present the code of one step as an example. Of course, if you want to check more, just visit my repository.
export const OnboardingStepFour: FC<OnboardingStepFourProps> = ({
onBackButtonClick,
onNextButtonClick,
}): JSX.Element => {
const { hasApiTokenData, isSetApiTokenLoading, setApiToken, setApiTokenError } = useSetApiToken();
const handleInputChange = debounce(async (event: ChangeEvent<HTMLInputElement>) => {
const result = await setApiToken(event.target.value);
if (result) {
onNextButtonClick();
}
}, 1000);
return (
<OnboardingStepLayout
subtitle="Paste your copied integration token below to validate your integration."
title="Validate your integration"
onBackButtonClick={onBackButtonClick}
>
<InputControl
isRequired
errorMessage={setApiTokenError || undefined}
isDisabled={isSetApiTokenLoading || hasApiTokenData}
isLoading={isSetApiTokenLoading}
label="Integration token"
name="integrationToken"
placeholder="Your integration token"
onChange={handleInputChange}
/>
</OnboardingStepLayout>
);
};
This code is entirely responsible for displaying step four in my onboarding process. I believe the only concern you might have is about making requests in organisms. Is this acceptable? There isn’t a one-size-fits-all answer, and I need to respond to these concerns with “It depends”. It depends on your structure.
If including an API call within a molecule or organism makes sense in the context of your application and does not overly complicate the component, it can be an acceptable solution. Just be cautious not to let presentation components become too tightly coupled with data-fetching or business logic, as it can make them more challenging to maintain and test.
In my scenario, this component is used in one place, and other solutions for performing an API call in that scenario are more complex and might produce way more code than necessary.
At this stage, the focus is on the structure and arrangement of the components rather than on the finer details of the UI. Templates also help to identify where state management should reside, which is usually in the page components that use the templates.
In the provided code example, we have an Onboarding
component that serves as a template:
import { FC } from 'react';
import { Flex, Heading, TabPanels, Tabs, Text } from '@presentation/atoms';
import { OnboardingTabList } from '@presentation/molecules';
import {
OnboardingStepFive,
OnboardingStepFour,
OnboardingStepOne,
OnboardingStepThree,
OnboardingStepTwo,
} from '@presentation/organisms';
import { OnboardingProps } from './onboarding.types';
export const Onboarding: FC<OnboardingProps> = ({
activeTabs,
createNotionIntegrationTabRef,
displayCreateNotionIntegrationTab,
displaySelectNotionDatabaseTab,
displayShareDatabaseIntegrationTab,
displayValidateIntegrationTab,
displayVerifyDatabaseTab,
selectNotionDatabaseTabRef,
shareDatabaseIntegrationTabRef,
validateIntegrationTabRef,
verifyDatabaseTabRef,
}) => (
<Flex direction="column" overflowX="hidden" px={2} py={{ base: '20px', sm: '25px', md: '55px' }}>
<Flex direction="column" textAlign="center">
<Heading
color="gray.700"
fontSize={{ base: 'xl', sm: '2xl', md: '3xl', lg: '4xl' }}
fontWeight="bold"
mb="8px"
>
Configure your Notion integration
</Heading>
<Text withBalancer color="gray.400" fontWeight="normal">
This information will let us know from which Notion database we should use to get your
vocabulary.
</Text>
</Flex>
<Tabs
isLazy
display="flex"
flexDirection="column"
mt={{ base: '10px', sm: '25px', md: '35px' }}
variant="unstyled"
>
<OnboardingTabList
activeTabs={activeTabs}
createNotionIntegrationTabRef={createNotionIntegrationTabRef}
selectNotionDatabaseTabRef={selectNotionDatabaseTabRef}
shareDatabaseIntegrationTabRef={shareDatabaseIntegrationTabRef}
validateIntegrationTabRef={validateIntegrationTabRef}
verifyDatabaseTabRef={verifyDatabaseTabRef}
/>
<TabPanels maxW={{ md: '90%', lg: '100%' }} mt={{ base: '10px', md: '24px' }} mx="auto">
<OnboardingStepOne onNextButtonClick={displayCreateNotionIntegrationTab} />
<OnboardingStepTwo
onBackButtonClick={displayVerifyDatabaseTab}
onNextButtonClick={displayShareDatabaseIntegrationTab}
/>
<OnboardingStepThree
onBackButtonClick={displayCreateNotionIntegrationTab}
onNextButtonClick={displayValidateIntegrationTab}
/>
{activeTabs.validateIntegration ? (
<OnboardingStepFour
onBackButtonClick={displayShareDatabaseIntegrationTab}
onNextButtonClick={displaySelectNotionDatabaseTab}
/>
) : null}
{activeTabs.selectNotionDatabase ? (
<OnboardingStepFive onBackButtonClick={displayVerifyDatabaseTab} />
) : null}
</TabPanels>
</Tabs>
</Flex>
);
This Onboarding
component assembles atoms, molecules, and organisms to create the layout for the onboarding process. Notice that the state management and tab navigation logic has been separated from this component. The necessary state and callback functions are now received as props, allowing a higher-level “page” component to handle state and data management.
This separation of concerns keeps the template focused on the layout and structure while ensuring that state management is handled at the appropriate level.
In the end, I would like to present step 4 as a final result:
In the context of our previous discussion, the “page” component uses the Onboarding
template and handles state management for the onboarding process. While the code for this specific page component is not provided here, you can find it in my repository. As mentioned, there’s nothing extraordinary about the page component’s code; it primarily focuses on managing the state and passing it down to the Onboarding
template.
If we have a look at what atomic design looks like in practice. Let’s dive into the pros and cons of this approach.
While atomic design offers numerous obvious benefits, such as modularity, reusability, and maintainability, it also comes with a few disadvantages that are worth considering at the beginning:
However, this approach has its own already-mentioned strengths. Let’s talk about them in more detail:
This article was originally published by Pawe? Wojtasi?ski on Hackernoon.
In the rapidly evolving financial technology landscape, innovative product studios are emerging as powerful catalysts…
In an era defined by rapid technological advancement, Artificial Intelligence (AI) stands as a transformative…
In a historic moment for Indian esports, Wasfi “YoshiKiller” Bilal secured a silver medal at…
The Tech Panda takes a look at recently launched gadgets & apps in the market.…
The Tech Panda takes a look at what’s buzzing in the startup ecosystem. The startup…
With just days until the outcome of the U.S. presidential race, Bitcoin enthusiasts across the…