Creating Components
Create a new component based on our design system's architecture. Each component is structured around three core files:
Component Structure
src/components/your-component/
├── your-component.ts # Types and props definitions
├── your-component.vue # Component template and setup
└── use-your-component.ts # Component logic and composables
Define Component Types (your-component.ts)
First, create the types file to define your component's props, emits, and types:
import type { PropType, ExtractPropTypes } from 'vue';
// Helper function for prop types (used in some components)
export const definePropType = <T>(val: unknown): PropType<T> => val as PropType<T>;
// Define constants for prop validation
const EXAMPLE_SIZES = ['small', 'medium', 'large'] as const;
// Define your props with JSDoc comments for documentation
export const componentPropTypes = {
/**
* @description Component size
*/
size: {
type: String as PropType<(typeof EXAMPLE_SIZES)[number]>,
validator: (value: (typeof EXAMPLE_SIZES)[number]) => EXAMPLE_SIZES.includes(value),
default: 'medium',
},
/**
* @description Whether the component is disabled
*/
disabled: {
type: Boolean,
default: false,
},
};
// Define your emits with type validation
export const componentEmitTypes = {
click: (evt: MouseEvent): evt is MouseEvent => evt instanceof MouseEvent,
};
// Export types for use in other files
export type ComponentPropTypes = ExtractPropTypes<typeof componentPropTypes>;
export type ComponentEmitTypes = typeof componentEmitTypes;
Create Component Template (your-component.vue)
Create the Vue component file that defines the template and connects the props and composable:
<template>
<div
ref="componentRef"
v-bind="componentProps"
:class="['spr-component__base', componentClasses]"
@click="handleClick"
>
<slot />
</div>
</template>
<script lang="ts" setup>
import { useComponent } from './use-your-component';
import { componentEmitTypes, componentPropTypes } from './your-component';
const props = defineProps(componentPropTypes);
const emit = defineEmits(componentEmitTypes);
const { componentRef, componentProps, componentClasses, handleClick } = useComponent(props, emit);
</script>
Note that our components typically use spr-
prefix for class names and follow BEM-like naming conventions for CSS classes.
Implement Component Logic (use-your-component.ts)
Create the composable that contains your component's logic. Use plain TypeScript to handle business logic with pure functions, while applying a layer of Vue reactivity on top.
import { computed, ref, type ComputedRef } from 'vue';
import { toRefs } from 'vue';
import classNames from 'classnames';
import type { SetupContext } from 'vue';
import type { ComponentEmitTypes, ComponentPropTypes } from './your-component';
export const useComponent = (props: ComponentPropTypes, emit: SetupContext<ComponentEmitTypes>['emit']) => {
// Extract reactive props
const { size, disabled } = toRefs(props);
const componentRef = ref<HTMLElement | null>(null);
// Compute component properties
const componentProps = computed(() => ({
disabled: disabled.value,
}));
// Compute CSS classes using classNames utility
const componentClasses = computed(() => {
return classNames({
'spr-size-small': size.value === 'small',
'spr-size-medium': size.value === 'medium',
'spr-size-large': size.value === 'large',
'spr-disabled': disabled.value,
});
});
// Handle events
const handleClick = (evt: MouseEvent) => {
if (disabled.value) {
evt.stopPropagation();
return;
}
emit('click', evt);
};
return {
componentRef,
componentProps,
componentClasses,
handleClick,
};
};
Component Registration
Components are automatically registered globally with the configured prefix. For example, if your component is named your-component.vue
, it will be registered as:
<spr-your-component size="medium" />
The prefix spr-
is the default for our design system components.
Example Usage
Here's how to use your newly created component:
<template>
<spr-your-component size="medium" :disabled="false" @click="handleClick"> Component Content </spr-your-component>
</template>
<script lang="ts" setup>
const handleClick = (evt: MouseEvent) => {
console.log('Component clicked!', evt);
};
</script>
Best Practices
Prop Validation
- Always describe prop types and validators to ensure proper use and avoid runtime issues.
- Use JSDoc comments for props to document their purpose.
- To properly handle undefined props, use default values when applicable.
Type Safety
- TypeScript provides type checking, improved IDE support, and safer development.
- Create interfaces for props, emitters, and slots to maintain consistency.
- Use
ExtractPropTypes
to ensure type safety for props.
Composables
- Extract reusable logic into composables (useXYZ) to improve modularity and maintainability.
- Use
toRefs
to maintain reactivity of individual props. - Keep the composables concentrated on a particular task.
- Use the
classNames
utility for complex conditional class logic.
Naming Conventions
- Component Files: File names should be kebab-case.vue.
- For props and emits, use camelCase.
- Types and Interfaces: To define types and interfaces, use PascalCase.
- CSS Classes: Use the
spr-
prefix and follow BEM-like naming when possible.
Styles
- Use Tailwind CSS utility classes with our
spr-
prefix. - Follow the design system color tokens (e.g.,
spr-background-color-base
,spr-text-color-strong
). - For interactive states, use proper hover/active classes.
- Avoid using inline styles unless absolutely necessary for dynamic behavior.
Accessibility (A11y)
- Ensure that components are accessible by default, following ARIA principles.
- Test screen readers and keyboard navigation.
- Use semantic HTML elements and include fallback behaviors for bespoke components.
- Add appropriate
aria-
attributes when needed.
Slot Management
- Use named slots to improve readability and extensibility.
- Set default content for slots to handle edge circumstances where no content is given.
Responsive Design
- Build responsive components with Tailwind CSS or media queries.
- Test components on various screen sizes and devices.
For more detailed examples, check out existing components like button, lozenge, or accordion in the source code.