Button
A clickable action element with color, variant and size options.
Usage
Variants
subtle combines the tinted background of soft with a border. link uses
the same text color transitions as text and adds an underline on hover.
Colors
Every color across every variant, so it's easy to compare both within a color (is soft/outline/ghost legible?) and across colors (do they stay distinguishable at a glance?).
Icons
icon and trailingIcon add an icon before or after the label - any
registered icon name works, not just hugeicons:*.
<SButton icon="hugeicons:download-01">Download</SButton>
<SButton trailing-icon="hugeicons:arrow-right-01">Continue</SButton>
Icon only
There's no separate iconOnly prop - pass icon with no default slot
content and it shapes itself to a square automatically (matching its own
size), rather than requiring an explicit flag you could forget to set.
Always add aria-label yourself when there's no visible text - it falls
through to the root element like any other attr (see
Accessibility below).
<SButton icon="hugeicons:settings-01" variant="outline" aria-label="Settings" />
Square
Icon-only buttons already shape themselves to a square automatically (see
above) - square is for when the content isn't an icon but should still get
that same equal-width/height treatment, like a page number or a calendar day.
Without it, each button sizes to its own text width and a row of them (a
pagination bar, a calendar grid) won't line up:
<SButton square>1</SButton>
<SButton square>31</SButton>
Text
text never paints a background, at any state - only the label/icon color
shifts on hover. Meant for an action button that sits tight against another
control's own border, like a field's clear button - ghost's hover-fill
would otherwise visually compete with that border right at the seam:
The border in this comparison belongs to the illustrative field, not either button.
<div class="flex items-center rounded-[var(--selaras-resolved-radius-md)] ring-1 ring-inset ring-[var(--selaras-resolved-border-default)]">
<span>Example field</span>
<SButton icon="hugeicons:cancel-01" variant="text" aria-label="Clear" />
</div>
link is the underlined text-action variant. Use it for an action that
navigates; text is the unadorned text action.
Loading
loading swaps the leading icon for a spinner. It doesn't imply disabled -
combine both if a busy button shouldn't be clickable, the same split
Select and
Autocomplete already use
for their own loading prop.
<SButton icon="hugeicons:floppy-disk" :loading="saving" :disabled="saving" @click="save">
{{ saving ? 'Saving...' : 'Save' }}
</SButton>
As a link
to renders a NuxtLink for client-side navigation. Use as="a" with
href for external links, or set as explicitly to override automatic
routing. Disabled routed buttons expose aria-disabled, leave the tab order,
and ignore clicks.
<SButton as="a" href="https://github.com" target="_blank" rel="noopener" trailing-icon="hugeicons:square-arrow-up-right">
Open a link
</SButton>
<SButton to="/account">Account</SButton>
<SButton variant="link" to="/forgot-password">Forgot password?</SButton>
For the common internal-route case, to handles NuxtLink automatically. If
you need another routing component, pass its resolved component reference in
as - Primitive renders the explicit component via h():
<script setup lang="ts">
const NuxtLinkComponent = resolveComponent('NuxtLink')
</script>
<template>
<SButton :as="NuxtLinkComponent" to="/components/elements/button">
Browse components
</SButton>
</template>
Raised
raised adds --selaras-resolved-shadow-md, independent of variant - useful for a solid
button that needs to lift off a busy background, or even a soft/ghost
one that wants some elevation without switching to solid.
<SButton raised>Raised solid</SButton>
<SButton raised variant="soft">Raised soft</SButton>
Badge
There's no built-in badge slot - overlay a real Badge
on an icon-only button with a relative/absolute pair:
<div class="relative inline-flex">
<SButton variant="outline" icon="hugeicons:notification-01" aria-label="Notifications" />
<SBadge label="3" color="danger" size="sm" class="absolute -top-1.5 -right-1.5" />
</div>
Button group
Adjacent buttons visually joined into one unit are their own component - see ButtonGroup.
Sizes and states
size takes sm / md / lg, disabled prevents interaction and dims the
button - see the Props table below.
Custom :ui
Every slot can be overridden without fighting the defaults — the override is tailwind-merge'd, so conflicting utilities resolve instead of both applying.
<SButton :ui="{ base: 'rounded-full' }">Pill button</SButton>
To see exactly what you'd be overriding - the current default classes for
every slot and variant - here's Button's own theme file:
Accessibility
A default as="button" gets native button semantics (keyboard-activatable
with Enter/Space, exposed as role="button") for free -
nothing to configure. Rendering as="a" instead exposes link semantics and
keyboard behavior instead, which is exactly why As a link exists
rather than styling an <a> to merely look like a button. An icon-only button
has no accessible name of its own - always pass aria-label in that case, as
shown above.
Props
| Prop | Type | Default |
|---|---|---|
as | string | Component | 'button' |
color | ColorRole (built-in and registered roles) | primary |
to | string | - |
variant | 'solid' | 'soft' | 'subtle' | 'outline' | 'ghost' | 'text' | 'link' | solid |
size | 'sm' | 'md' | 'lg' | md |
block | boolean | false |
raised | boolean | false |
disabled | boolean | false |
loading | boolean | false |
icon | string | - |
trailingIcon | string | - |
square | boolean | - |
ui | Partial<Record<'base' | 'leadingIcon' | 'trailingIcon', string | object>> | - |
Slots
| Slot | Props | Description |
|---|---|---|
icon | { class } | Replaces the leading icon entirely; the scoped class carries the icon's resolved size classes |
trailing-icon | { class } | Replaces the trailing icon entirely; the scoped class carries the icon's resolved size classes |