Buttons & Fields
Button
<Button tone="cta" :loading="saving" @click="save">
<template #icon><Icon name="s-arrow-down-tray" /></template>
{{ t('Save changes') }}
</Button>| Prop | Default | Purpose |
|---|---|---|
label | '' | Fallback text when no default slot is passed. |
tone | 'primary' | cta (one-off ask, saturated) · primary (tinted, persistent) · secondary · outlined · ghost · danger. |
size | 'md' | sm (28px) · md (36px) · lg (40px). |
shape | 'rounded' | rounded or pill. |
loading | false | Spinner in place of the leading icon; disables the button. |
disabled, fullWidth | false | |
type | 'button' | submit for form buttons. |
href, target, rel | null | Renders an <a>; target="_blank" adds rel="noopener". |
ripple | true | Press feedback. |
menuLabel, menuWidth | 'Open menu', 240 | The chevron half when #menu is filled. |
Events: click, menu-open, menu-close. Slots: default, #icon, #iconRight, #chip (an inline chip after the label), #menu (turns the button into a split button; receives { close }).
Application-Wide Defaults
startVueIslands(registry, {
theme: { button: { shape: 'pill', size: 'md', tone: 'primary' } },
});provideTheme({ button: { shape: 'pill' } }) does the same for a subtree, and the theme's button.tones table takes new tones. An explicit prop always wins. The whole picture is on Theming; provideButtonDefaults() and BUTTON_DEFAULTS_KEY from before keep working.
ButtonGroup
Joins directly nested buttons into one strip with shared seams. Props: shape, ariaLabel.
IconButton
An icon-only button. label is the accessible name and the tooltip.
<IconButton :label="t('Columns')" size="lg" @click="open = !open"><IconColumns /></IconButton>| Prop | Default | Purpose |
|---|---|---|
label | required | Accessible name and tooltip text. |
size | 'md' | sm · md · lg. |
tone | 'quiet' | quiet (no surface, tint on hover) · secondary · primary · outlined (the Button surfaces) · active (tinted, for a toggle that is on) · danger · plain (no colour of its own). |
tooltip | true | false keeps the accessible name without the tooltip. |
disabled, ripple | false, true | |
href, target | '', '_blank' | Renders an anchor. |
Event: click.
EditButton
The quiet pencil beside an editable value. Props: label (required), size (sm · md). Event: click.
vRipple
The press feedback as a directive, for anything a pointer lands on:
<tr v-ripple @click="open(row)">…</tr>
<button v-ripple="!disabled">…</button>The ripple spawns on pointerdown; the element is made relative and clipped if it was not already. No stylesheet to import. A viewer who asked for reduced motion gets none.
Outside Vue
The effect itself carries no framework — the directive is a shell around the core, which any adapter or plain DOM code can use directly. It ships from the package root, and from @aaix/laravel-islands/ripple for a consumer that wants the effect without the runtime:
import { attachRipple, delegateRipple } from '@aaix/laravel-islands';
const detach = attachRipple(element);
const detachAll = delegateRipple(document, '.sidebar-item, .topbar-btn');attachRipple binds one element, delegateRipple listens once at a root and serves every element matching the selector — the one to reach for when a server-rendered fragment gets replaced, since a directly bound listener would not survive the swap. Both take an optional isEnabled(el) predicate and return a detach function. spawnRipple(el, event) is the primitive underneath, for a press that some other gesture decides.
Fields
Every field sits on one shared frame — 36px tall at size="md", rounded-md, a hairline ring that turns primary on focus. All are v-model components and forward class and other attributes to the native element.
| Shared prop | Default | Purpose |
|---|---|---|
shape | 'rounded' | sharp · rounded · pill. |
size | 'md' | sm (32px) · md (36px) · lg (40px). |
disabled, readonly, required | false | Forwarded. |
placeholder | '' |
TextField
<TextField v-model="form.email" type="email" required />| Prop | Default | Purpose |
|---|---|---|
type | 'text' | text · email · url · tel · password · search. |
align | 'left' | |
mono, tabular | false | Monospace for codes; tabular figures for numbers. |
NumberField
<NumberField v-model="goal" :min="0" :step="10" stepper :decrease-label="t('Less')" :increase-label="t('More')" />
<NumberField v-model="threshold" suffix="€" />| Prop | Default | Purpose |
|---|---|---|
align, tabular | 'right', true | |
min, max, step | null | Forwarded and respected by the stepper. |
prefix, suffix | '' | A unit drawn inside the frame. |
stepper | false | The minus/plus pair. |
decreaseLabel, increaseLabel | '' | Accessible names of the stepper buttons. |
The model is a number, or null while empty.
TextArea
Props: rows (4), mono. Multi-line fields grow from the shared height.
SelectField
A native <select> on the shared frame, for a short list. Props: options ({ value, label }[], or hand-written <option>s in the default slot).
FileField
<FileField v-model="upload" accept="image/*" multiple :label="t('Choose photos')" :hint="t('JPEG or PNG, up to 10 MB')" />Props: accept, multiple (model becomes a FileList), label, hint. Events: update:modelValue, change. Reset by setting the model to null.
DateTimeField
A field for a moment, a day or a time of day. Typing is understood in the common shapes (8.9.2026 14:00, 8.9. 14:00, 2026-09-08T14:00, 14:00); the button beside it opens a calendar and two clock columns, where the choice stays a draft until it is applied. Works in the browser's local time and shows a 24-hour clock.
<DateTimeField v-model="form.pickupAt" :labels="{ apply: t('Apply'), clear: t('Clear'), today: t('Today') }" />
<DateTimeField v-model="settings.opensAt" mode="time" :minute-step="15" />
<DateTimeField
v-model="form.dueAt"
:shortcuts="[{ label: t('Tomorrow 09:00'), value: () => tomorrowAt(9) }]"
/>| Prop | Default | Purpose |
|---|---|---|
mode | 'datetime' | datetime · date · time. The model is an ISO string, YYYY-MM-DD or HH:MM; null while empty. |
variant | 'field' | field shows the value in an input; button is only the trigger as an IconButton, for a view that writes the value out itself. |
tone | 'quiet' | The IconButton tone of the button variant. |
minuteStep | 5 | Entries of the minute column; 1 lists every minute. |
min, max | null | Bounds in the model's format; days outside are struck through. |
shortcuts | [] | { label, value } rows above the calendar; value is a Date, a model string or a function returning either. |
weekStart | 1 | Monday. 0 or 7 start the week on Sunday. |
locale | browser | BCP 47 tag for month and weekday names. |
labels | {} | open, previousMonth, nextMonth, previousYear, nextYear, chooseMonth, today, hours, minutes, clear, apply. |
The month heading is a button: one press shows the twelve months, a second the surrounding years, so a far-off date is three clicks away.
formatDateTime, parseTypedDateTime, toDateTimeModel and parseDateTimeModel are exported alongside for a view that wants the same wording or parsing elsewhere.
DateRangeField
A span of days. Two months side by side: the first click starts the span, the second ends it, the footer applies it; shortcuts beside the calendar apply at once. The field variant also understands a typed span (8.9. – 16.9.2026, 2026-09-08 - 2026-09-16, a single day). The model is { from, to } as YYYY-MM-DD strings, null while empty, in the browser's local time.
<DateRangeField
v-model="state.created"
variant="filter"
:placeholder="t('Created')"
:shortcuts="[
{ label: t('Today'), from: today(), to: today() },
{ label: t('Last 7 days'), range: () => lastDays(7) },
{ label: t('This month'), range: thisMonth },
]"
:labels="{ clear: t('Clear'), apply: t('Apply') }"
/>| Prop | Default | Purpose |
|---|---|---|
variant | 'field' | field is a form control with a typed input; filter, filter-card and filter-pill are toolbar triggers that colour a set span, as the selects do. |
months | 2 | How many months the calendar shows side by side. |
min, max | null | Bounds as YYYY-MM-DD; days outside cannot be picked. |
shortcuts | [] | { label, from, to } rows beside the calendar, or { label, range } with range() returning { from, to }; model strings or Dates. The active one carries the primary tint. |
weekStart, locale | 1, browser | As for DateTimeField. |
shape, size, disabled, placeholder | As for the other fields. | |
labels | {} | open, previousMonth, nextMonth, clear, apply. |
Arrow keys move through the days, Enter picks, Escape closes. formatDayRange(from, to, locale) is exported alongside: 8 – 16 Sep 2026, or the single day when both are the same.
Checkbox
Props: indeterminate, ariaLabel (required when no visible label sits beside it).
Switch
For a setting that takes effect at once; the handle carries a glyph as well as a colour. Props: tone (primary · success · danger), ariaLabel.
Radio and RadioGroup
<RadioGroup v-model="form.scope" orientation="horizontal">
<label class="flex items-center gap-2"><Radio value="company" /> {{ t('Company goal') }}</label>
<label class="flex items-center gap-2"><Radio value="personal" /> {{ t('Personal goal') }}</label>
</RadioGroup>RadioGroup owns the model, names the group (auto-generated unless name is passed) and takes orientation and disabled. A Radio outside a group takes modelValue, value and name itself.
Slider
<Slider v-model="opacity" :min="0" :max="100" :step="5" />
<Slider v-model="density" :options="[{ value: 'compact', label: t('Compact') }, { value: 'cozy', label: t('Cozy') }]" />| Prop | Default | Purpose |
|---|---|---|
options | null | { value, label }[] for named stops; the slider snaps to the nearest. |
min, max, step | 0, 100, 1 | The numeric range without options. |
minLabel, maxLabel | '' | Captions at either end. |
Events: update:modelValue on every move, commit on release — bind the save to commit.
ColorPicker
A hex field with a swatch that opens the picker: saturation plane, hue rail, optional alpha rail, presets and a format switch.
<ColorPicker v-model="theme.primary" :labels="{ copy: t('Copy'), presets: t('Presets') }" />| Prop | Default | Purpose |
|---|---|---|
alpha | false | Adds the alpha rail; the model becomes #rrggbbaa below full opacity. |
presets | 14 colours | Hex strings under the plane; [] hides the palette. |
formats | ['hex', 'rgb', 'hsl', 'hsv'] | Display formats. The model is always hex. |
copyable | true | A copy button beside the value. |
labels | {} | open, plane, hue, alpha, presets, copy, copied. |
WysiwygEditor
A TipTap rich-text field. The model is HTML; an empty document emits ''. It is the one helper with a dependency of its own, so it lives behind its own import and the rest of the helpers load without TipTap installed:
npm install @tiptap/vue-3 @tiptap/starter-kitimport { WysiwygEditor } from '@aaix/laravel-islands/vue/helpers/wysiwyg';Two extension points keep one editor for every field: extensions takes TipTap extensions beyond the starter kit, and the tools slot renders after the built-in buttons with the editor as slot prop, for a control the buttons cannot express.
<WysiwygEditor v-model="html" :extensions="[FontSize]">
<template #tools="{ editor }">
<SelectField :model-value="editor.getAttributes('fontSize').size ?? ''" :options="sizes" @update:model-value="editor.chain().focus().setFontSize($event).run()" />
</template>
</WysiwygEditor>FieldCaption and FieldGroup
FieldCaption is the 10px uppercase caption above a value — always a <span>, wrapped by the callsite in the semantic element it belongs to:
<label class="block">
<FieldCaption>{{ t('Weekly goal') }}</FieldCaption>
<NumberField v-model="goal" class="mt-1" />
</label>FieldGroup groups fields or segments under an optional label; muted greys it, tone tints the whole group when its state is the message.