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' | xs (24px, a micro control in a dense toolbar) · 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. |
IconPicker
A field that opens a searchable grid of icons. The model is the chosen icon's value; the clear button sets ''. The picker knows no icon library: sets names the groups to choose from, a segmented switch appears when there is more than one, and each set loads its icons only when the picker opens. The grid opens scrolled to the chosen icon.
<IconPicker v-model="form.icon" :sets="sets" :labels="{ placeholder: t('Choose icon') }" />const sets = [
{
value: 'brand',
label: t('Brand'),
owns: (value) => value.startsWith('brand-'),
load: async () => Object.entries(await import('./brandIcons.js'))
.map(([name, component]) => ({ value: `brand-${name}`, label: name, component })),
},
];| Prop | Default | Purpose |
|---|---|---|
sets | — | { value, label, load, owns? }. load() resolves to { value, label, component } items; label is what the search matches and the tooltip shows. owns(value) picks the set a stored value belongs to when the picker opens. |
columns | 8 | Icons per row. |
width | 384 | Width of the panel in pixels. |
labels | {} | placeholder, search, clear, empty. |
The trigger shows the chosen icon once its set has loaded. To show it before that, render it yourself in the selected slot, which receives the value.
Heroicons
Projects on Heroicons get a ready picker and a component that draws one Heroicon from its Blade name (heroicon-o-truck, -s-, -m-, -c-), behind their own import:
npm install @heroicons/vueimport { HeroiconPicker, NamedHeroicon } from '@aaix/laravel-islands/vue/helpers/heroicons';<HeroiconPicker v-model="form.icon" :labels="{ outline: t('Outline'), solid: t('Solid') }" />
<NamedHeroicon :name="link.icon" class="h-5 w-5" />HeroiconPicker offers outline and solid; styles takes any of o, s, m, c, and labels adds outline, solid, mini and micro to the picker's own. The stored names are the ones Blade Heroicons renders, so the server can draw the same icon. NamedHeroicon loads only the one icon it draws. heroiconSets(styles, labels) hands the same sets to an IconPicker of your own.
WysiwygEditor
A TipTap rich-text field. The model is HTML; an empty document emits ''. It is one of the two helpers with a dependency of their 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.