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'xs (24px, a micro control in a dense toolbar) · 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.

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.

vue
<IconPicker v-model="form.icon" :sets="sets" :labels="{ placeholder: t('Choose icon') }" />
js
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 })),
    },
];
PropDefaultPurpose
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.
columns8Icons per row.
width384Width 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:

bash
npm install @heroicons/vue
js
import { HeroiconPicker, NamedHeroicon } from '@aaix/laravel-islands/vue/helpers/heroicons';
vue
<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:

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.