Skip to content

Design Kit Component Design

Components in the Design Kit should follow a structured approach to ensure consistency and integration with our existing design system.

Components should be designed to be as reusable as possible, and should be implemented in a way that allows for easy customization and extension.

For that reason, we have a set of guidelines to follow when implementing a new component.

Props

We should aim to have props that are easy to understand and use, and that can work in a variety of contexts.

Props should be simple and focused, and should not be overly specific to a particular use case.

We should aim to have a minimal number of props and slots, and should avoid having props that are dependent on each other.

This means that we would favor having a single prop that can be used to customize the component in a variety of ways, rather than having multiple props that are dependent on each other.

Example of a simple and focused props which can work together and doesn't overlap

<c-button variant="primary">Button Primary</c-button>
<c-button variant="secondary">Button Seconday</c-button>
<c-button variant="ghost" with-arrow>Button Ghost with an Arrow</c-button>

Avoid overly specific props which can overlap and can conflict with each other

<c-button primary>Button Primary</c-button>
<c-button secondary>Button Seconday</c-button>
<c-button ghost with-arrow>Button Ghost with an Arrow</c-button>

Avoid prop collisions and dependencies by design

<c-button primary ghost>Button with a Collision</c-button>

Presentational

Components should be presentational.

A presentational component is a component that is focused on the presentation of data and behavior. It is not concerned with managing state or complex logic.

This means that your component should not interact with any external state, should not have any side effects, listen to events, or make any API calls.

Stateless

Components should be stateless.

Ensure that your component is not dependent on any external state or context, and that it can be easily customized and extended.

This means that your component should not have a data property, and should not use this to access any state. Instead, use props to pass data into your component.

Example of a stateless component:

<c-toggle :value="checked" />

As you can see, the c-toggle component is stateless, and is dependent on the checked prop to determine its state.

Prop :value is used to follow Vue's v-model best practice. For more information on how to use v-model in your component, please refer to the Vue documentation.

Communication

Utilize emits and props for component communication. This keeps the component's implementation details abstracted, promoting flexibility.

Example of a component emitting an event:

<c-toggle @input="toggleChecked" />

As you can see, the c-toggle component emits an input event when the state changes.

Event @input is used to follow Vue's v-model best practice. For more information on how to use v-model in your component, please refer to the Vue documentation.

Storybook

Write stories for your component to document all states and variations.

Include a story for each state and variation of your component, and use the argTypes parameter to document the various props and slots and the args parameter to set the default values for the props and slots.

Use actions to simulate user interactions and events.

Example of a story for a component:

import CButton, { variants } from './CButton.vue';

export default {
    // Add the component to the Design Kit category in Storybook
    title: 'Design Kit/Atoms/Buttons/Button',

    // Add the component to the story
    component: CButton,

    // Add the autodocs tag to generate documentation automatically
    tags: ['autodocs'],

    // Add the argTypes to provide controls and documentation for the props and slots
    argTypes: {
        variant: {
            control: {
                type: 'select',
                options: Object.values(variants)
            },
        },
        withArrow: {
            control: {
                type: 'boolean',
            },
        },
        click: {
            action: 'clicked',
        },

        // Slots (please keep this comment)
        default: {
            type: 'string',
            description: 'Default slot',
        },
    },

    // Add the args to set the default values for the props and slots
    args: {
        variant: 'primary',
        withArrow: false,
        // Slots (please keep this comment)
        default: 'Button',
    },

    // Add the render function to provide a template for the stories
    render: (args) => ({
        components: { CButton },
        props: Object.keys(args),
        template: `
            <c-button
                :variant="variant"
                :with-arrow="withArrow"
                @click="click"
            >{{ $props.default }}</c-button>
        `,
    }),
};

// Use Component Story Format (CSF) to export each state and variation
export const Primary = {
    args: {
        variant: 'primary',
        withArrow: false,
    },
};

export const PrimaryWithArrow = {
    args: {
        variant: 'primary',
        withArrow: true,
    },
};

export const Secondary = {
    args: {
        variant: 'secondary',
        withArrow: false,
    },
};

export const SecondaryWithArrow = {
    args: {
        variant: 'secondary',
        withArrow: true,
    },
};

Tests

Write Jest Unit Tests to ensure your component functions as expected.

Unit tests should cover the various states and variations of your component, and should test its behavior and functionality.

This ensures that your component works as expected, and that it can be easily maintained and extended.

The Unit Tests should complement the stories in Storybook, and should cover all states and variations.

Example of a unit test for a component:

import { mount } from '@vue/test-utils';

import CButton from './CButton.vue';

describe('CButton', () => {
    it('renders a button with the correct label', () => {
        const wrapper = mount(CButton, {
            props: {
                label: 'Primary',
            },
        });

        expect(wrapper.text()).toBe('Primary');
    });
});

Container Components

If your component requires state or complex logic, consider creating a container component to manage this complexity.

A container component is a component that is responsible for managing the state and logic of a child component.

It is a common pattern in Vue applications, and it helps to keep the child component focused on its presentation and behavior.

Example of a container component managing the state and logic of a child component:

<template>
    <c-toggle v-model="checked" />
</template>

<script>
export default {
    name: 'CToggleContainer',

    data() {
        return {
            // We default the checked state to false.
            // In a real-world scenario, you'd likely have a loading or a SSR state.
            checked: false,
        };
    },

    mounted() {
        // Fetch the state from the server when the component is mounted
        this.fetchToggleState();
    },

    watch: {
        // Watch for changes to the checked state
        checked(newValue, oldValue) {
            // v-model is optimistic by it's nature, so we need to update the state on the server
            // and revert the change if the API call fails.
            if (newValue !== oldValue) {
                this.updateToggleState(newValue);
            }
        },
    },

    methods: {
        fetchToggleState() {
            // Fetch the state from the server
            axios.get('/api/toggle').then((response) => {
                // We expect the response to have a boolean isChecked property.
                // In a real-world scenario, you'd want to handle errors and loading states.
                this.checked = response.data.isChecked;
            });
        },
        updateToggleState(newState) {
            // Update the state on the server
            axios.post('/api/toggle', { isChecked: newState })
                .error((error) => {
                    // Undo the change if the API call fails
                    this.checked = !newState;
                });
        },
    },
};
</script>

In this example, the CToggleContainer component is responsible for managing the state and logic of the CToggle component.

This allows the CToggle component to focus on its presentation and behavior, and makes it easier to maintain and extend.

Additionally, it allows the CToggle component to be reused in other contexts, and makes it easier to test and reason about.

Finally, it allows the CToggleContainer to control the state and logic of the CToggle component, for example in the case of an API call error.

In this example we're using Vue's v-model best practice. For more information on how to use v-model in your component, please refer to the Vue documentation.

Exceptions

  • Typography is implemented using the @layer components functionality of TailwindCSS. This allows us to use the same typography classes in both HTML and Vue components.
  • Colours are implemented in tailwind.config.js and used in the components using the bg- and text- classes.

Next Steps

Now that you understand the structure and organization of the Design Kit, let's move on to learn about implementing a new component.

Implementing a New Component in Design Kit