Keyboard shortcut: Ctrl + K
Get started

Button

The button that triggers an action, and the reference from which the tone and variant tables of every other coloured component are taken. It renders a native <button>, or an <a> as soon as it is given an href.

Usage

vue
<script setup lang="ts">
import { VButton } from 'vectis-ui'
</script>

<template>
  <VButton>Save changes</VButton>
</template>

Examples

Variants and tones

variant offers four ways of painting the button, and tone three meanings: accent, neutral and danger.

vue
<script setup lang="ts">
import { VButton } from 'vectis-ui'

const variants = ['solid', 'soft', 'outline', 'ghost'] as const
const tones = ['accent', 'neutral', 'danger'] as const
</script>

<template>
  <div v-for="tone in tones" :key="tone" class="row">
    <VButton v-for="variant in variants" :key="variant" :variant="variant" :tone="tone">
      {{ variant }}
    </VButton>
  </div>
</template>

<style scoped>
.row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-3);
}
</style>

Elevated

elevated applies the shadow scale to whichever variant is in use. A ghost or outline button also gains a raised background.

vue
<script setup lang="ts">
import { VButton } from 'vectis-ui'

const variants = ['solid', 'soft', 'outline', 'ghost'] as const
</script>

<template>
  <div class="row">
    <VButton v-for="variant in variants" :key="variant" :variant="variant">
      {{ variant }}
    </VButton>
  </div>

  <div class="row">
    <VButton v-for="variant in variants" :key="variant" :variant="variant" elevated>
      {{ variant }}
    </VButton>
  </div>
</template>

<style scoped>
.row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-3);
}
</style>

Sizes

size sets the height: 24, 32, 40, 48 or 56 pixels. The type, the padding and the icons follow.

vue
<script setup lang="ts">
import { VButton } from 'vectis-ui'
</script>

<template>
  <VButton size="xs">Extra small</VButton>
  <VButton size="sm">Small</VButton>
  <VButton size="md">Medium</VButton>
  <VButton size="lg">Large</VButton>
  <VButton size="xl">Extra large</VButton>
</template>

Compact

compact takes 4px off the height, nothing else moving.

vue
<script setup lang="ts">
import { VButton } from 'vectis-ui'
</script>

<template>
  <VButton size="sm">Small</VButton>
  <VButton size="sm" compact>Small compact</VButton>
  <VButton size="md">Medium</VButton>
  <VButton size="md" compact>Medium compact</VButton>
  <VButton size="lg">Large</VButton>
  <VButton size="lg" compact>Large compact</VButton>
</template>

Full width

fullWidth stretches the button across the inline size of its parent and makes it block level.

vue
<script setup lang="ts">
import { VButton } from 'vectis-ui'
</script>

<template>
  <div class="column">
    <VButton>Save</VButton>
    <VButton variant="outline" tone="neutral">Cancel</VButton>
  </div>

  <div class="column">
    <VButton full-width>Save</VButton>
    <VButton full-width variant="outline" tone="neutral">Cancel</VButton>
  </div>
</template>

<style scoped>
/* Both columns are the same width and align their items to the start, so what widens
   the second pair is the prop and not the layout around it. */
.column {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: var(--vectis-space-3);
  inline-size: 220px;
}
</style>

With icons

iconStart and iconEnd put an icon on either side of the label, and iconFilled switches them to their filled form. The #start and #end slots take over when the content is more than an icon.

vue
<script setup lang="ts">
import { VButton } from 'vectis-ui'
import { arrow_right_alt as arrowRightAlt, notifications, search } from 'vectis-ui/icons'
</script>

<template>
  <VButton :icon-start="search">Search</VButton>
  <VButton :icon-end="arrowRightAlt">Next</VButton>
  <VButton :icon-start="search" :icon-end="arrowRightAlt">Search and go</VButton>
  <VButton :icon-start="notifications" variant="outline" tone="neutral">Notify me</VButton>
  <VButton :icon-start="notifications" icon-filled variant="outline" tone="neutral">
    Notifying
  </VButton>
</template>

Custom icons

Both icon props take an IconSource: one of the library icons, a name handed to the resolver your application installed, SVG path data, a component, or an image.

vue
<script setup lang="ts">
import { h } from 'vue'
import { VButton } from 'vectis-ui'
import { ICON_VIEW_BOX, cloud_upload as cloudUpload, code, search } from 'vectis-ui/icons'

import firefoxLogo from '~/assets/img/firefox-browser-svg.svg'

// A component icon, the shape an icon set such as Lucide ships: the contract is a single
// <svg> root. The drawing is borrowed from the library's own registry rather than redrawn.
const CodeIcon = () =>
  h('svg', { viewBox: ICON_VIEW_BOX, fill: 'currentColor' }, [h('path', { d: code.paths[0] })])
</script>

<template>
  <VButton :icon-start="search">A library icon</VButton>
  <VButton icon-start="translate" variant="outline" tone="neutral">A name</VButton>
  <VButton :icon-start="{ path: cloudUpload.paths[0] }" variant="outline" tone="neutral">
    SVG path data
  </VButton>
  <VButton :icon-start="{ component: CodeIcon }" variant="outline" tone="neutral">
    A component
  </VButton>
  <VButton :icon-start="{ src: firefoxLogo }" variant="outline" tone="neutral">An image</VButton>
</template>

href renders the button as an <a>. A disabled or loading link is made inert, its address dropped.

vue
<script setup lang="ts">
import { VButton } from 'vectis-ui'
import { arrow_right_alt as arrowRightAlt } from 'vectis-ui/icons'
</script>

<template>
  <VButton href="#usage" :icon-end="arrowRightAlt">Back to usage</VButton>
  <VButton href="#usage" variant="outline" tone="neutral" disabled>Unavailable</VButton>
</template>

States

disabled greys the button out through the colour tokens. loading disables it, announces it as busy and puts a spinner where the start icon was.

vue
<script setup lang="ts">
import { VButton } from 'vectis-ui'

const variants = ['solid', 'soft', 'outline', 'ghost'] as const
</script>

<template>
  <div class="row">
    <VButton v-for="variant in variants" :key="variant" :variant="variant" disabled>
      {{ variant }}
    </VButton>
  </div>

  <div class="row">
    <VButton loading>Saving</VButton>
    <VButton variant="outline" tone="neutral" loading>Checking</VButton>
  </div>
</template>

<style scoped>
.row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-3);
}
</style>

API

Props

PropTypeDefault
variantButtonVariant'solid' | 'outline' | 'ghost' | 'soft''solid'
How much visual weight the action carries: solid is filled with the tone, soft uses a tinted background, outline keeps only a border, and ghost shows nothing until it is hovered. Inside a VButtonGroup the group decides it, as it does the size, the density and the elevation.
toneButtonTone'accent' | 'neutral' | 'danger''accent'
What the action means: accent for the ordinary one, neutral for a secondary one, danger for one that destroys something. On a button a tone is an intention, which is why states such as success or warning are not offered here. Left out inside a VButtonGroup it takes the group's tone; on its own the button is accent.
elevatedbooleanfalse
Raises the button off the page with the shadow scale, whatever the variant. A ghost or outline button also gains a raised surface, because in the dark theme a shadow lying on the page background has nothing casting it.
sizeButtonSize'xs' | 'sm' | 'md' | 'lg' | 'xl''md'
The height of the button, taken from the size scale shared by every control: 24, 32, 40, 48 and 56 pixels.
compactbooleanfalse
Takes 4px off the height, leaving the padding, the text and the icons as they are.
fullWidthbooleanfalse
Stretches the button across the whole inline size of its parent instead of leaving it as wide as its label. It also becomes block-level, so it no longer sits on a line of text.
hrefstringnone
Turns the button into an <a> pointing at this address. A disabled or loading link becomes inert: the address is dropped, so it can be neither focused nor followed.
typeButtonHTMLAttributes['type']'button'
The native type of the button. It is ignored as soon as href makes it a link.
disabledbooleanfalse
Makes the button unusable: it stops responding, leaves the tab order and greys out through the colour tokens rather than through opacity.
loadingbooleanfalse
Shows a spinner, disables the button and announces it as busy. The spinner takes the place of the start icon, so the two are never shown side by side.
iconStartIconSourcenone
An icon before the label. The #start slot replaces it.
iconEndIconSourcenone
An icon after the label. The #end slot replaces it.
iconFilledbooleanfalse
Renders both icons in their filled form, the font's FILL axis. It has no effect on the #start and #end slots, whose icons you build yourself.

Slots

SlotType
default{}
The label of the button.
start{}
Content placed before the label, usually an icon. Mark it aria-hidden when it only repeats what the label already says.
end{}
Content placed after the label.

Types

The types the tables above name, written as the library declares them. The ones carrying export can be imported from vectis-ui to type your own code; the others are the shape of what a slot hands out.

export interface BuiltinIcon {
  name: string
  paths: readonly [string] | readonly [string, string]
}
export type IconRender =
  | { path: string; viewBox?: string }
  | { component: Component; props?: Record<string, unknown> }
  | { src: string }
  | { text: string; class?: string }
  | { class: string }
export type IconSource = string | BuiltinIcon | IconRender