Skip to content

Form components

innoboxrr-form-elements (Vue, 6.8.0) and innoboxrr-react-form-elements (React, 3.8.0) export the same 37 components, with the same names. There's a reason for that: LaraPack writes the same form_component from laraimport.json into both the Vue and the React forms, and a name missing from one package would break that framework's build. A parity test in each repository fails if one package exports something the other doesn't.

Styling comes from the form-core theme. No component needs UIkit, Tailwind or Font Awesome.

Install

bash
npm i innoboxrr-form-elements innoboxrr-form-core
bash
npm i innoboxrr-react-form-elements innoboxrr-form-core
js
import 'innoboxrr-form-core/styles'
import FormElements from 'innoboxrr-form-elements'

app.use(FormElements) // registers all 37 components globally

// or import only the ones you use
import { TextInputComponent, DrawerComponent } from 'innoboxrr-form-elements'
jsx
import 'innoboxrr-form-core/styles'
import 'innoboxrr-react-form-elements/src/css/form-elements.css'
import '@yaireo/tagify/dist/tagify.css'       // if you use TagsInputComponent
import 'react-phone-number-input/style.css'   // if you use CountrySelectInputComponent

import { TextInputComponent, DrawerComponent } from 'innoboxrr-react-form-elements'
VueReact
Peer dependenciesvue ^3.5.0, sortablejs ^1.14.0, lightvue (optional)react ^19.0.0, react-dom ^19.0.0
Global registrationapp.use(FormElements)None: React has no app plugin
The components' own stylesIn each component's <style scoped>In src/css/form-elements.css, imported once

What every control shares

WhatVueReactWhy
The valuev-modelvalue + onChange(value)onChange receives the value, not the event, just like update:modelValue. The generator emits both forms from the same JSON, so they have to mean the same thing.
Label and helplabel, helplabel, helphelp renders an icon carrying the text in data-tooltip and aria-label.
ValidationvalidatorsvalidatorsWritten to data-validators, which js-validator reads.
Lengthmin_length, max_lengthminLength, maxLength (the underscore names are accepted too)Written to data-min_length and data-max_length.
ClasscustomClasscustomClassReplaces the theme token's class; it isn't added to it.
No valueManages itselfA React control without value is uncontrolled. The mode is fixed on the first render, to avoid React's warning.

For the DOM event, React has onInput, onFocus, onBlur, onEnter and onPaste where the Vue twin emits input, focus, blur, enter and paste.

The catalogue

GroupComponents
TextTextInputComponent, TextareaInputComponent, TagsInputComponent, CountrySelectInputComponent, CodeInputComponent
SelectionSelectInputComponent, SelectSearchInputComponent, ModelSearchInputComponent, TimezoneSelectInputComponent, CheckboxInputComponent, RadioInputComponent, SingleCheckboxInputComponent, MultiCheckboxInputComponent, SwitchComponent, StarsInputComponent, ColorPickerInputComponent
CompositeDynamicGroupInputComponent, FqsInputComponent, PolymorphicInputComponent
EditorsEditorInputComponent, TextEditorMonoStyleInputComponent, CodeMirrorComponent
FilesFileInputComponent, FileDropInputComponent, SimpleFileInputComponent, AvatarInputComponent
BasicsButtonComponent, IconComponent, InputErrorComponent
DesktopDialogComponent, DrawerComponent, MenuComponent, CommandPaletteComponent, SkeletonComponent, ClickToEditComponent, ToastRegionComponent, ConfirmHostComponent

In React, each component wraps the equivalent of the library its Vue twin wraps. Where the library is framework-agnostic, it is literally the same one:

ComponentVueReact
TextInputComponent (mask)innoboxrr-maskjs/vueinnoboxrr-maskjs, the same engine
TagsInputComponent@yaireo/tagify@yaireo/tagify/react, the same library
EditorInputComponent@tinymce/tinymce-vue@tinymce/tinymce-react
CodeMirrorComponentvue-codemirror@uiw/react-codemirror, the same CodeMirror 6
SelectSearchInputComponentvue-selectreact-select
CountrySelectInputComponentvue-tel-inputreact-phone-number-input, the same libphonenumber-js
DynamicGroupInputComponentvuedraggable@dnd-kit/sortable, with keyboard reordering
ColorPickerInputComponentlightvue (optional; without it, <input type="color">)react-colorful
IconComponent@iconify/vue@iconify/react
MenuComponentthe popover attribute and @floating-ui/domthe same

Text

TextInputComponent

vue
<TextInputComponent
    type="text"
    name="phone"
    label="Phone"
    validators="required"
    :mask-format="{ mask: '(___) ___-____', format: '(***) ***-****' }"
    v-model="form.phone" />
jsx
<TextInputComponent
    type="text"
    name="phone"
    label="Phone"
    validators="required"
    maskFormat={{ mask: '(___) ___-____', format: '(***) ***-****' }}
    value={form.phone}
    onChange={(value) => setField('phone', value)} />
VueReactDefaultWhat it does
typetyperequiredThe <input> type. With password, a button to reveal the password appears.
namenamerequired in Vue
label, help, iconsame'', null, ''icon is a semantic or Iconify name.
placeholderplaceholdernull
validatorsvalidatorsnull
min_length, max_lengthminLength, maxLengthnullIn Vue they're also written to the min and max attributes.
stepsstepsnullThe step attribute.
readonlyreadOnlyunset
autofocus, autocompleteautoFocus, autoCompleteunset
maskFormatmaskFormat{} in Vue, null in ReactA maskjs { mask, format }; see maskjs.
showPasswordLabel, hidePasswordLabelsame'Show password', 'Hide password'The eye button's aria-label.
customClasscustomClassunset
idgeneratedThe label points at it.
events enter, input, focus, blur, pasteonEnter, onInput, onFocus, onBlur, onPasteThey receive the DOM event. enter is releasing the Enter key.

TextareaInputComponent

VueReactDefault
namenamerequired in Vue
labellabel, help''
rowsrows5
placeholder, validatorssamenull
min_length, max_lengthminLength, maxLength (or the underscore names)null
customClasscustomClass, idunset

TagsInputComponent

VueReactDefaultWhat it does
namenamerequired in Vue
label, help, placeholdersame'', null, ''
customClasscustomClass'fe-input ' in Vue
modelValue (string or array)value[] in Vue
validators, id
whitelist, maxTags, duplicatesduplicates: falseTagify options.
tagifyRefA ref to reach the Tagify instance.

CountrySelectInputComponent

A phone number with a country selector, validated with libphonenumber-js.

VueReactDefault
labellabel, help''
defaultPhone, defaultCountrysame'', null
disableddisabledfalse
wrapperClass, containerClass, labelClasssamenull
dropdownOptions, inputOptions, preferredCountries{}, {}, [] (vue-tel-input options)
name, id, placeholder, validators'telephone', null, 'Ingresa un número telefónico', null
change eventonCountryChange

The event carries different data in each framework

  • Vue emits change with { phone, country, isValid }. With a valid number, phone is the national number. Leaving the field with an invalid number clears it and emits { phone: '', country, isValid: false }.
  • React calls onCountryChange with { phone, country, callingCode, national, isValid }. phone is the full value and the national number comes separately, in national.

CodeInputComponent

A verification code, one character per box.

VueReactDefault (Vue / React)
fieldsfields3 / 6
fieldWidth, fieldHeightsame56 / 40
requiredrequiredtrue / false
disableddisabledfalse
title, classNamesameunset / null, ''
autoFocus, valuefalse
change(code) eventonChange(code)
complete(isComplete) eventonComplete(code)

In Vue, complete fires on every change with a boolean. In React, onComplete is called only once every box is filled, and it receives the code.

Selection

SelectInputComponent

vue
<SelectInputComponent name="status" label="Status" validators="required" v-model="form.status">
    <option value="">Choose one</option>
    <option value="draft">Draft</option>
</SelectInputComponent>
jsx
<SelectInputComponent name="status" label="Status" validators="required" value={form.status} onChange={setStatus}>
    <option value="">Choose one</option>
    <option value="draft">Draft</option>
</SelectInputComponent>
VueReactDefault
namenamerequired in Vue
label, helpsame'', null
multiplemultiplefalse
sizesizenull
validators, customClasssamenull
default slotchildrenThe <option> elements
id

SelectSearchInputComponent

A select with search.

VueReactDefaultWhat it does
inputLabel, helpsame'', nullThe field label.
optionsoptions[]
labellabel'label'The option key that is displayed.
reducereducethe whole optionWhat is stored as the value.
multiple, clearable, disabled, loading, appendToBodysamefalse, true, false, false, false
placeholderplaceholder''
customClasscustomClassunset
ajax, route, method, q, searchParams, minSearchLength, parseBeforeSubmit, debounceTimefalse, '', 'post', '', {}, 2, identity, 300Remote search in Vue.
noOptionsText'Nothing here.'
search(response) eventonSearch(term)
name, validators, id
  • Vue also accepts the rest of vue-select's props: taggable, pushTags, filterable, searchable, closeOnSelect, selectOnTab, getOptionLabel, getOptionKey, filterBy and so on. With ajax, the search sends _token, paginate: 0, the q key with the term, and searchParams. With method: 'get', all of that goes in the query string.
  • React passes remaining props to react-select. It publishes the value in an <input type="hidden"> with name and data-validators, because the library doesn't expose an input to put them on.

ModelSearchInputComponent

Searches a model's records against an API route.

VueReactDefaultWhat it does
labelStr, placeholderStrsamerequired in Vue
routerouterequired in VueThe search URL.
methodmethod'get'
qq'id'The parameter that carries the term.
externalFiltersexternalFilters{}Filters added to the search.
reducereduce(option) => option.id
getOptionLabeloptionLabel(option) => `ID: ${option.id}` in Vue; 'name' in ReactA function in Vue; a record key in React.
multiple, hideOnEmitsamefalse
debounceTimedebounce300Milliseconds.
minLength1Minimum length before searching.
noOptionsText'Nothing results found'
events submit, selectedonSubmit, onSelected
customClasscustomClassnull

TimezoneSelectInputComponent

VueReactDefault
namenamerequired in Vue
label, helpsame'', null
placeholderplaceholder'Select a timezone'
validatorsvalidators''
multiple, sizefalse, null

CheckboxInputComponent and RadioInputComponent

VueReactDefaultWhat it does
namenamerequired in Vue
texttext''The text next to the box.
valvalnull for Checkbox; required for RadioThe value this option contributes.
validators, customClasssamenull
checked (Radio only)false
default slotchildren

SingleCheckboxInputComponent

VueReactDefault
ididrequired in Vue
labellabel''
v-model:checkedchecked + onCheckedChange(checked)false
valuevaluenull

MultiCheckboxInputComponent

VueReactDefaultWhat it does
optionsoptionsrequired in Vue[{ id, name }]. In Vue a validator requires both keys.
v-model:valuevalue + onChangerequired in VueThe checked ids.
idid''

React derives the selection from the value. The Vue version recomputes it with document.querySelectorAll, so two groups with the same id interfere with each other.

SwitchComponent

VueReactWhat it does
v-modelvalue + onChange(boolean)
change(event) eventonToggle(event)The DOM event.
remaining propsPassed to the <input type="checkbox">.

StarsInputComponent

VueReactDefault
maxmax5
v-model (number)value + onChange0 in Vue
namename'rating'
char, inactiveCharsame'★', null
readonlyreadOnlyfalse
starsSizestarsSize'50px'
activeColor, inactiveColor, shadowColor, hoverColornull
slots activeLabel, inactiveLabel

ColorPickerInputComponent

VueReactDefault
labellabel, help, id''
clearableclearabletrue
colorscolors16 Material colors
bottomBartrue
v-modelvalue + onChange'#607C8A' in Vue
name, validators'color', null

Without lightvue, the Vue version falls back to <input type="color">, which opens the operating system's dialog and can't be styled or tested. React uses react-colorful, 2.8 kB with no dependencies, and publishes the value in a hidden input with name and data-validators.

Composite

DynamicGroupInputComponent

A list of field groups that can be added, removed and reordered.

VueReactDefaultWhat it does
v-model (array)value + onChangerequired in VueOne object per group.
inputsConfiginputsConfigrequired in VueOne field per entry, with key, type, label and options; in Vue, also attributes.
labellabel''
addButtonLabel, removeButtonLabel, itemLabelsame'Añadir', 'Eliminar', 'Item'
hasSufixtrue

In React the drag handle is a <button> you can reach with Tab: @dnd-kit also reorders with the keyboard. A form that can only be reordered with a mouse isn't accessible.

FqsInputComponent

Frequently asked questions: a list of { question, answer }.

VueReactDefault
v-model (array)value + onChangerequired in Vue
inputClassinputClass'fe-input '
labelslabels{ title: 'Add frequency asked questions', question: 'Question', answer: 'Answer', add: 'Add Question', remove: 'Remove question' }
uploadUrlnull
name'fqs'

PolymorphicInputComponent

A single component that decides which control to render from a configuration.

vue
<PolymorphicInputComponent
    :props="{ type: 'select', name: 'size', label: 'Size', options: ['S', 'M', 'L'] }"
    v-model="answer"
    @save="save" />
jsx
<PolymorphicInputComponent
    config={{ type: 'select', name: 'size', label: 'Size', options: ['S', 'M', 'L'] }}
    value={answer}
    onChange={setAnswer}
    onSave={save} />
Configuration keyUse
typeVue: text, number, date, time, url, email, textarea, radio, select, checkbox, file. React: the same, plus switch and editor.
label, name, placeholder, validators, customClass, iconPassed to the control.
minLength, maxLength, readonlyPassed to the text control.
optionsFor radio, select and checkbox. In React a select option can be { value, label }.
  • The prop. In Vue it is literally called props, kept for compatibility. In React it is called config and accepts props as an alias.
  • Saving. After a change, a button appears that calls save (onSave in React) with the value.
  • Vue, checkbox type. The value is a JSON array, and each change emits save immediately.
  • Vue, file type. It uploads with FileInputComponent and stores the file id.
  • React, file type. It uses SimpleFileInputComponent.

Editors

EditorInputComponent

A rich text editor built on TinyMCE.

VueReactDefault (Vue / React)
id, namesamerequired in Vue
label, helpsame'', null
heightheight400 / 300
disableddisabledfalse
initialValueinitialValue''
plugins, toolbarsameDifferent in each package
menubar, inlinetrue, false
output'html' (or 'text')
tinymceCdntinymceScriptSrcVue loads TinyMCE 6.3.2 from cdnjs
apiKey
uploadUrl, uri, file, onFileUploadSuccessnull, '/', false, null
showSpeechRecognitionfalse
extraConfigremaining props{}
error event
validators

TextEditorMonoStyleInputComponent

The twins are not the same editor

  • Vue. It wraps EditorInputComponent (TinyMCE) with speech dictation. Its props are label, name, id ('tmce'), placeholder, validators, min_length, max_length, defaultShowEditor (false), showSpeechRecognition (true), height (200), plugins, toolbar, menubar, inline, output, file, disabled, initialValue and customClass.
  • React. It is CodeMirrorComponent with language="html". Its props are label, help, name, height ('400px'), readOnly, value and onChange.

CodeMirrorComponent

vue
<CodeMirrorComponent v-model="config" lang="json" label="Site configuration" />
jsx
<CodeMirrorComponent
    language="json"
    label="Site configuration"
    name="config"
    value={config}
    onChange={setConfig} />
VueReactDefault (Vue / React)What it does
langlanguage'html' / 'javascript'html, css, javascript or json, loaded on demand.
themetheme'auto' / 'dark'Vue: auto, dark or light. React: dark or light.
autofocusfalseWhether the editor takes focus on mount.
placeholder'Escriba su codigo aqui...'
labellabel, help''
height, readOnly'300px', falseIn Vue the height is fixed at 400 px.
name, validatorsReact publishes the value in a hidden input with name and data-validators.
exposes viewCodeMirror's EditorView.

In Vue, Tab indents and the tab size is 4. In React, remaining props go to @uiw/react-codemirror.

What changed in 6.8.0 (Vue)

  • The language loads on demand.
    • Before. The component statically imported html, css, javascript, json and one-dark. In the base application, an editor that only edits JSON produced a 580 kB chunk (200 kB gzipped).
    • Now. lang arrives through import(), and each language is its own chunk. An application that only mounts the editor with lang="json" goes from 637.3 kB (222.7 kB gzipped) to 498.5 kB (167.7 kB gzipped) in light mode. Dark mode adds 2.7 kB (1.2 kB gzipped) for one-dark.
  • While the language loads, the editor works as plain text. When it arrives, the editor reconfigures without rebuilding the view, so neither the cursor nor the history is lost.
  • Loaded languages are remembered. An editor that mounts again starts directly with its language; for example inside a dialog, whose content only exists while it is open.
  • Only the latest request counts: a slow language doesn't overwrite the next one.
  • A lang outside the list falls back to html without throwing. Previously, constructor or __proto__ found something in the language map and broke the editor.
  • The editor follows the theme.
    • Before. It was always painted with one-dark, even in a light application.
    • Now. theme="auto" applies the same rule as form-core's variables: data-theme on <html> wins, and without it prefers-color-scheme decides. It switches live when either changes. dark and light force a mode. one-dark is only downloaded the first time the editor has to paint in dark mode.
  • autofocus defaults to false. Before, the editor took focus on mount, even at the bottom of a form.
  • The label names the editor through aria-labelledby, and clicking it focuses the editor. A <label for> can't point at CodeMirror's contenteditable. Before, a screen reader announced a field with no name. With no label text the attribute isn't added, since it would name the editor with nothing.

May affect existing users

  • Focus. If a screen relied on the editor taking focus on mount, you now have to ask for it:

    vue
    <CodeMirrorComponent v-model="html" autofocus />
  • Appearance. To get the old dark look back in a light application, force it with theme="dark".

What changed in 3.8.0 (React)

  • The language also loads on demand. An application that only mounts the editor with language="json" goes from 804.6 kB (270.9 kB gzipped) to 666.6 kB (215.9 kB gzipped). html, css and javascript stay in chunks nobody requests.
  • Plain text while loading. A language outside the list leaves the editor as plain text, without throwing.
  • theme is still dark (the default) or light. one-dark stays in the bundle because @uiw/react-codemirror imports and re-exports it: loading it with import() from this package wouldn't save a single byte.

In React the label doesn't name the editor yet

In React, CodeMirrorComponent renders the label without associating it with the editor, so a screen reader announces it with no name. The aria-labelledby association only exists in Vue.

Files

FileInputComponent

Drop or pick files, validate them with form-core's describeFiles and upload them to uploadUrl.

VueReactDefault
uploadUrluploadUrlrequired in Vue
methodmethod'POST'
autoUploadautoUploadfalse
namename'file'
visibilityvisibility'public'
maxSize, totalMaxSizesame0 (no limit)
maxFilesmaxFiles1
validMimesvalidMimesText, images, audio, video, PDF, Office and gzip
message, onDropMessage, onUploadMessage, maxFilesMessage, errorsTitlelabelsVue: separate strings. React: { drop, maxFiles, overTotal, upload, uploading, failed }
dropzoneClass, previewGridClass, hideOnMaxFilesReached, showTopPreview, showBottomPreview'drop-zone', 'fe-w-quarter', false, false, false
slots normalSlot, onDropSlot, onUploadSlot
events startUpload, updateFileList, endUploadonStartUpload, onFileListChange, onEndUpload

FileDropInputComponent

VueReactDefault
multiplemultiplefalse
mainText, subTextsame'Arrastra y suelta el archivo aqui', 'o haz clic para seleccionar los archivos.'
accept
change(files) eventonFilesChange(files)An array of File.

SimpleFileInputComponent

VueReactDefault
inputNameinputName'file'
labellabel'Seleccionar archivo'
customClasscustomClassnull
accept
input(file) eventonInput(file)

AvatarInputComponent

VueReactDefault
avatarUrl, uploadUrlsamerequired in Vue
uploadMethoduploadMethod'POST'
name'avatar'
upload(json) eventonUpload(json)The server response, already parsed as JSON.

In Vue the file is sent in a FormData under the file field. A failure is only logged to the console.

Basics

ButtonComponent

vue
<ButtonComponent value="Save" />
<ButtonComponent variant="secondary" type="button" value="Cancel" @click="close" />
jsx
<ButtonComponent value="Save" />
<ButtonComponent variant="secondary" type="button" value="Cancel" onClick={close} />
VueReactDefaultWhat it does
variantvariant'primary'secondary, danger or link pick the theme token; any other value uses button.
valuevalue'Enviar' in VueThe text.
typetype'submit'
disableddisabledfalse
customClasscustomClassunset
default slotchildren

IconComponent

VueReactDefaultWhat it does
namenamerequiredA name from the icon map, or an Iconify name.
sizesizenullWidth and height.
customClassclassNamenull

It is aria-hidden on purpose: a decorative icon next to its text shouldn't be read twice. When the icon is the only cue, such as a button with no text, the label belongs in the button's aria-label. A live setIcons() repaints it.

InputErrorComponent

PropWhat it does
errorsThe errors object from a Laravel 422, { field: [messages] }.
typeThe key this component looks at.

It renders one paragraph per message, with the error token's class, and nothing when there are no errors.

Desktop pieces

They rely on what the browser already does, with no UI library:

  • <dialog> with showModal() provides the top layer, the inert background, trapped focus, Escape and focus return.
  • The popover attribute provides light dismiss.
  • Floating UI positions menus.

Where showModal() or popover don't exist, as in jsdom or an old browser, the components fall back to the open and hidden attributes.

DialogComponent and DrawerComponent

vue
<DrawerComponent v-model:open="open" title="New product">
    <CreateForm @submit="save" />

    <template #footer="{ close }">
        <ButtonComponent variant="secondary" type="button" value="Cancel" @click="close" />
    </template>
</DrawerComponent>
jsx
<DrawerComponent
    open={open}
    onOpenChange={setOpen}
    title="New product"
    footer={({ close }) => (
        <ButtonComponent variant="secondary" type="button" value="Cancel" onClick={close} />
    )}>
    <CreateForm onSubmit={save} />
</DrawerComponent>
VueReactDefaultWhat it does
v-model:openopen + onOpenChange(open)false
titletitlenullNames the dialog through aria-labelledby.
labellabelnullaria-label when there's no title.
size (dialog only)size'md'sm, md or lg.
side (drawer only)side'end'end on the right, start on the left.
dismissibledismissibletrueWith false, Escape and the backdrop don't close it and there's no X.
closeLabelcloseLabel'Cerrar'The X button's aria-label.
close eventonCloseEmitted together with update:open set to false.
header slotheaderReplaces the title.
default slot with { close }children (node or ({ close }) => … function)
footer slot with { close }footer (node or function)
  • open is in charge. Escape, a backdrop click or the X only request closing through update:open / onOpenChange(false): whoever opened it closes it. If the <dialog> kept its own state, it would drift from your form's boolean.
  • The content only exists while it is open. A form comes back clean every time, and two copies of the same form don't collide on DOM ids.
  • Focus on open. In Vue, use the usual autofocus attribute. In React, mark the element with data-autofocus: React doesn't write autofocus to the DOM.
vue
<MenuComponent
    :items="[
        { id: 'edit', label: 'Edit', icon: 'edit', action: edit },
        { separator: true },
        { id: 'delete', label: 'Delete', icon: 'delete', danger: true, disabled: ! allowed, disabledReason: 'Not allowed' },
    ]"
    :before-open="loadPermissions"
    @select="(item) => console.log(item.id)" />
jsx
<MenuComponent
    items={[
        { id: 'edit', label: 'Edit', icon: 'edit', action: edit },
        { separator: true },
        { id: 'delete', label: 'Delete', icon: 'delete', danger: true, disabled: ! allowed, disabledReason: 'Not allowed' },
    ]}
    beforeOpen={loadPermissions}
    onSelect={(item) => console.log(item.id)} />
VueReactDefaultWhat it does
itemsitems[]The items (below).
labellabel'Acciones'Name of the menu and its button.
iconicon'more'The default button's icon.
placementplacement'bottom-end'Floating UI placement.
beforeOpenbeforeOpennullA function (can be async) awaited before opening.
events select, open, closeonSelect, onOpen, onClose
trigger slot with { toggle, open, loading, triggerProps }renderTrigger({ toggle, open, loading, triggerProps })Replaces the button. Put triggerProps on your button: they're the ARIA attributes.
exposes open() and close()

An item can be:

  • An action: { id, label, icon, shortcut, danger, disabled, disabledReason, action }. action(item) is called when it's chosen, after select.
  • A separator: { separator: true }.
  • A group heading: { group: 'Text' }.

And this is how it behaves:

  • beforeOpen is awaited before opening. Meanwhile the button is disabled. It's there for a row's permissions: opening with everything disabled and enabling it later makes users watch what they can't do flicker.
  • An item without permission doesn't disappear. It shows as disabled (aria-disabled) and explains why in data-tooltip: someone who can't do something should know the action exists.
  • Keyboard. Arrow keys, Home and End move through the usable items. Opening moves focus to the first item; closing returns it to the button.

CommandPaletteComponent

vue
<CommandPaletteComponent
    v-model:open="palette"
    :items="[
        { id: 'products', label: 'Products', group: 'Go to', icon: 'box', action: goToProducts },
        { id: 'new', label: 'New product', group: 'Create', shortcut: 'N', keywords: ['add'], action: createProduct },
    ]" />
jsx
<CommandPaletteComponent
    open={palette}
    onOpenChange={setPalette}
    items={[
        { id: 'products', label: 'Products', group: 'Go to', icon: 'box', action: goToProducts },
        { id: 'new', label: 'New product', group: 'Create', shortcut: 'N', keywords: ['add'], action: createProduct },
    ]} />
VueReactDefaultWhat it does
v-model:openopen + onOpenChangefalse
itemsitems[]{ id, label, group, icon, shortcut, keywords, action }
placeholderplaceholder'Buscar…'
emptyTextemptyText'Sin resultados'
labellabel'Paleta de comandos'
hotkeyhotkey'k'With Ctrl or Cmd it opens and closes the palette. null removes the shortcut.
select eventonSelectEmitted before action(item) is called.
  • The filter ignores case and accents: typing «configuracion» finds «Configuración». It searches label, group and keywords.
  • Groups are shown in the order they first appear.
  • Keyboard. Arrow keys move through the list and Enter runs the item. Opening clears the search and focuses the input.

SkeletonComponent

vue
<SkeletonComponent :lines="3" />
<SkeletonComponent shape="circle" :width="40" :height="40" />
<SkeletonComponent shape="block" height="12rem" />
jsx
<SkeletonComponent lines={3} />
<SkeletonComponent shape="circle" width={40} height={40} />
<SkeletonComponent shape="block" height="12rem" />
PropDefaultWhat it does
shape'text'text, circle or block.
lines1Only with text. With several lines, the last one is 60% wide, so it reads as a paragraph rather than a table.
width, heightnullA number means pixels; a string, any CSS length.

It is aria-hidden: what announces loading to a screen reader is the aria-busy of the container waiting for it, not each rectangle.

ClickToEditComponent

A value that is edited where it is: in a table cell or on a detail card.

vue
<ClickToEditComponent
    :value="product.title"
    label="Title"
    :save="(title) => updateModel(product.id, { title })" />
jsx
<ClickToEditComponent
    value={product.title}
    label="Title"
    onSave={(title) => updateModel(product.id, { title })} />
VueReactDefaultWhat it does
valuevalue''
typetype'text'The edit field's type.
placeholderplaceholder'—'What shows when the value is empty.
labellabel'Editar'The aria-label of the button and the field.
saveonSavenullFunction that saves; it is awaited.
input eventonInputThe confirmed value.
customClasscustomClassunsetReplaces the field's class.
exposes start()Opens editing.
  • Confirm and cancel. Enter or leaving the field confirms; Escape cancels.
  • No change, nothing happens. A value equal to the previous one neither saves nor emits.
  • With save, confirming waits for it to finish. While saving, the field is disabled.
  • If save throws, the cell stays open showing the error's message, or «No se pudo guardar». Closing it would show a value that wasn't saved.
  • Vue and React used to behave differently: leaving the field kept it open in Vue and confirmed it in React. Now both behave the same.

ToastRegionComponent and ConfirmHostComponent

vue
<!-- At the application root, exactly once -->
<ToastRegionComponent label="Notifications" close-label="Close" />
<ConfirmHostComponent close-label="Close" />
jsx
{/* At the application root, exactly once */}
<ToastRegionComponent label="Notifications" closeLabel="Close" />
<ConfirmHostComponent closeLabel="Close" />
ComponentPropsWhat it renders
ToastRegionComponentlabel ('Avisos'), closeLabel ('Cerrar')form-core's notify() queue
ConfirmHostComponentcloseLabel ('Cerrar')form-core's confirmAction() question
  • The toast region lives in the top layer with popover="manual". Otherwise a toast arriving while a drawer is open would sit under the inert backdrop, which is exactly where a form gets saved. If a dialog opens afterwards, the region shows itself again to stay on top.
  • Roles. A danger toast has role="alert"; the others, role="status".
  • The confirmation is a small dialog that starts with focus on cancel: a reflexive Enter shouldn't delete anything. Escape or a click outside count as cancel.

What gets mounted once

ComponentOnly onceWhy
ToastRegionComponentYes, at the rootThere is a single toast queue for the whole application. Two regions would render every toast twice; none would render nothing, even though notify() keeps filling the queue.
ConfirmHostComponentYes, at the rootEvery host listens to the same confirmation: with two, the question opens twice. With none, confirmAction() falls back to window.confirm.
CommandPaletteComponentYesIt listens for the shortcut on window when mounted. Two mounted palettes would both respond to Ctrl+K.
DialogComponent, DrawerComponent, MenuComponentNoEach instance is independent; its content only exists while open.

Vue ↔ React mapping

The general shape

VueReact
v-modelvalue + onChange(value)
v-model:openopen + onOpenChange(open)
:custom-classcustomClass
min_length / max_lengthminLength / maxLength (the underscore names are accepted too)
default slotchildren
scoped slot (footer, header with { close })a prop with a node or a ({ close }) => … function
MenuComponent's trigger slotrenderTrigger({ toggle, open, loading, triggerProps })
@eventonEvent
:mask-format (the v-format directive inside)maskFormat
autofocus attribute inside a dialogdata-autofocus
defineExpose (view, open(), close(), start())not available in these components
app.use(FormElements)not available

Per-component differences

ComponentVueReact
TextInputComponentreadonly, autofocus, autocompletereadOnly, autoFocus, autoComplete, id
SelectSearchInputComponentinputLabel and all vue-select props; remote search with ajax; @search(response)inputLabel, react-select props; onSearch(term); hidden input with name
ModelSearchInputComponentgetOptionLabel (function), debounceTime, noOptionsTextoptionLabel (key), debounce, minLength
SingleCheckboxInputComponentv-model:checkedchecked + onCheckedChange
MultiCheckboxInputComponentv-model:valuevalue + onChange
SwitchComponent@change(event)onToggle(event)
StarsInputComponentreadonly, colors and slotsreadOnly
CountrySelectInputComponent@change({ phone, country, isValid }) with a national phoneonCountryChange({ phone, country, callingCode, national, isValid })
CodeInputComponent3 boxes of 56 px; @complete(boolean)6 boxes of 40 px; onComplete(code) when complete
PolymorphicInputComponentprops propconfig prop (accepts props); switch and editor types
EditorInputComponenttinymceCdn, file upload, output, extraConfigtinymceScriptSrc, apiKey, validators
TextEditorMonoStyleInputComponentTinyMCE with dictationCodeMirror in html mode
CodeMirrorComponentlang, theme auto/dark/light, autofocus, exposes viewlanguage, theme dark/light, height, readOnly, name
FileInputComponentseparate strings and slots; @updateFileListlabels; onFileListChange
FileDropInputComponent, SimpleFileInputComponent@change, @inputonFilesChange, onInput, accept
AvatarInputComponent@uploadonUpload, name
IconComponentcustomClassclassName
ClickToEditComponent:save, @input, exposes start()onSave, onInput
MenuComponent@select, @open, @close, exposes open() and close()onSelect, onOpen, onClose

Why some differences are deliberate

  • MultiCheckboxInputComponent. React derives the selection from the value instead of reading the DOM, so two groups with the same id don't interfere.
  • Components that wrap a library with no input of its own publish the value in an <input type="hidden"> with name and data-validators, so the validator can find it. In React these are SelectSearchInputComponent, ColorPickerInputComponent, CodeMirrorComponent and EditorInputComponent.
  • In React, passing id changes both the control's id and its label's for. Without that, the label would point at nothing.