• Front End Dev

Reusable & accessible button component

My method to build a time saving reusable Vue.js button component.

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>

Solid button rendered appearance

Solid button used for primary and important actions.

Ghost button rendered appearance

Ghost button style, useful for actions with less emphasis than the solid style suggests.

Outline button rendered appearance

Outline button style, similar usage scenario to a ghost button.

Text button rendered appearance

Text only button style, probably used for most of the buttons on this blog.

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.