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 thebg-
andtext-
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.