Skip to content

Buttons & Fields ​

Button ​

vue
<Button tone="cta" :loading="saving" @click="save">
    <template #icon><Icon name="s-arrow-down-tray" /></template>
    {{ t('Save changes') }}
</Button>
PropDefaultPurpose
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.
loadingfalseSpinner in place of the leading icon; disables the button.
disabled, fullWidthfalse
type'button'submit for form buttons.
href, target, relnullRenders an <a>; target="_blank" adds rel="noopener".
rippletruePress feedback.
menuLabel, menuWidth'Open menu', 240The 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 ​

js
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.

vue
<IconButton :label="t('Columns')" size="lg" @click="open = !open"><IconColumns /></IconButton>
PropDefaultPurpose
labelrequiredAccessible 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).
tooltiptruefalse keeps the accessible name without the tooltip.
disabled, ripplefalse, 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:

vue
<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:

js
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 propDefaultPurpose
shape'rounded'sharp · rounded · pill.
size'md'sm (32px) · md (36px) · lg (40px).
disabled, readonly, requiredfalseForwarded.
placeholder''

TextField ​

vue
<TextField v-model="form.email" type="email" required />
PropDefaultPurpose
type'text'text · email · url · tel · password · search.
align'left'
mono, tabularfalseMonospace for codes; tabular figures for numbers.

NumberField ​

vue
<NumberField v-model="goal" :min="0" :step="10" stepper :decrease-label="t('Less')" :increase-label="t('More')" />
<NumberField v-model="threshold" suffix="€" />
PropDefaultPurpose
align, tabular'right', true
min, max, stepnullForwarded and respected by the stepper.
prefix, suffix''A unit drawn inside the frame.
stepperfalseThe 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 ​

vue
<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.

vue
<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) }]"
/>
PropDefaultPurpose
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.
minuteStep5Entries of the minute column; 1 lists every minute.
min, maxnullBounds 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.
weekStart1Monday. 0 or 7 start the week on Sunday.
localebrowserBCP 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.

vue
<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') }"
/>
PropDefaultPurpose
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.
months2How many months the calendar shows side by side.
min, maxnullBounds 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, locale1, browserAs for DateTimeField.
shape, size, disabled, placeholderAs 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 ​

vue
<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 ​

vue
<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') }]" />
PropDefaultPurpose
optionsnull{ value, label }[] for named stops; the slider snaps to the nearest.
min, max, step0, 100, 1The 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.

vue
<ColorPicker v-model="theme.primary" :labels="{ copy: t('Copy'), presets: t('Presets') }" />
PropDefaultPurpose
alphafalseAdds the alpha rail; the model becomes #rrggbbaa below full opacity.
presets14 coloursHex strings under the plane; [] hides the palette.
formats['hex', 'rgb', 'hsl', 'hsv']Display formats. The model is always hex.
copyabletrueA 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:

bash
npm install @tiptap/vue-3 @tiptap/starter-kit
js
import { 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.

vue
<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:

vue
<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.

Released under the MIT License.