Button

Displays a button or a component that looks like a button.

Examples

Basic

PropDefaultTypeDescription
label-stringThe label of the button.
Preview
Code

Variant

PropDefaultTypeDescription
btnsolid{variant}The variant of the button.
VariantDescription
solidThe default variant.
outlineThe outline variant.
softThe soft variant.
ghostThe ghost variant.
linkThe link variant.
textThe text variant.
~The unstyle or base variant
Preview
Code

Color

PropDefaultTypeDescription
btn{variant}-primary{variant}-{color}The color of the button.
Preview
Code
Dynamic colors:
Color with states:
Custom colors using utilities:
Static colors:

Size

PropDefaultTypeDescription
sizesmstringAllows you to change the size of the button.

🚀 Adjust input size freely using any size, breakpoints (e.g., sm:sm, xs:lg), or states (e.g., hover:lg, focus:3xl).

Preview
Code

Rounded

PropDefaultTypeDescription
roundedmdstringSet the button to have rounded corners.
Preview
Code

Square

PropDefaultTypeDescription
squaretrueboolean, stringSet the button to have the same width and height. If you provide empty value or true, it will provide 2.5em.
Preview
Code

Icon

PropDefaultTypeDescription
icon-booleanForce the label to be an icon.
leading-stringDisplay leading icon.
trailing-stringDisplay trailing icon.
Preview
Code
Icon buttons with and without square preset
Icon with states
Leading icon with label
Trailing icon with label
PropDefaultTypeDescription
to-stringThe link to navigate to.
Preview
Code

Block

PropDefaultTypeDescription
block-booleanSet the button to have full width.
Preview
Code

Disabled

PropDefaultTypeDescription
disabled-booleanSet the button to disabled.
Preview
Code

Loading

PropDefaultTypeDescription
loading-booleanSet the button to loading state.
loading-placementleadingleading, trailing, labelSet the loading icon placement.
Preview
Code

Slots

Default

NamePropsDescription
default-The button label.

Leading

NamePropsDescription
leading-The leading icon.
Preview
Code

Trailing

NamePropsDescription
trailing-The trailing icon.
Preview
Code

Loading

NamePropsDescription
loading-The loading icon.
Preview
Code

Presets

shortcuts/btn.ts
type BtnPrefix = 'btn'

export const staticBtn: Record<`${BtnPrefix}-${string}` | BtnPrefix, string> = {
  // config
  'btn-default-variant': 'btn-solid',
  'btn-loading-icon': 'i-loading',
  'btn-default-radius': 'rounded-md',

  // base
  'btn': 'btn-rectangle px-1em py-0.5em bg-transparent transition-colors text-0.875em leading-5 gap-x-0.5em rounded-md whitespace-nowrap inline-flex justify-center items-center btn-disabled font-medium cursor-pointer',
  'btn-disabled': 'disabled:n-disabled',
  'btn-label': '',
  'btn-icon-label': 'text-1em',
  'btn-leading': '-ml-0.14285714285714285em text-1em',
  'btn-trailing': '-mr-0.14285714285714285em text-1em',
  'btn-loading': 'animate-spin text-1em',
  'btn-rectangle': 'h-2.5em',
  'btn-square': 'w-2.5em h-2.5em',

  // options
  'btn-block': 'w-full',
  'btn-reverse': 'flex-row-reverse',

  // variants
  'btn-solid-white': 'bg-base text-base ring-1 ring-base ring-inset shadow-sm btn-focus hover:bg-muted',
  'btn-ghost-white': 'text-base btn-focus hover:bg-$c-gray-50',
  'btn-outline-white': 'text-base ring-1 ring-base ring-inset btn-focus hover:bg-$c-gray-50',

  'btn-solid-gray': 'bg-$c-gray-50 text-$c-gray-800 ring-1 ring-base ring-inset shadow-sm btn-focus hover:bg-$c-gray-100',
  'btn-ghost-gray': 'text-$c-gray-600 btn-focus hover:bg-$c-gray-100',
  'btn-soft-gray': 'text-$c-gray-600 bg-$c-gray-50 btn-focus hover:bg-$c-gray-100',
  'btn-outline-gray': 'text-muted hover:text-$c-gray-600 ring-1 ring-base ring-inset btn-focus hover:bg-$c-gray-50',
  'btn-link-gray': 'text-muted btn-focus hover:text-base hover:underline underline-offset-4',
  'btn-text-gray': 'text-$c-gray-600 btn-focus hover:text-$c-gray-900',

  'btn-solid-black': 'bg-inverted text-inverted shadow-sm btn-focus',
  'btn-link-black': 'text-base btn-focus hover:underline underline-offset-4',
  'btn-text-black': 'text-base btn-focus',
  'btn-soft-black': 'text-base bg-base btn-focus shadow-sm',

  'btn-text-muted': 'text-muted btn-focus hover:text-accent',
  'btn-link-muted': 'text-muted btn-focus hover:underline underline-offset-4',
  'btn-ghost-muted': 'text-accent hover:text-muted btn-focus hover:bg-muted',

  'btn-soft-accent': 'text-accent bg-accent btn-focus',
  'btn-text-accent': 'text-accent btn-focus',
  'btn-link-accent': 'text-accent btn-focus hover:underline underline-offset-4',
}

export const dynamicBtn: [RegExp, (params: RegExpExecArray) => string][] = [
  // base
  [/^btn-focus(-(\S+))?$/, ([, , c = 'primary']) => `focus-visible:outline-${c}-600 dark:focus-visible:outline-${c}-500 focus-visible:outline-2 focus-visible:outline-offset-2`],

  // variants
  [/^btn-solid(-(\S+))?$/, ([, , c = 'primary']) => `btn-focus-${c} text-inverted shadow-sm bg-${c}-600 hover:bg-${c}-500 dark:bg-${c}-500 dark:hover:bg-${c}-400`],
  [/^btn-text(-(\S+))?$/, ([, , c = 'primary']) => `btn-focus-${c} text-${c}-600 dark:text-${c}-500 hover:text-${c}-500 dark:hover:text-${c}-400`],
  [/^btn-outline(-(\S+))?$/, ([, , c = 'primary']) => `btn-focus-${c} text-${c}-500 dark:text-${c}-400 ring-1 ring-inset ring-${c}-500 dark:ring-${c}-400 hover:bg-${c}-50 dark:hover:bg-${c}-950`],
  [/^btn-soft(-(\S+))?$/, ([, , c = 'primary']) => `btn-focus-${c} text-${c}-600 dark:text-${c}-400 bg-${c}-50 dark:bg-${c}-950 hover:bg-${c}-100 dark:hover:bg-${c}-900`],
  [/^btn-ghost(-(\S+))?$/, ([, , c = 'primary']) => `btn-focus-${c} text-${c}-600 dark:text-${c}-400 hover:bg-${c}-100 dark:hover:bg-${c}-900`],
  [/^btn-link(-(\S+))?$/, ([, , c = 'primary']) => `btn-focus-${c} text-${c}-600 dark:text-${c}-500 hover:underline underline-offset-4`],
]

export const btn = [
  ...dynamicBtn,
  staticBtn,
]

Props

types/button.ts
import type { HTMLAttributes } from 'vue'
import type { RouteLocationRaw } from 'vue-router'

interface BaseExtensionProps {
  square?: HTMLAttributes['class']
  rounded?: HTMLAttributes['class']
  class?: HTMLAttributes['class']
  breadcrumbActive?: string
  breadcrumbInactive?: string
  paginationSelected?: string
  paginationUnselected?: string
  dropdownMenu?: string
  toggleOn?: string
  toggleOff?: string
}

export interface NButtonProps extends BaseExtensionProps {
  /**
   * Change the button type.
   *
   * @default 'button'
   */
  type?: 'button' | 'submit' | 'reset'
  /**
   * Change the loading placement of the button.
   *
   * @default 'leading'
   */
  loadingPlacement?: 'leading' | 'trailing' | 'label'
  /**
   * Convert `label` prop to icon component.
   *
   * @default false
   * @example
   * icon
   * label="i-heroicons-information-circle"
   */
  icon?: boolean
  /**
   * Disable the button.
   *
   * @default false
   */
  disabled?: boolean
  /**
   * Swap the position of the leading and trailing icons.
   *
   * @default false
   */
  reverse?: boolean
  /**
   * Show loading state on button
   * @default false
   */
  loading?: boolean
  /**
   * Make the button full width.
   *
   * @default false
   */
  block?: boolean
  /**
   * Change the button tag to `NuxtLink` component,
   * This allows you to use `NuxtLink` available props.
   *
   * @see https://nuxt.com/docs/api/components/nuxt-link#props
   * @example
   * to="/"
   */
  to?: RouteLocationRaw
  /**
   * Add a label to the button.
   *
   * @example
   * label="Click me"
   */
  label?: string
  /**
   * Allows you to add `UnaUI` button preset properties,
   * Think of it as a shortcut for adding options or variants to the preset if available.
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/button.ts
   * @example
   * btn="solid-green block square"
   */
  btn?: string
  /**
   * Add leading icon the button,
   * This also allows you to add utility classes to the icon.
   *
   * @example
   * leading="i-heroicons-information-circle text-green-500 dark:text-green-400 text-2xl"
   */
  leading?: string
  /**
   * Add trailing icon the button.
   * This also allows you to add utility classes to the icon.
   *
   * @example
   * trailing="i-heroicons-information-circle text-green-500 dark:text-green-400 text-2xl"
   */
  trailing?: string
  /**
   * Allows you to change the size of the input.
   *
   * @default sm
   *
   * @example
   * size="sm" | size="2cm" | size="2rem" | size="2px"
   */
  size?: string

  /**
   * `UnaUI` preset configuration
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/button.ts
   */
  una?: {
    // base
    btnDefaultVariant?: string
    btn?: string
    btnLabel?: string
    btnIconLabel?: string
    btnLoading?: string
    btnTrailing?: string
    btnLeading?: string

    // icon
    btnLoadingIcon?: string
  }
}

Components

Button.vue
<script setup lang="ts">
import type { NButtonProps } from '../../types'
import { createReusableTemplate } from '@vueuse/core'
import { computed } from 'vue'
import { cn } from '../../utils'
import NIcon from '../elements/Icon.vue'
import NLink from '../elements/Link.vue'

const props = withDefaults(defineProps<NButtonProps>(), {
  type: 'button',
  loadingPlacement: 'leading',
  square: false,
  una: () => ({
    btnDefaultVariant: 'btn-default-variant',
  }),
})

const mergeVariants = computed(() => {
  return {
    'btn': props.btn,
    'breadcrumb-active': props.breadcrumbActive,
    'breadcrumb-inactive': props.breadcrumbInactive,
    'pagination-selected': props.paginationSelected,
    'pagination-unselected': props.paginationUnselected,
    'dropdown-menu': props.dropdownMenu,
    'toggle-on': props.toggleOn,
    'toggle-off': props.toggleOff,
  }
})

const loadingPlacement = computed(() => props.loadingPlacement === 'leading' && props.icon ? 'label' : props.loadingPlacement)

const btnVariants = ['solid', 'outline', 'soft', 'ghost', 'link', 'text'] as const

const hasVariant = computed(() =>
  Object.values(mergeVariants.value).some(variantList =>
    btnVariants.some(variant => variantList?.includes(variant)),
  ),
)

const isBaseVariant = computed(() =>
  Object.values(mergeVariants.value).some(variantList =>
    variantList?.includes('~'),
  ),
)

const [DefineTemplate, ReuseTemplate] = createReusableTemplate()
</script>

<template>
  <Component
    :is="to ? NLink : 'button'"
    :to="to"
    :type="to ? null : type"
    :class="cn(
      (square === '' || square === true) && 'btn-square',
      block && 'btn-block',
      !rounded && 'btn-default-radius',
      !hasVariant && !isBaseVariant ? una?.btnDefaultVariant : null,
      reverse && 'btn-reverse',
      'btn',
      una?.btn,
      props.class,
    )"
    :disabled="to ? null : disabled || loading"
    :aria-label="icon ? label : null"
    :rounded
    :size
    :square
    v-bind="mergeVariants"
  >
    <DefineTemplate v-if="loading">
      <slot name="loading">
        <NIcon
          :name="una?.btnLoadingIcon ?? 'btn-loading-icon'"
          :class="una?.btnLoading"
          btn="loading"
        />
      </slot>
    </DefineTemplate>

    <ReuseTemplate v-if="loading && loadingPlacement === 'leading'" />
    <slot
      v-else
      name="leading"
    >
      <NIcon
        v-if="leading"
        :name="leading"
        :class="una?.btnLeading"
        btn="leading"
      />
    </slot>

    <ReuseTemplate v-if="loading && loadingPlacement === 'label'" />
    <slot v-else>
      <NIcon
        v-if="label && icon"
        :name="label"
        btn="icon-label"
        :class="una?.btnIconLabel"
      />
      <span
        v-if="!icon"
        btn="label"
        :class="una?.btnLabel"
      >
        {{ label }}
      </span>
    </slot>

    <ReuseTemplate v-if="loading && loadingPlacement === 'trailing'" />
    <slot
      v-else
      name="trailing"
    >
      <NIcon
        v-if="trailing"
        :name="trailing"
        btn="trailing"
        :class="una?.btnTrailing"
      />
    </slot>
  </Component>
</template>