paint-brush
Design Systems: A Front-End Engineer's Guide to Efficient Atomic Designby@esemonday
276 reads

Design Systems: A Front-End Engineer's Guide to Efficient Atomic Design

by Ese MondayMay 1st, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Learn about design systems and their importance in front-end development. Explore Atomic Design methodology introduced by Brad Frost, breaking UI components into reusable blocks like atoms, molecules, and organisms. Discover how design systems promote visual consistency and structure applications for front-end engineers. No prerequisites required, but familiarity with React.js and TypeScript is recommended.
featured image - Design Systems: A Front-End Engineer's Guide to Efficient Atomic Design
Ese Monday HackerNoon profile picture

Introduction

Have you ever worked on a project, and on further review, your styling seems misaligned or inconsistent in some way? Or are you looking for styling guidelines for your next solo project? - That's where design systems come to the rescue. This article breaks down design systems, The benefits they provide, and how to build them out in front-end applications from the perspective of a front-end engineer. While there are no stringent prerequisites to follow along, a good understanding of Reactjs and Typescript is required

What are Design Systems?

In 2013, Brad Frost introduced the concept of Atomic Design. In its essence, Atomic design is a methodology that breaks down UI components into smaller, reusable building blocks categorized as atoms, molecules, organisms, templates, and pages. Just like a Lego brick, the smaller pieces fuse to form bigger pieces, which also fuse to form more complex pieces.

Design systems are a set of standard, reusable components and patterns that are used to create visual consistency throughout your project. From the perspective of a front-end engineer, it teaches you how to structure applications into reusable components and avoid repetition.

What are Atoms?

Atoms are the foundational building blocks of a design system. In front-end development, atoms typically correspond to basic HTML elements like buttons, input fields, checkboxes, and icons.


They represent the smallest, indivisible components that cannot be broken down further without losing meaning.

What are Molecules?

Molecules comprise a group of atoms working together to form a more complex unit. It can be a combination of atoms, such as a search bar (consisting of an input field atom and a button atom, a form (combining input fields, buttons, and labels), or a card component (containing text, images, and buttons).

What are Organisms?

Organisms are higher-level components that combine various atoms or molecules to form a distinct section of a user interface. An example of an organism is a header that is composed of a logo, navigation menu, and search bar, a product listing that comprises a product card and filtering/sorting control, or a form section.

Putting Design System Into Practice

To effectively translate a design system to code, consider the following principles and practices:

1. Define Design Tokens:

Design tokens are foundational values that are used to maintain visual consistency across components. These foundation values include colors, typography styles, spacing units, and breakpoints.

/* Primary Colors */
:root {
  --primary: #007bff;
  --primary-50: #cce0ff;
  --primary-100: #99ccff;
  --primary-200: #66b3ff;
  --primary-300: #3399ff;
  --primary-400: #007bff;
  --primary-500: #005cbf;
  --primary-600: #004499;
  --primary-700: #003374;
  --primary-800: #00264d;
  --primary-900: #001a33;
}

/* Secondary Colors */
:root {
  --secondary: #ff9900;
  --secondary-50: #fff2e6;
  --secondary-100: #ffddb3;
  --secondary-200: #ffc680;
  --secondary-300: #ffb34d;
  --secondary-400: #ffa326;
  --secondary-500: #ff9900;
  --secondary-600: #e68a00;
  --secondary-700: #cc7a00;
  --secondary-800: #b36b00;
  --secondary-900: #995c00;
}

/* Additional Colors */
:root {
  /* Danger Colors */
  --danger: #dc3545;
  --danger-50: #fee2e4;
  --danger-100: #fcc6cc;
  --danger-200: #faaeb4;
  --danger-300: #f9969c;
  --danger-400: #f77e84;
  --danger-500: #f55d6e;
  --danger-600: #f34d5f;
  --danger-700: #f03d50;
  --danger-800: #ee2d41;
  --danger-900: #ec1d32;

  /* Info Colors */
  --info: #17a2b8;
  --info-50: #d8eef2;
  --info-100: #b2ddee;
  --info-200: #8dd1e9;
  --info-300: #67c5e5;
  --info-400: #41b9e1;
  --info-500: #17a2b8;
  --info-600: #138496;
  --info-700: #106273;
  --info-800: #0d4851;
  --info-900: #0a2c30;

  /* Warning Colors */
  --warning: #ffc107;
  --warning-50: #fff8e0;
  --warning-100: #ffecb3;
  --warning-200: #ffe380;
  --warning-300: #ffd24d;
  --warning-400: #ffca26;
  --warning-500: #ffc107;
  --warning-600: #e6ae00;
  --warning-700: #bf9500;
  --warning-800: #997d00;
  --warning-900: #806800;

  /* Success Colors */
  --success: #28a745;
  --success-50: #d4edda;
  --success-100: #a9e2c8;
  --success-200: #7dceb5;
  --success-300: #51baa3;
  --success-400: #37b386;
  --success-500: #28a745;
  --success-600: #218838;
  --success-700: #1e7232;
  --success-800: #1a602b;
  --success-900: #154c22;
  
  /* White & Black Colors */
  --white: #fff;
  --black: #000;
  
}

:root {
  /* Font Family */
  --font-family: 'Arial', sans-serif;

  /* Font Sizes */
  --font-size-small: 14px;
  --font-size-base: 16px;
  --font-size-medium: 18px;
  --font-size-large: 24px;
  --font-size-extra-large: 32px;

  /* Font Weights */
  --font-weight-normal: 400;
  --font-weight-bold: 700;

  /* Line Heights */
  --line-height-base: 1.5;
  --line-height-small: 1.3;
  --line-height-large: 1.8;
  
  /* button size */
   --button-small: 8px;
  --button-medium: 16px;
  --button-large: 24px;
    
  /* padding size */
   --padding-small: 8px;
  --padding-medium: 16px;
  --padding-large: 24px;

}

2. Establish Component Structure:

Design components should have a consistent structure and naming convention, and each component should contain its functionality and presentation (styling), accept props, and emit events as needed.

src/
|-- components/
|   |-- atoms/
|   |   |-- Button/
|   |   |   |-- Button.tsx
|   |   |   |-- Button.css
|   |-- molecules/
|   |   |-- SearchBar/
|   |   |   |-- SearchBar.tsx
|   |   |   |-- SearchBar.css
|   |-- organisms/
|   |   |-- Header/
|   |   |   |-- Header.tsx
|   |   |   |-- Header.css
|   |-- templates/
|   |   |-- MainTemplate/
|   |   |   |-- MainTemplate.tsx
|   |   |   |-- MainTemplate.css
|   |-- pages/
|   |   |-- HomePage/
|   |   |   |-- HomePage.tsx
|   |   |   |-- HomePage.css

The directory structure above represents a project organized according to atomic design principles. Consistency is achieved through the categorization and hierarchy of components within distinct folders.

3. Implement Atomic Design Principles

  1. Button Atom
/* Button.tsx */
import React, { FC, ComponentProps } from 'react';
import './Button.css';

type ButtonProps = ComponentProps<'button'> & {
  className?:
    | 'primary'
    | 'secondary'
    | 'danger'
    | 'info'
    | 'warning'
    | 'success'
    | string;
  size?: 'small' | 'medium' | 'large' | 'medium';
  children: React.ReactNode;
};

const Button: FC<ButtonProps> = ({
  size = 'medium',
  children,
  className,
  ...rest
}) => {
  return (
    <button className={`button ${className} ${size}`} {...rest}>
      {children}
    </button>
  );
};

export default Button;

Explanation:

In the button component above,

  • The Button component accepts several props, including type, size, onClick, and children
  • size determines the font size of the button text,
  • className determines the background color and text color of the button.
  • children is a required prop that represents the content inside the button, which can be text, icons, or other React nodes.
  • ...rest syntax collects all the remaining props not explicitly defined into a variable named rest.

<aside> 💡 Within the <button> element's JSX, the rest variable is an object containing all these remaining props and the spread operator ({...rest}) is used to spread all the collected props onto the <button> element

</aside>

For the button styling,


/* Button.css */
.button {
  padding: var(--button-medium) var(--button-large);
  font-size: var(--button-medium);
  border: none;
  border-radius: 4px;
  cursor: pointer;
  outline: none;
  transition: opacity 0.2s ease-in-out;
}

/* Primary Button Styles */
.primary {
  background-color: var(--primary);
  color: var(--white);
}

/* Secondary Button Styles */
.secondary {
  background-color: var(--secondary);
  color: var(--white);
}

/* Danger Button Styles */
.danger {
  background-color: var(--danger);
  color: var(--white);
}

/* Info Button Styles */
.info {
  background-color: var(--info);
  color: var(--white);
}

/* Warning Button Styles */
.warning {
  background-color: var(--warning);
  color: var(--white);
}

/* Success Button Styles */
.success {
  background-color: var(--success);
  color: var(--white);
}

/* Size Variants */
.small {
  padding: var(--button-small) var(--button-medium);
  font-size: var(--button-small);
}

.medium {
  /* Default styles applied */
}

.large {
  padding: var(--button-medium) var(--button-large);
  font-size: var(--button-large);
}

/* Hover State */
.button:hover {
  opacity: 0.8;
}
/*Disabled state*/
.button:disabled{
  cursor: not-allowed;
}

From this point onward, creating a button of any type or size is as simple as importing the Button component and specifying the desired type and size and any additional props allowed on HTML buttons.

import Button from '/path to Button component'
const handleClick = () => {
console.log(click)
}

const App = () => {
return (
	<main>
		{/* A large sized primary button */}
		<Button type="primary" size="large" onClick={handleButtonClick} >
            Primary Button
     </Button>
          
  	{/* A medium sized secondary button */}
		<Button
         className="secondary"
	       size="large"
         disabled={true} //Disable the button
         onClick={handleButtonClick}
         aria-label="Secondary Button" // Attributes for accessibility
          >
    
    {/* A small sized danger button */}
    <Button type="danger" size="small" onClick={handleButtonClick}>
            Danger Button
     </Button>
     
     {/* A medium sized info button */}
     <Button type="info" size="medium" onClick={handleButtonClick}>
            Info Button
      </Button>
      
      {/* A large sized warning button */}
      <Button type="warning" size="large" onClick={handleButtonClick}>
            Warning Button
      </Button>
      
      {/* A small sized success button */}
      <Button type="success" size="small" onClick={handleButtonClick}>
            Success Button
       </Button>
	
	</main>


b. Input Atom


/*Input.tsx*/
import React, { ChangeEvent, ComponentProps } from 'react';
import './Input.css';

type InputProps = Omit<ComponentProps<'input'>, 'onChange'> & {
  type?: 'text' | 'password' | 'email';
  className?: 'small' | 'medium' | 'large';
  value: string;
  onChange: (value: string) => void; 
  placeholder?: string;
  isValid?: boolean;
  label?: string;
};

const Input: React.FC<InputProps> = ({
  type = 'text',
  className = 'medium',
  value,
  onChange,
  placeholder,
  isValid = true,
  label,
  ...rest
}) => {
  let classNames = `input ${className}`;
  if (!isValid) {
    classNames += ' error';
  } else {
    classNames += ' valid';
  }

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    onChange(e.target.value);
  };

  return (
    <div className="input-container">
      {label && <label className="input-label">{label}</label>}
      <input
        type={type}
        className={classNames}
        value={value}
        onChange={handleChange}
        placeholder={placeholder}
        {...rest}
      />
    </div>
  );
};

export default Input;

Explanation:

In the Input component above,

  • The Input component accepts several props, including type, size, value, onChange, and placeholder.
  • The onChange is an event, and it specifies the function to call when the input value changes, invoking the onChange prop function with the updated value.
  • The className prop determines the padding and font size of the input based on the specified size (small, medium, large).
  • The input element (<input>) is wrapped inside a div with the class input-container, allowing for additional styling and structure.
  • The ...rest syntax collects all the remaining props not explicitly defined in the atom into a variable named rest.

For the Input styling,

/* Input.css */

.input {
  padding: var(--padding-medium);
  font-size: var(--font-size-base);
  border: 1px solid #ccc;
  border-radius: 4px;
  width: 100%;
  box-sizing: border-box;
  transition: border-color 0.2s ease-in-out;
}
.input-container {
  margin-bottom: 10px; 
}
.input-label {
  font-size: var(--font-size-small);
  font-weight: var(--font-weight-bold);
  margin-bottom: 8px;
  display: block;
  color: var(--black);
}
.input.small {
  padding: var(--padding-small);
}

.input.large {
  padding: var(--padding-large);
}

.input:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 5px var(--primary);
}

.input:hover {
  border-color: var(--primary);
}

.input.error {
  border-color: var(--danger);
  box-shadow: 0 0 5px var(--danger);
}

.input.valid {
  border-color: var(--success);
  box-shadow: 0 0 5px var(--success);
}

4. Separation of Concerns

Separate concerns between structure (HTML), styles (CSS), and behavior (JavaScript/TypeScript):

  • Place markup and structure in .tsx files.
  • Define styles in separate .css or .scss files.
  • Use CSS Modules, Styled Components, or other styling solutions to keep styles within components.

You can find the completed code on Stackblitz.

The Benefits of a Design System Approach

By adopting a design system approach:

  1. Consistency: You ensure a cohesive user experience by having a single source of truth for your UI components.

  2. Efficiency: You reduce development time by leveraging components that have been pre-built, tested, and documented.

  3. Scalability: You can now easily create new variations of components as your project evolves, without complications to your code base.

  4. Collaboration: You improve communication and alignment between designers and developers by speaking a common visual language.


Conclusion

As a front-end developer, embracing a design system approach can significantly transform your development process and the quality of your final product while also improving your code structure.

Here are three popular open-source design system kits to elevate your front-end applications:

1. Material-UI

  • Website: Material-UI
  • GitHub Repository: Material-UI GitHub
  • Description: Material-UI is a popular React UI framework that implements Google's Material Design principles. It provides a comprehensive set of ready-to-use components such as buttons, inputs, cards, and more.

2. Ant Design

  • Website: Ant Design
  • GitHub Repository: Ant Design GitHub
  • Description: Ant Design is a design system and React UI library developed by Ant Financial. It offers a wide range of high-quality components and design patterns, following the principles of Ant Design's comprehensive design language.

3. Bootstrap

Website: Bootstrap

GitHub Repository: Bootstrap GitHub

Description: Bootstrap is one of the most popular CSS frameworks for building responsive and mobile-first web projects. It provides a collection of pre-styled components (e.g., buttons, forms, navbars) and utilities (e.g., grid system, typography) that streamline front-end development.



References