All articles

From Keyboard Shredder to Frontend Craftsman: The Guide to Mastering Reusable Components in Vue

Vue · 25 Oct 2026 · Michael Quintero

Reusable Components in Vue: less copy/paste, more elegance

We’ve all fallen into the trap. You need a button similar to the one you already made. What do you do? Ctrl + C, Ctrl + V. A few days later, you need another one. Ctrl + C, Ctrl + V. Suddenly, you’ve got ten identical buttons, and when the designer asks for a color change, you realize you have to update ten files.

Copy-paste is the enemy of scalability. The solution is reusable components. Today I’ll show you how to create smart, flexible components that will save you time and keep your code clean and professional.

The three pillars of a reusable component

For a component to be truly reusable, it must be flexible. And for that, you need to master three key concepts: Props, Slots and Emits.

1. Props: Flexibility is key

Props are like the "arguments" you pass to a component. They allow you to modify its behavior or appearance.

Example: A flexible button

<!-- BaseButton.vue -->
<script setup>
import { computed } from 'vue'

const props = defineProps({
  label: { type: String, required: true },
  variant: {
    type: String,
    default: 'primary',
    validator: (value) => ['primary', 'secondary', 'danger'].includes(value),
  },
  size: { type: String, default: 'md' },
  type: { type: String, default: 'button' },
  disabled: { type: Boolean, default: false },
})

const variants = {
  primary: 'bg-blue-600 text-white hover:bg-blue-700',
  secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
  danger: 'bg-red-600 text-white hover:bg-red-700',
}

const sizes = {
  sm: 'px-3 py-1.5 text-sm',
  md: 'px-4 py-2',
  lg: 'px-6 py-3 text-lg',
}

const classes = computed(() => [
  'rounded-lg font-semibold transition-colors',
  variants[props.variant],
  sizes[props.size],
  props.disabled && 'cursor-not-allowed opacity-50',
])
</script>

<template>
  <button :type="type" :class="classes" :disabled="disabled">
    {{ label }}
  </button>
</template>

Usage:

<script setup>
import BaseButton from '@/components/BaseButton.vue'

const save = () => console.log('Saved!')
</script>

<template>
  <BaseButton label="Save" @click="save" />
  <BaseButton label="Cancel" variant="secondary" size="sm" />
  <BaseButton label="Delete account" variant="danger" size="lg" disabled />
</template>

2. Slots: To inject content

Slots are placeholders in a component where the parent can "inject" its own content.

Example: A generic card

<!-- BaseCard.vue -->
<template>
  <article class="overflow-hidden rounded-xl bg-white shadow-md">
    <!-- Named slot: only rendered if the parent fills it -->
    <header v-if="$slots.header" class="border-b px-6 py-4 font-bold">
      <slot name="header" />
    </header>

    <!-- Default slot, with fallback content -->
    <div class="px-6 py-4">
      <slot>Nothing to show yet.</slot>
    </div>

    <footer v-if="$slots.footer" class="bg-gray-50 px-6 py-4">
      <slot name="footer" />
    </footer>
  </article>
</template>

Usage:

<template>
  <BaseCard>
    <template #header>Pro plan</template>

    <p>Unlimited projects, priority support and a custom domain.</p>

    <template #footer>
      <BaseButton label="Choose plan" />
    </template>
  </BaseCard>

  <!-- With no content, the card shows its fallback text -->
  <BaseCard />
</template>

3. Emits: Talking to the parent

Emits allow a child component to communicate with its parent. It’s a way of saying: "Hey, something happened here!".

Example: A form that emits an event

<!-- NewsletterForm.vue -->
<script setup>
import { ref } from 'vue'

const emit = defineEmits({
  // Validate the payload: the event only fires with a valid email
  subscribe: (email) => email.includes('@'),
})

const email = ref('')

const submit = () => {
  emit('subscribe', email.value.trim())
  email.value = ''
}
</script>

<template>
  <form class="flex gap-2" @submit.prevent="submit">
    <label for="newsletter-email" class="sr-only">Email</label>
    <input
      id="newsletter-email"
      v-model="email"
      type="email"
      required
      placeholder="you@example.com"
      class="flex-1 rounded-lg border px-4 py-2"
    />
    <BaseButton type="submit" label="Subscribe" />
  </form>
</template>

Usage in parent:

<script setup>
import { ref } from 'vue'
import NewsletterForm from '@/components/NewsletterForm.vue'

const message = ref('')

// The child only says "someone subscribed"; the parent decides what to do
const handleSubscribe = (email) => {
  message.value = `Thanks! We'll write to ${email}.`
}
</script>

<template>
  <NewsletterForm @subscribe="handleSubscribe" />
  <p v-if="message" class="mt-2 text-green-700">{{ message }}</p>
</template>

Conclusion

Building reusable components is not just a good habit, it’s a mindset that transforms you from a simple "coder" into an "interface architect". Start thinking of your code as a set of well-designed Lego pieces, and you’ll see your development speed and code quality skyrocket.

I write these articles while designing and building interfaces for clients. If you want something like this in your project, see my frontend design and development services or how I rebuilt the Clínica de Urgencias Bucaramanga website.