Build forms with @allxsmith/bestax-bulma — Field/Control/Label/Help composition, inputs, selects, checkboxes, radios, switches, and advanced controls (Autocomplete, Combobox, Slider, Numberinput, PinInput, Rate, Taginput, File, date/time). There is no form/validation library; this skill shows the components and the validate-it-yourself error pattern. Use when building a form, wiring inputs to state, or showing validation/error state.
90%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
This skill covers the form components in @allxsmith/bestax-bulma and how to compose them.
Important: bestax-bulma ships no form/validation library — there is no integration with
formik, react-hook-form, yup, or zod, and no useForm-style hook. You own your form state with
plain React (useState / useReducer or any library you choose) and feed validation results
back via each input's own color, message, and messageColor props on the convenience inputs
(Input, Select, TextArea, …). Always put validation state on the input: Field has no
message/messageColor, and although FieldProps types a color, Field discards it — it
renders no class, so setting it looks right and does nothing. (FieldLabel/FieldBody do honor
color, as the has-text-* helper.) See Validation without a library below.
<Input label=… />) and explicit
Field + Control + *Base composition.To build a brand-new input component (not just use the existing ones), use the
bestax-custom-component skill instead.
Bulma forms are a three-tier structure. bestax models it directly:
Field // container + layout (horizontal / grouped / hasAddons)
├── label // rendered from Field's `label` prop, or a <label> you put in <Field.Label>
└── Control // wraps ONE input; adds icons + loading
├── InputBase / SelectBase / TextAreaBase // the raw styled element
└── <p class="help">…</p> // help / validation messageYou rarely write all of this by hand. The convenience components (Input, Select,
TextArea, …) auto-wrap themselves in Field + Control when they aren't already inside one,
using context (useInsideField / useInsideControl) to detect their surroundings.
// Convenience: one line, auto-wrapped in Field + Control.
<Input label="Email" type="email" placeholder="you@example.com" />
// Explicit composition: full control over layout.
<Field label="Email">
<Control iconLeftName="envelope" hasIconsLeft>
<InputBase type="email" placeholder="you@example.com" />
</Control>
</Field>horizontal — label and control side by side (wraps children in Field.Body; use
Field.Label / Field.Body directly for multi-control rows).grouped — true | 'centered' | 'right' | 'multiline', controls in a row.hasAddons — true | 'centered' | 'right', attached controls (input + button).<Field hasAddons>
<Control isExpanded>
<InputBase placeholder="Search" aria-label="Search" />
</Control>
<Control>
<Button color="primary">Go</Button>
</Control>
</Field>A grouped or hasAddons Field names none of its controls, and a placeholder disappears once
the user types, so give each input in the row an aria-label, or a visible label on an outer
Field (references/patterns.md, "Grouped controls and addons").
Fieldset groups several fields under one name: a native <fieldset> whose legend names the
group. variant is default (bordered), filled or unstyled. disabled disables every
control inside, the native ones and bestax's own widgets (Rate, the time wheels) alike, so it's
also how to switch a whole form off while it saves. message describes the group; each input
still carries its own error.
<Fieldset legend="Shipping address" disabled={saving}>
<Input label="Street" name="street" />
<Input label="City" name="city" />
</Fieldset>Prefer it to a hand-written <fieldset disabled>, which the browser applies to native controls
only: a Rate inside one keeps taking input while its value is dropped from the form.
All import from @allxsmith/bestax-bulma. Convenience components auto-wrap Field+Control;
*Base components are the raw styled elements for explicit composition.
| Component | What it is |
|---|---|
Field, Field.Label, Field.Body | Field container + horizontal label/body parts. |
Control | Wraps one input; left/right icons, isLoading, isExpanded, size. |
Fieldset | A section named by its legend; disabled switches off all of it. |
Input / InputBase | Text input (convenience / raw). |
Select / SelectBase | Dropdown select. |
TextArea / TextAreaBase | Multiline text. |
Checkbox / Checkboxes | Single checkbox / managed group (array value). |
Radio / Radios | Single radio / managed single-select group. |
CheckboxCard / RadioCard | A checkbox or radio as a card with a title and a description. |
Switch | Toggle switch (isRounded, isThin, isOutlined, RTL). |
File | File upload input with label/message. |
Autocomplete | Free text with suggestions it filters for you, + keyboard nav. |
Combobox | A value from a list: data for a select, tags or groups, or parts. |
Slider | Range slider; single/dual thumbs, steps, tooltips, vertical. |
Numberinput | Numeric input with increment/decrement, min/max, step, stepper. |
PinInput | A one-time code or PIN as a row of boxes, all in one real input. |
Rate | Star rating; max, precision (half/quarter), custom icons, disabled. |
Taginput | Tag/chip input; suggestions, confirm keys, closable tags. |
DateInput / TimeInput / DateTimeInput (+ *Base) | Date / time / datetime pickers; month or year via granularity. |
DateRangeInput (+ *Base) | Start and end date: [Date | null, Date | null], one calendar. |
(NumberInput and TagInput also exist as deprecated aliases of Numberinput/Taginput —
same components; prefer the lowercase-second-word spellings.)
Autocomplete or Combobox? A field whose text is the value, with suggestions to help type it,
is Autocomplete, which filters its data itself. A value picked from a list is Combobox. Given
data, it filters by label, keeps the value (value/onChange) and shows the picks as tags with
multiple, so a searchable select, a multi-select or a grouped list is one element. Build it from
parts for a server search whose answer must show unfiltered, or a "Create" item: then it keeps no
value and never sets the field's text, so you filter the items, keep the value and set the text in
onSelect.
A verification code or a PIN is PinInput, not a row of one-character Inputs with focus
moved by hand. It holds the code in one real input under a row of boxes, so a screen reader hears
one field named by its label, and paste, autofill from a text message (autoComplete is
"one-time-code") and password managers work as they do on any input. Give it length (6 by
default), value/onChange with the code as a string, and verify in onComplete, which fires
once each time a change fills the last box. It takes digits unless type="alphanumeric", turns a
digit from any script (Arabic-Indic, Persian) into its ASCII digit, drops anything else typed or
pasted, and isMasked shows dots for a PIN. A paste replaces the whole code. Its default pattern
asks for the full length, so required rejects a partial code, not just an empty one. Say the
length in the label ("Enter the 6-digit code"): the boxes are hidden from a screen reader, and the
message isn't tied to the input, so it isn't read on focus. Separate help text needs
aria-describedby on the PinInput.
Across the convenience inputs (Input, Select, TextArea, and similar):
| Prop | Type | Purpose |
|---|---|---|
color | 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger' | Visual state — use 'danger' for errors, 'success' for valid. |
size | 'small' | 'medium' | 'large' | Input size. |
value / onChange | controlled value + handler | Standard React controlled inputs. |
defaultValue | uncontrolled initial value | When not controlling state. |
disabled, readOnly | boolean | Native states (readOnly on *Base). |
label | ReactNode | Field label (convenience components; auto-associated via htmlFor). |
message | ReactNode | Help / validation text rendered as <p class="help">. |
messageColor | a Bulma color | Colors the help text ('danger' for errors). |
iconLeftName / iconRightName | string | Icon shortcuts; pair with hasIconsLeft/Right. |
isLoading | boolean | Loading spinner on the Control; Select draws it in place of the chevron. |
Plus the full Bulma helper props (m, p, textColor, display, …) on every component
via useBulmaClasses.
Full-width is isFullwidth on every component that supports it (Button, LinkButton,
Select, File, Table, Tabs, Sidebar) — always write isFullwidth. The deprecated
spellings compile only where they historically existed: isFullWidth everywhere except
Sidebar, fullwidth on Tabs only, fullWidth on Sidebar only.
Labels. Default to the convenience label prop, with horizontal for a side-by-side row.
Input, Select, TextArea, File, Numberinput, PinInput, Slider, DateInput,
TimeInput, DateTimeInput, Autocomplete and Taginput then render the Field and the label
and wire htmlFor/id, to your id when you pass one
(<Input label="Email" id="email" horizontal />); an explicit labelProps={{ htmlFor }} wins. Combobox renders its Field and label too, and wires
them to its field. Given data, its id, aria-label and aria-labelledby go to the field;
built from parts, put them on Combobox.Input, since on the Combobox they land on its wrapper
div. The group inputs (Checkboxes, Radios, Rate, DateRangeInput) name their
role="group"/"radiogroup" wrapper through aria-labelledby instead. Checkboxes and Radios
also take as="fieldset", which renders their own Field as a native <fieldset> with the label
as its <legend> (the label column when horizontal), named by the legend with no ids. Inside a
Field a convenience input drops its label, and an inline date/time picker has no visible
input to wire.
Composed, a labeled Field names the one control it holds: a composed InputBase/SelectBase/
TextAreaBase, a composed DateInputBase/TimeInputBase/DateTimeInputBase that is not
inline, or any input above takes its generated id, and a group (a composed DateRangeInputBase
too, inline or not) points aria-labelledby at the label's own id. A control with an id of
its own never takes that id, so <Field label="Email"><Input id="email" /></Field> renders a
label that names nothing: leave the id off, or pass the same value as
labelProps={{ htmlFor: 'email' }}. Wire it that way too where the association is skipped, in a
grouped/hasAddons row and in a horizontal Field whose body holds an inner Field (a nested
Field starts its own scope). For a group there, put labelProps={{ id, htmlFor: undefined }} on
the outer Field and a matching aria-labelledby on the group; the htmlFor: undefined keeps an
unmatched for out of server-rendered HTML. For a Checkboxes or Radios row on its own,
<Checkboxes as="fieldset" horizontal label="…"> needs none of that.
A range Slider points the for at its low thumb and starts both thumbs' names with the label
("Price range Minimum value"); its ariaLabel={[low, high]} replaces those names outright, so
leave it off when a label already names the Slider. Those thumbs and an Autocomplete's, a
Combobox's or a Taginput's list point aria-labelledby at the label's id, which a label
wired by hand has only when you pass one, labelProps={{ htmlFor, id }}; without it they keep their
fallback names ("Minimum value"/"Maximum value", "Suggestions"). A <label htmlFor> you put in
Field.Label yourself (references/patterns.md) names its control through the for alone, so
label a row holding either with the Field's label prop. An aria-label or aria-labelledby
you give a group wins over any label, and a Checkbox, Radio or Switch takes nothing from a
Field on its own: its children name it, and a Field's label names one only when you wire it as
you would any control with an id of its own, labelProps={{ htmlFor }} plus that control's
id. A labeled Field whose for nothing takes (a group, a Checkbox, Radio or Switch, an
input with an id of its own, your own markup) drops it once mounted, though server-rendered HTML
keeps it. labelProps={{ htmlFor: undefined }} opts out of the association entirely.
<Input label message … />) — for typical, single-control fields. Fewer
lines, auto-wrapping, built-in message/messageColor. Default to this.Field + Control + InputBase) — when you need grouped controls, addons,
multiple controls per field, or custom layout. The convenience components detect they're
already inside a Field/Control and won't double-wrap, so you can mix the two. Inside a
Control they render no Control of their own, so set their Control-level props, such as
the icon props, isLoading and controlSize, on that Control: given to the input there,
they do nothing and warn in development. Select draws its own isLoading, so that one
stays on the Select. Inside a Control they render no Field either, so a convenience
input you place in a Control (for icons, say) takes its label, horizontal and class
name from a Field wrapped around that Control. Given label, message, horizontal or fieldClassName in a
Control with no Field around it, it renders its own Field inside the .control,
which Bulma's styles don't expect, and warns in development. Autocomplete and
Numberinput are the exception: in a bare Control they always render their own Field,
so give that Control a Field around it.There is no built-in validation. The pattern is: own your state, compute errors yourself, and
reflect them with color + message + messageColor.
import { useState } from 'react';
import { Input, Button } from '@allxsmith/bestax-bulma';
function SignupForm() {
const [email, setEmail] = useState('');
const [touched, setTouched] = useState(false);
const valid = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email);
const error =
touched && !valid ? 'Please enter a valid email address.' : undefined;
return (
<form
onSubmit={e => {
e.preventDefault();
setTouched(true);
if (valid) {
// submit…
}
}}
>
<Input
label="Email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
onBlur={() => setTouched(true)}
color={error ? 'danger' : undefined}
message={error}
messageColor={error ? 'danger' : undefined}
iconLeftName="envelope"
/>
<Button color="primary" type="submit" mt="3">
Sign up
</Button>
</form>
);
}Rules of thumb:
color="danger" on the input and messageColor="danger" on the help text so both the
control and the message read as an error. Use 'success' to signal a valid field.value/onChange/error
string into these props. bestax does not prescribe one.<p class="help"> via the message prop of the convenience
component, or add it manually inside the Field when composing.See references/api.md for per-component props and references/patterns.md for a full
multi-field form plus the advanced inputs.
bestax ships the whole form surface — Input, Select, TextArea, Checkbox(es), Radio(s), Switch, File,
Autocomplete, Combobox, Slider, Numberinput, PinInput, Rate, Taginput, the date/time inputs, and
Fieldset to group them (see the inventory above). Compose these; don't hand-roll raw <input class="input"> markup or
reinvent a control. If you think a control is missing, check bulma-ui/src/index.ts and
docs/docs/api/form/ first — it's probably already there under a different name.
A form is not finished at the last field. The two components that carry the result are easy to miss because Bulma has something that looks close:
Toast, not Notification/Message. Mount
<ToastContainer position="top-right" /> once at the app root, then call
toast.success('Demo booked') from the submit handler (.danger for a failed submit). It
self-dismisses.Dialog, not Modal. Mount <DialogContainer /> at the root, then
if (await dialog.confirm({ title: 'Delete this key?', message: '…', type: 'danger' })) ….
It resolves to a boolean, so a destructive action stays one if rather than a state machine.
Modal is an empty shell — with it you rebuild the title, message and button row by hand.Both also work as plain controlled components (<Toast message … onClose>,
<Dialog isOpen … onConfirm onCancel>) when the state should live in your component.
Forms have layout, spacing, and stateful behavior that types and unit tests don't cover.
Before calling a form done, render it and look at it: run pnpm storybook (in bulma-ui)
or the docs dev server, open the form, and check field alignment/spacing, the help-text/error
states, and the validation flow (submit empty → fields turn danger with messages; fix → errors
clear). If claude-in-chrome or Playwright is available, drive the browser and screenshot the
valid and error states; otherwise eyeball it yourself. No browser at all (headless CI)? Fall
back to a production build plus a Node renderToString smoke render, grep the emitted HTML
for the expected classes/states, and say plainly that the visual pass is still owed.
label prop, the group
inputs, and Field + single-control composition do it on their own. A composed labeled
Field names a control with an id of its own, or one in an inner Field, only when its
labelProps={{ htmlFor }} matches that id, and a multi-control Field's controls are
labeled individually (aria-label, aria-labelledby, or a <label htmlFor> matching
each control's id).value and onChange (or use defaultValue uncontrolled).color="danger" + message + messageColor="danger".Field + Control composition.Toast and any "are you sure?" is a Dialog — not a hand-placed
Notification or a Modal you filled in yourself.renderToString fallback above
counts only if you grepped the emitted classes/states and said the visual pass is owed.allxsmith/bestax
05bf0cb
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.