I’ve learned a lot about how Vue.js works in the last 12 months and I’m confident in using it now for things like quickly prototyping interfaces or doing some exploration work to see how UI elements work together etc. I’ve also used it for the interactive areas in this blog alongside the Astro.js framework to get the best of both worlds.
I’ve done enough of these tasks to have worked out a base set of components that work for me. Primarily using PrimeVue as a starting point, which is a collection of pre-made accessible components which can be uses as-is or built upon to create customised versions to use in projects. This time I decided that to learn a bit more I’d create my own button from scratch, implementing the features I’d need, and maintaining accessibility also.
Spec
General:
- Accessible to WCAG 2.2
- Multiple styles (Ghost, Solid, Outline)
- Compatible with Tailwind
Icon:
- Accept an icon (and gracefully handle having no icon)
- Allow the icon to be placed on the left or right of the label
The button component is set up to receive the following props: label
which is the text for the button, ariaLabel
which works in tandem with the label, if it’s not present to ensure accessibility, A trailing
prop is passed when the icon needs to be after the label rather than the default before.
Finally, there is a validated style
string which allows me to pass the specific button style to allow me to use the button in a variety of situations. I can pass ghost, solid or outline. If nothing is passed then the style will default to solid.
Usage
<Button label="Press me" type="solid" trailing />
<template #icon>
{{ Icon }}
</template>
</Button>
I just find it super cool that all you have to do to get these different styles is change the type
variable to one of the above, and it’s changed!
Complete Component
For convenience here is the code for the complete component.
<script setup>
const props = defineProps({
label: String,
ariaLabel: String,
trailing: Boolean,
type: {
type: String,
required: false,
default: 'text', // Default style
validator: (value) => ['ghost', 'solid', 'outline', 'text'].includes(value), // Ensure valid styles
},
})
</script>
<template>
<button :name="ariaLabel || label" tabindex="0" role="button"
class="flex gap-1 items-center px-2 py-1 rounded-sm transition" :class="{
'flex-row-reverse': trailing,
'bg-blue-700 hover:bg-blue-600 text-white': type === 'solid', // Solid style
'bg-slate-200 hover:bg-slate-300 text-slate-700': type === 'ghost', // Ghost style
'bg-transparent hover:bg-slate-2 text-slate-800': type === 'text', // Ghost style
'border border-blue-700 text-blue-700': type === 'outline' // Outline style
}" :aria-label="ariaLabel || label" :title="ariaLabel || label">
<div v-if="$slots.icon">
<slot name="icon"></slot>
</div>
<span v-if="label">{{ label }}</span>
</button>
</template>
There are of course many different ways you could set this up, and I’ve given a very simple example. What I’ve done in previous projects is built more functionality into components as I’ve gone along, this is mainly to ensure I can easily change things like text size, colour, style in a single place, and ensure that the UI remains consistent and clear to those using it.