Skip to main content

Overview

The Button component is built with reka-ui’s Primitive component and uses class-variance-authority (CVA) to manage variants. It supports multiple visual styles and sizes.

Basic Usage

<script setup lang="ts">
import { Button } from '@/components/ui/button'
</script>

<template>
  <Button>Click me</Button>
</template>

Button Variants

The button component supports 6 visual variants:
<template>
  <div class="flex flex-wrap gap-4">
    <Button variant="default">Default</Button>
    <Button variant="destructive">Destructive</Button>
    <Button variant="outline">Outline</Button>
    <Button variant="secondary">Secondary</Button>
    <Button variant="ghost">Ghost</Button>
    <Button variant="link">Link</Button>
  </div>
</template>

Default

Primary action button with filled background:
<Button variant="default">Save Changes</Button>

Destructive

For dangerous or destructive actions:
<Button variant="destructive">Delete Account</Button>
Example from resources/js/components/DeleteUser.vue:42-44:
<Button variant="destructive" data-test="delete-user-button">
  Delete account
</Button>

Outline

Button with border and transparent background:
<Button variant="outline">Cancel</Button>

Secondary

For secondary actions with muted styling:
<Button variant="secondary">Learn More</Button>

Ghost

Minimal button without background until hover:
<Button variant="ghost">View Details</Button>
Styled like a text link:
<Button variant="link">Read documentation</Button>

Button Sizes

Buttons come in 6 different sizes:
<template>
  <div class="flex items-center gap-4">
    <Button size="sm">Small</Button>
    <Button size="default">Default</Button>
    <Button size="lg">Large</Button>
  </div>
</template>

Size Variants

  • sm - Small button (h-8)
  • default - Default size (h-9)
  • lg - Large button (h-10)
  • icon - Square icon button (size-9)
  • icon-sm - Small icon button (size-8)
  • icon-lg - Large icon button (size-10)

Icon Buttons

Buttons optimized for displaying only an icon:
<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { X, Plus, Settings } from 'lucide-vue-next'
</script>

<template>
  <div class="flex gap-2">
    <Button size="icon-sm" variant="ghost">
      <X class="size-4" />
    </Button>
    
    <Button size="icon">
      <Plus class="size-4" />
    </Button>
    
    <Button size="icon-lg" variant="outline">
      <Settings class="size-5" />
    </Button>
  </div>
</template>

Buttons with Icons

Combine text with icons:
<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { Mail, Download, Trash2 } from 'lucide-vue-next'
</script>

<template>
  <div class="flex gap-4">
    <!-- Icon on the left -->
    <Button>
      <Mail class="size-4" />
      Send Email
    </Button>
    
    <!-- Icon on the right -->
    <Button variant="outline">
      Download
      <Download class="size-4" />
    </Button>
    
    <!-- Destructive with icon -->
    <Button variant="destructive">
      <Trash2 class="size-4" />
      Delete
    </Button>
  </div>
</template>
Icons are automatically sized using the [&_svg:not([class*='size-'])]:size-4 selector.

Loading State

Show loading indicator with disabled state:
<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { Spinner } from '@/components/ui/spinner'
import { ref } from 'vue'

const isLoading = ref(false)

const handleClick = async () => {
  isLoading.value = true
  // Perform async operation
  await new Promise(resolve => setTimeout(resolve, 2000))
  isLoading.value = false
}
</script>

<template>
  <Button :disabled="isLoading" @click="handleClick">
    <Spinner v-if="isLoading" />
    {{ isLoading ? 'Saving...' : 'Save' }}
  </Button>
</template>
Example from resources/js/pages/system/auth/Login.vue:89-98:
<Button
  type="submit"
  class="mt-4 w-full"
  :disabled="processing"
  data-test="login-button"
>
  <Spinner v-if="processing" />
  Log in
</Button>

Disabled State

Buttons can be disabled:
<template>
  <Button disabled>Disabled Button</Button>
</template>
Disabled buttons automatically receive:
  • pointer-events-none - Prevents clicks
  • opacity-50 - Visual indication of disabled state

As Child (Polymorphic)

Render the button as a different element:
<script setup lang="ts">
import { Button } from '@/components/ui/button'
</script>

<template>
  <!-- Render as a link -->
  <Button as-child>
    <a href="https://example.com" target="_blank">
      Visit Website
    </a>
  </Button>
</template>
This is useful for:
  • Router links
  • Anchor tags
  • Custom interactive elements

Full Width Button

Make button take full container width:
<template>
  <Button class="w-full">Full Width Button</Button>
</template>

Button Groups

Group related buttons:
<template>
  <div class="inline-flex rounded-md shadow-sm" role="group">
    <Button variant="outline" class="rounded-r-none">Left</Button>
    <Button variant="outline" class="rounded-none border-x-0">Center</Button>
    <Button variant="outline" class="rounded-l-none">Right</Button>
  </div>
</template>

Common Patterns

Form Submit Button

<script setup lang="ts">
import { useForm } from '@inertiajs/vue3'
import { Button } from '@/components/ui/button'

const form = useForm({
  // form data
})

const submit = () => {
  form.post('/endpoint')
}
</script>

<template>
  <form @submit.prevent="submit">
    <!-- form fields -->
    <Button type="submit" :disabled="form.processing">
      {{ form.processing ? 'Submitting...' : 'Submit' }}
    </Button>
  </form>
</template>

Dialog Trigger

<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { Dialog, DialogTrigger, DialogContent } from '@/components/ui/dialog'
</script>

<template>
  <Dialog>
    <DialogTrigger as-child>
      <Button>Open Dialog</Button>
    </DialogTrigger>
    <DialogContent>
      <!-- dialog content -->
    </DialogContent>
  </Dialog>
</template>

Confirmation Actions

<template>
  <div class="flex gap-2">
    <Button variant="outline">Cancel</Button>
    <Button variant="destructive">Confirm Delete</Button>
  </div>
</template>

Toggle Components

For toggle functionality, use the Toggle component:
<script setup lang="ts">
import { Toggle } from '@/components/ui/toggle'
import { Bold } from 'lucide-vue-next'
import { ref } from 'vue'

const isBold = ref(false)
</script>

<template>
  <Toggle v-model="isBold" aria-label="Toggle bold">
    <Bold class="size-4" />
  </Toggle>
</template>

Toggle Group

For mutually exclusive toggles:
<script setup lang="ts">
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
import { AlignLeft, AlignCenter, AlignRight } from 'lucide-vue-next'
import { ref } from 'vue'

const alignment = ref('left')
</script>

<template>
  <ToggleGroup v-model="alignment" type="single">
    <ToggleGroupItem value="left">
      <AlignLeft class="size-4" />
    </ToggleGroupItem>
    <ToggleGroupItem value="center">
      <AlignCenter class="size-4" />
    </ToggleGroupItem>
    <ToggleGroupItem value="right">
      <AlignRight class="size-4" />
    </ToggleGroupItem>
  </ToggleGroup>
</template>

Button Props

PropTypeDefaultDescription
variant'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link''default'Visual style variant
size'default' | 'sm' | 'lg' | 'icon' | 'icon-sm' | 'icon-lg''default'Size variant
asstring | Component'button'Element or component to render as
asChildbooleanfalseRender as child component
disabledbooleanfalseDisable button
classstring-Additional CSS classes

Accessibility

  • Use descriptive text or aria-label for icon-only buttons
  • Disabled buttons have pointer-events-none and reduced opacity
  • Focus states have visible ring indicators
  • Proper type attribute for form buttons (submit, button, reset)

Best Practices

  1. Use the right variant - destructive for dangerous actions, outline for secondary actions
  2. Add loading states - Prevent double-clicks during async operations
  3. Disable when needed - Disable buttons during form submission
  4. Icon accessibility - Always provide aria-label for icon-only buttons
  5. Consistent sizing - Use the same size for related buttons in a group

Build docs developers (and LLMs) love