Skip to content

Design Library

The design core library creates reusable components used throughout our products (e.g., airs, integrates, Jira).

Introduction

The fluid attacks design components library is an open-source npm package written in TypeScript and built with React.

Public Oath

Contributing

Please read the contributing page first.

Design standards

As our products rapidly evolve, the user interface is constantly modified. Since the platform and website redesign in 2024, we encountered the need to create a robust and maintainable design library to hold all the basic elements used to construct digital interfaces.

For this purpose, we applied different known software standard principles:

  1. Atomic Design: a methodology for designing and building user interfaces by breaking them down into progressively complex components, similar to how atoms combine to form molecules in chemistry, allowing for reusable and consistent components across an application, with the core idea being to create a hierarchical structure of UI elements, starting from basic “atoms” and building up to more complex “molecules”, “organisms”, “templates”, and finally “pages”.
  2. KISS, “Keep It Simple, Stupid”: a software development guideline that encourages developers to keep their code and systems as simple as possible. The principle is based on the idea that the simplest solution is often the best, and unnecessary complexity should be avoided.
  3. DRY, “Don’t Repeat Yourself”: a software development principle that aims to reduce the amount of repetitive code and logic in a system. This can help improve code maintainability, reduce the potential for errors, and streamline the development process.
  4. YAGNI, “You Aren’t Gonna Need It”: a software development principle that encourages developers to only add features when they are required, rather than trying to predict future needs.
  5. Single Responsibility (SRP): a component should have only one specific, well-defined purpose or functionality, and should not be responsible for multiple unrelated tasks; essentially, a component should only have one reason to change, making it more modular, easier to understand, maintain, and test independently.

Getting Started

The components are stored in the common/design/src/components directory, each one must contain an index.(ts|tsx) file with the React reusable component and optional files such as:

  • types.ts: To specify the modifiable attributes of the component using types or interface.
  • styles.ts: To create the custom-styled components and different CSS styling props.
  • utils.ts: To add util functions, (e.g., filters, calculations, transformations).
  • index.stories.tsx: To add the storybook file to visualize and test the component.

Writing a component

All design components have a prototype in Figma with the anatomy specifics, interactions, and visual animations required. When working on a new component, the first question is: Can this be a variant of an existing component or is it brand new? As mentioned before, everything that can be reused to limit duplication is always preferred.

Afterward, you need to define the component’s properties or attributes. These are everything that can change (e.g., text, icons, user events, etc).

Here’s an example of a types.ts file:

/**
* Toggle button component props.
* @interface IToggleButtonProps
* @property {string} [defaultSelection] The option selected by default.
* @property {string[]} options The list options label.
* @property {Function} handleClick The function to handle the click event.
*/
interface IToggleButtonProps {
defaultSelection?: string;
options: string[];
handleClick: (selection: string) => void;
}
export type { IToggleButtonProps };

Key aspects of the example are:

  1. The documentation enclosed in /** */ is mandatory and it follows the structure of the JSDoc documentation generator. The rule is required by Eslint and it applies to interface only. With the intention to keep the documentation concise, it is required to:
    1. Add a description of the interface’s purpose.
    2. Use @interface to define the interface name.
    3. Use @extends in cases where the interface inherits external props.
    4. Use @property to define each property, following the structure:
      • @property {Type} property_name Short prop description.
  2. The name should be I<ComponentName>Props.

When defining a component’s properties, you should review the @core directory. Inside it, there are globally defined interfaces that can be use in individual components to extend and manage commonly needed properties.

import type { IBorderModifiable, IDisplayModifiable, ... } from "components/@core";
/**
* Button component props.
* @interface IButtonProps
* @extends IBorderModifiable
* @extends IDisplayModifiable
* @extends IMarginModifiable
* @extends IPaddingModifiable
...
*/

For these properties to apply correctly to the component, make sure to:

  1. Create the style using the @core base in the styles.ts file:

    import { BaseComponent } from "components/@core";
    const StyledContainer = styled(BaseComponent)`
    ...
    `;
  2. Create the react component using forwardRef function in the index.tsx file:

    import type { PropsWithChildren, Ref } from "react";
    import { forwardRef } from "react";
    import { StyledContainer } from "./styles";
    import type { IContainerProps } from "./types";
    const Container = forwardRef(function Container(
    {
    children,
    ...
    ...props
    }: Readonly<PropsWithChildren<IContainerProps>>,
    ref: Ref<HTMLDivElement>,
    ): JSX.Element {
    return (
    <StyledContainer
    ...
    ref={ref}
    {...props}
    >
    {children}
    </StyledContainer>
    );
    });

Styling and Theming

The styling of components is done using both Styled Components and Tailwind.

The storybook is a workshop page to showcase the UI components. It is a public domain and is the true source for keeping all digital products in sync with the design team of Fluid Attacks.

All components can be test locally using Storybook, Eslint and Stylelint, running the following jobs:

Terminal window
m . /common/design

The local storybook can run in localhost:6006 or any other specified port available.

Testing

Besides visually testing the components, Storybook also provides the packages @storybook/test and @storybook/addon-interactions that integrates the component stories with a testing environment powered using Jest. With this feature, we use the same story as a mock and can validate the components’ render and user interactions.

To create a test, inside the index.stories.tsx file create a play function, adding the expect and user events inside it.

import { expect, within } from "@storybook/test";
...
Default.play = async ({ canvasElement, step }): Promise<void> => {
const canvas = within(canvasElement);
await step("should render a Button component", async (): Promise<void> => {
await expect(
canvas.getByRole("button", { name: "Button text" }),
).toBeInTheDocument();
await expect(canvas.getByTestId("arrow-right-icon")).toBeInTheDocument();
});
};

This tests are run in production and local environment directly in the component story using the interactions addon.

Test-light Test-dark

Helpful resources