Skip to main content
Use the shadcn-vue Spinner component inside buttons to show loading states during async operations.

Component Locations

services/frontend/src/components/ui/button/
├── Button.vue      # Standard shadcn-vue button
└── index.ts        # Button variants and exports

services/frontend/src/components/ui/spinner/
├── Spinner.vue     # Animated loading spinner
└── index.ts        # Spinner exports

Usage

Import both Button and Spinner, then conditionally render the Spinner inside the button:
<script setup lang="ts">
import { ref } from 'vue'
import { Button } from '@/components/ui/button'
import { Spinner } from '@/components/ui/spinner'
import { toast } from 'vue-sonner'

const isSubmitting = ref(false)

const handleSubmit = async () => {
  isSubmitting.value = true
  try {
    await saveData()
    toast.success('Saved successfully')
  } catch (error) {
    toast.error('Save failed', { description: error.message })
  } finally {
    isSubmitting.value = false
  }
}
</script>

<template>
  <Button
    :disabled="isSubmitting"
    @click="handleSubmit"
  >
    <Spinner v-if="isSubmitting" class="mr-2" />
    Save Changes
  </Button>
</template>

Pattern

The loading button pattern consists of three parts:
  1. Disable the button - Add :disabled="isLoading" to prevent double clicks
  2. Show the spinner - Add <Spinner v-if="isLoading" class="mr-2" /> before the text
  3. Keep the text visible - The button text remains visible during loading

Spinner Customization

The Spinner component accepts a class prop for styling:
<!-- Smaller spinner -->
<Spinner v-if="isLoading" class="mr-2 size-3" />

<!-- Different color -->
<Spinner v-if="isLoading" class="mr-2 text-white" />

Examples

Form Submit Button

<Button :disabled="isSubmitting" @click="handleSubmit">
  <Spinner v-if="isSubmitting" class="mr-2" />
  {{ isSubmitting ? 'Saving...' : 'Save Changes' }}
</Button>

Delete Button

<Button
  variant="destructive"
  :disabled="isDeleting"
  @click="handleDelete"
>
  <Spinner v-if="isDeleting" class="mr-2" />
  Delete
</Button>

With Validation

<Button
  :disabled="!isFormValid || isSubmitting"
  @click="handleSubmit"
>
  <Spinner v-if="isSubmitting" class="mr-2" />
  Submit
</Button>