Button
The standard action, in six variants and four sizes, with a loading state. Renders as any tag or component.
Install
If this is the first component in the project, set up once. This writes ermine.json and copies the theme stylesheet. The installation guide covers Vue, Nuxt and Astro.
npx ermineui initpnpm dlx ermineui inityarn dlx ermineui initbunx ermineui initThen add the component:
npx ermineui add buttonpnpm dlx ermineui add buttonyarn dlx ermineui add buttonbunx ermineui add buttonThat copies 2 files into src/components/ui/button/, 1 helper into src/lib/ermine/, and the 1 component it is built from. Files you already have are kept.
Use
import { Button } from '@/components/ui/button'<Button><Icon name="plus" /> New project</Button>
<Button size="icon" aria-label="Delete"><Icon name="trash" /></Button>
<Icon name="circle-check" label="Passed" :size="20" />In Nuxt, with the auto-import setting, the tags work in templates without the import. In Astro, use it inside a Vue island.
What gets copied
| File | Where it goes |
|---|---|
Button.vue | src/components/ui/button/Button.vue |
index.ts | src/components/ui/button/index.ts |
primitive.ts helper | src/lib/ermine/primitive.ts |
It is built from: Spinner. These are added with it.
The folders are the suggested ones. ermine.json says where yours are.
Source
This is exactly what the command copies. To install by hand, create each file below and the helpers listed above.
Button.vue 163 lines
<script setup lang="ts">
import type { Component } from 'vue'
import { Primitive } from '../../lib/primitive'
import { Spinner } from '../spinner'
withDefaults(
defineProps<{
variant?: 'default' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'link'
size?: 'sm' | 'md' | 'lg' | 'icon'
/** Tag or component to render, e.g. "a" or NuxtLink. */
as?: string | Component
/** Pass the button's styling and behavior to the single child instead of rendering a tag. */
asChild?: boolean
disabled?: boolean
/** Set while the action is running: shows a spinner and stops the button being pressed again. */
loading?: boolean
}>(),
{ variant: 'default', size: 'md', as: 'button' },
)
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:type="as === 'button' && !asChild ? 'button' : undefined"
class="er-button"
:data-variant="variant"
:data-size="size"
:disabled="(as === 'button' || asChild) && (disabled || loading) ? true : undefined"
:aria-disabled="as !== 'button' && (disabled || loading) ? true : undefined"
:aria-busy="loading || undefined"
>
<!-- With as-child the content belongs to the child, so there is nowhere to put a spinner. -->
<Spinner v-if="loading && !asChild" label="" />
<!-- An icon button has room for one thing, so the spinner stands in for the icon. -->
<slot v-if="!loading || asChild || size !== 'icon'" />
</Primitive>
</template>
<style>
@layer components {
.er-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
height: var(--er-control-md);
padding: 0 1rem;
border: 1px solid transparent;
border-radius: var(--er-radius-control);
background: var(--er-primary);
color: var(--er-primary-foreground);
font-family: inherit;
font-size: 0.875rem;
font-weight: var(--er-weight-medium);
line-height: 1;
white-space: nowrap;
text-decoration: none;
cursor: pointer;
user-select: none;
transition:
background-color var(--er-duration),
color var(--er-duration),
border-color var(--er-duration),
box-shadow var(--er-duration);
}
.er-button:hover {
background: color-mix(in oklab, var(--er-primary) 88%, var(--er-background));
}
.er-button:focus-visible {
outline: 2px solid var(--er-ring);
outline-offset: 2px;
}
.er-button:disabled,
.er-button[aria-disabled='true'] {
opacity: 0.5;
pointer-events: none;
}
.er-button svg {
flex-shrink: 0;
width: 1rem;
height: 1rem;
pointer-events: none;
}
/* Variants */
.er-button[data-variant='secondary'] {
background: var(--er-secondary);
color: var(--er-secondary-foreground);
}
.er-button[data-variant='secondary']:hover {
background: color-mix(in oklab, var(--er-secondary) 80%, var(--er-foreground) 6%);
}
.er-button[data-variant='outline'] {
border-color: var(--er-input);
background: var(--er-input-background);
color: var(--er-foreground);
box-shadow: var(--er-shadow-xs);
}
.er-button[data-variant='outline']:hover,
.er-button[data-variant='ghost']:hover,
.er-button[data-variant='outline'][data-state='open'],
.er-button[data-variant='ghost'][data-state='open'] {
background: var(--er-accent);
color: var(--er-accent-foreground);
}
.er-button[data-variant='ghost'] {
background: transparent;
color: var(--er-foreground);
}
.er-button[data-variant='destructive'] {
background: var(--er-destructive);
color: var(--er-destructive-foreground);
}
.er-button[data-variant='destructive']:hover {
background: color-mix(in oklab, var(--er-destructive) 88%, var(--er-background));
}
.er-button[data-variant='link'] {
height: auto;
padding: 0;
background: transparent;
color: var(--er-foreground);
text-decoration: underline;
text-decoration-color: transparent;
text-underline-offset: 4px;
}
.er-button[data-variant='link']:hover {
text-decoration-color: currentColor;
}
/* Sizes */
.er-button[data-size='sm'] {
gap: 0.375rem;
height: var(--er-control-sm);
padding: 0 0.75rem;
font-size: 0.8125rem;
}
.er-button[data-size='lg'] {
height: var(--er-control-lg);
padding: 0 1.5rem;
}
.er-button[data-size='icon'] {
width: var(--er-control-md);
padding: 0;
}
}
</style>index.ts 3 lines
/** Button — the standard action, in six variants and four sizes, with a loading state. Renders as any tag or component. */
export { default as Button } from './Button.vue'