Skip to main content
Version: 3.x

Create a UI Component

In this guide, you will learn how to build a UI Component. We will do so by creating our own. We will use Storybook in the process because we think it is a good way to be productive for this task.

In Front-Commerce components are classified under two categories: the UI components available in the theme/components folder, and the Business components available in the theme/modules and theme/pages folders.

info

If you feel the need to understand why we went for this organization, feel free to refer to React components structure first.

As mentioned in the introduction, we will use Storybook in the process because it is our usual workflow when creating a UI Component. But if you don't need it or prefer to add your stories later, feel free to leave the parts mentioning Storybook later.

Front-Commerce’s core UI components follow the same principles and you could dive into the node_modules/@front-commerce/theme-chocolatine/theme/components folder to find examples into our source code.

But first, let's define what is an ideal UI Component.

The ideal UI Component

In Front-Commerce we call UI component any component that is:

  • Reusable in many contexts: if a component is used only once in the whole application, it might feel like it doesn't exist purely for UI purposes. The component most likely needs to be moved to the /app/modules folder . That's also the reason why we avoid to give names too close to its business use. For instance, we don't want to have a UI component that would be called ProductDescription. It would be better to go for a Description that would thus be reusable by a Category.
  • Focused on abstracting away UI concerns: the goal of UI components is to hide styles or DOM concerns from their parents. It may be hard to achieve sometimes, but it will greatly improve the parent component's readability. For instance, a UI component should not give the opportunity to pass a className in its props as it may lead to many style inconsistencies across the theme.

How to build a UI Component?

Alright, that's nice in theory, but how does it translate in practice? First, we’ll try to get a bit more tangible by creating a UI component for adding a Reinsurance Banner on a page similar to the following mockup.

The reinsurance banner that we will implement

Defining the components

First, let's split the mockup in several UI components following the Atomic Design Principles.

The various components that will make up our banner

  • app/theme/components/atoms/Typography/Heading (green): enforces consistent font sizes in our theme for any title
  • app/theme/components/atoms/Icon (purple): enforces icon sizes and accessibility guidelines
  • app/theme/components/molecules/IllustratedContent (red): displays some content illustrated by an image and aligns images consistently across the whole theme
  • app/theme/components/organisms/FeatureList (orange): manages a list of cards and inlines them, regardless of the device size.

As you can see, each UI component will take place in the app/theme/components folder. To better understand why, please refer to our React components structure documentation.

note

If you have trouble splitting your mockups, you can refer to Thinking in React in the official React documentation or to Brad Frost's book about Atomic Design. You may want to organize your code differently and that's perfectly fine. The way we splitted things here is one of many possible solutions. Such choices will depend on your project and your team. It may be a better idea to keep things simple. It's often easier to wait and see how it can be reused later.

We won't be able to detail each component here. We will focus on IllustratedContent instead. But keep in mind that any UI component in Front-Commerce will look similar to what we are going to build here.

Setup your dev environment

Before doing the actual work let's bootstrap our dev environment. To do so, you will need to create three files:

  • IllustratedContent.js : will bootstrap the actual component.

    app/theme/components/molecules/IllustratedContent/IllustratedContent.tsx
    const IllustratedContent = () => {
    return <div>Illustrated Content</div>;
    };

    export default IllustratedContent;
  • index.js : will proxy the IllustratedContent.js file in order to be able to do imports on the folder directly. See this blog post for more context about this pattern.

    app/theme/components/molecules/IllustratedContent/index.ts
    import IllustratedContent from "theme/components/molecules/IllustratedContent/IllustratedContent";

    export default IllustratedContent;
  • IllustratedContent.stories.tsx : will add a story to the Storybook of your application. This will serve as living documentation and will allow anyone to understand what is IllustratedContent used for and how to use it.

    app/theme/components/molecules/IllustratedContent/IllustratedContent.stories.tsx
    import type { Meta, StoryObj } from "@storybook/react";
    import IllustratedContent from "./IllustratedContent";

    const meta: Meta<typeof IllustratedContent> = {
    component: IllustratedContent,
    };

    export default meta;

    type Story = StoryObj<typeof IllustratedContent>;

    export const Default: Story = {
    render: () => <IllustratedContent />,
    };

Once you've added your component, you must restart the styleguide (npm run styleguide). And once it is up and running, you can view your new story in components > molecules > IllustratedContent.

Now that you've done that, you can edit the IllustratedContent component, save, and view changes live in your browser.

Learn more

Implement your component

Now that everything is ready to go, you can do the actual work and implement the component. In the case of the IllustratedContent, it would look something like this:

app/theme/components/molecules/IllustratedContent/IllustratedContent.tsx
import type { ReactNode } from "react";

interface IllustratedContentProps {
media: ReactNode;
children: ReactNode;
}

const IllustratedContent: React.FC<IllustratedContentProps> = ({
media,
children,
}) => {
return (
<div className="illustrated-content">
<div className="illustrated-content__media">{media}</div>
<div className="illustrated-content__content">{children}</div>
</div>
);
};

export default IllustratedContent;

Styling your component

note

We are using Sass (hence the .scss extension). We believe that it is easier for developers new to the React Ecosystem to remain with this well-known CSS preprocessor.

Create your stylesheet in the same folder as your component to keep your project organized and maintainable.

app/theme/components/molecules/IllustratedContent/IllustratedContent.scss
.illustrated-content {
display: flex;
flex-direction: column;
align-items: center;
}
.illustrated-content__media {
width: 30%;
max-width: 10em;
text-align: center;
}

And import it in your component.

app/theme/components/molecules/IllustratedContent/IllustratedContent.tsx
import type { ReactNode } from "react";

import "./IllustratedContent.scss";

interface IllustratedContentProps {
media: ReactNode;
children: ReactNode;
}

// Rest of the component
note

As a side note, we also use BEM convention for our CSS code base. It makes it easier to avoid naming conflicts by adding a tiny bit of code convention. However, for your custom components, feel free to code however you like. There is no obligation here.

Document usage of our component

If our component can have different usages, we should also add new stories along the default one.

app/theme/components/molecules/IllustratedContent/IllustratedContent.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import IllustratedContent from "./IllustratedContent";
import Icon from "theme/components/atoms/Icon";
import { H3 } from "theme/components/atoms/Typography/Heading";
import { BodyParagraph } from "theme/components/atoms/Typography/Body";
import Image from "theme/components/atoms/Image";

const meta: Meta<typeof IllustratedContent> = {
component: IllustratedContent,
};

export default meta;

type Story = StoryObj<typeof IllustratedContent>;

export const Default: Story = {
render: () => (
<IllustratedContent media={<Icon title="calendar" icon="calendar-full" />}>
<H3>Shipping within 48h</H3>
</IllustratedContent>
),
};

export const WithContent: Story = {
name: "With a lot of content",
render: () => (
<IllustratedContent media={<Icon title="calendar" icon="calendar-full" />}>
<H3>Shipping within 48h</H3>
<BodyParagraph>
We are using many delivery services to let you choose what is best for
you!
</BodyParagraph>
</IllustratedContent>
),
};

export const WithImage: Story = {
name: "With an image",
render: () => (
<IllustratedContent
media={<Image src="https://placehold.co/600x400" alt="Placeholder" />}
>
<H3>Shipping within 48h</H3>
</IllustratedContent>
),
};

It has many major benefits such as:

  • document edge cases
  • provide a test suite thanks to snapshot testing (Storyshots)
  • create a common discussion base for designers, product managers, marketers, etc.

Use the component

Once you are satisfied with your component, you can use it anywhere. In this case, the IllustratedContent was to be used in the Reinsurance Banner. Thus, this module's component would look like this:

import FeatureList from "theme/components/organisms/FeatureList";
import IllustratedContent from "theme/components/molecules/IllustratedContent";
import Icon from "theme/components/atoms/Icon";
import { H3 } from "theme/components/atoms/Typography/Heading";

const ReinsuranceBanner = () => (
<FeatureList appearance="default">
<IllustratedContent media={<Icon title="calendar" icon="calendar-full" />}>
<H3>Shipping within 48h</H3>
</IllustratedContent>
<IllustratedContent media={<Icon title="cash" icon="cash" />}>
<H3>Money back guarantee</H3>
</IllustratedContent>
<IllustratedContent media={<Icon title="lock" icon="lock" />}>
<H3>Secured Payment</H3>
</IllustratedContent>
</FeatureList>
);

export default ReinsuranceBanner;

As you can see, we did not use a relative import. This is because in Front-Commerce we have a few aliases that will let you import files without worrying about your current position in the folder structure.

In our case, if the ReinsuranceBanner was in app/theme/modules/ReinsuranceBanner, we don't have to import the IllustratedContent by using relative paths ../../components/molecules/IllustratedContent but we can remain with theme/components/molecules/IllustratedContent which is more explicit. This is possible for any file located in the folder app/theme of a module.