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';
// Define constants for prop validation
const EXAMPLE_SIZES = ['small', 'medium', 'large'] as const;
// Define your props
export const componentPropTypes = {
size: {
type: String as PropType<(typeof EXAMPLE_SIZES)[number]>,
validator: (value: (typeof EXAMPLE_SIZES)[number]) => EXAMPLE_SIZES.includes(value),
default: 'medium',
},
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="['base-classes', componentClass]" @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, componentClass, handleClick } = useComponent(props, emit);
</script>
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 type { SetupContext } from 'vue';
import type { ComponentEmitTypes, ComponentPropTypes } from './your-component';
export const useComponent = (props: ComponentPropTypes, emit: SetupContext<ComponentEmitTypes>['emit']) => {
const componentRef = ref<HTMLElement | null>(null);
// Compute component properties
const componentProps = computed(() => ({
disabled: props.disabled,
}));
// Compute CSS classes
const componentClass = computed(() => {
return {
'size-small': props.size === 'small',
'size-medium': props.size === 'medium',
'size-large': props.size === 'large',
};
});
// Handle events
const handleClick = (evt: MouseEvent) => {
if (props.disabled) {
evt.stopPropagation();
return;
}
emit('click', evt);
};
return {
componentRef,
componentProps,
componentClass,
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 can be configured when installing the design system:
app.use(DesignSystem, { prefix: 'custom-' });
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.
- 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.
Composables
- Extract reusable logic into composables (useXYZ) to improve modularity and maintainability.
- Keep the composables concentrated on a particular task.
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 Pascal Case.
Scoped and Reusable Styles
- Avoid using inline styles unless absolutely necessary for dynamic behavior.
- Use Tailwind CSS to maintain design consistency between components.
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.
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 the button component implementation in the source code.