CtrlK
BlogDocsLog inGet started
Tessl Logo
SkillPublic

bestax-form

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.

Quality

90%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

Building forms with bestax-bulma

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.

Use when

  • Building a form out of bestax inputs, selects, checkboxes, switches, or advanced controls.
  • Deciding between the convenience components (<Input label=… />) and explicit Field + Control + *Base composition.
  • Showing help text and error/success states on fields.

To build a brand-new input component (not just use the existing ones), use the bestax-custom-component skill instead.

Field / Control / Label / Help composition

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 message

You 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>

Layout via 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").

Sections via Fieldset

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.

Component inventory

All import from @allxsmith/bestax-bulma. Convenience components auto-wrap Field+Control; *Base components are the raw styled elements for explicit composition.

ComponentWhat it is
Field, Field.Label, Field.BodyField container + horizontal label/body parts.
ControlWraps one input; left/right icons, isLoading, isExpanded, size.
FieldsetA section named by its legend; disabled switches off all of it.
Input / InputBaseText input (convenience / raw).
Select / SelectBaseDropdown select.
TextArea / TextAreaBaseMultiline text.
Checkbox / CheckboxesSingle checkbox / managed group (array value).
Radio / RadiosSingle radio / managed single-select group.
CheckboxCard / RadioCardA checkbox or radio as a card with a title and a description.
SwitchToggle switch (isRounded, isThin, isOutlined, RTL).
FileFile upload input with label/message.
AutocompleteFree text with suggestions it filters for you, + keyboard nav.
ComboboxA value from a list: data for a select, tags or groups, or parts.
SliderRange slider; single/dual thumbs, steps, tooltips, vertical.
NumberinputNumeric input with increment/decrement, min/max, step, stepper.
PinInputA one-time code or PIN as a row of boxes, all in one real input.
RateStar rating; max, precision (half/quarter), custom icons, disabled.
TaginputTag/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.

Common props

Across the convenience inputs (Input, Select, TextArea, and similar):

PropTypePurpose
color'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger'Visual state — use 'danger' for errors, 'success' for valid.
size'small' | 'medium' | 'large'Input size.
value / onChangecontrolled value + handlerStandard React controlled inputs.
defaultValueuncontrolled initial valueWhen not controlling state.
disabled, readOnlybooleanNative states (readOnly on *Base).
labelReactNodeField label (convenience components; auto-associated via htmlFor).
messageReactNodeHelp / validation text rendered as <p class="help">.
messageColora Bulma colorColors the help text ('danger' for errors).
iconLeftName / iconRightNamestringIcon shortcuts; pair with hasIconsLeft/Right.
isLoadingbooleanLoading 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.

Convenience vs composed

  • Convenience (<Input label message … />) — for typical, single-control fields. Fewer lines, auto-wrapping, built-in message/messageColor. Default to this.
  • Composed (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.

Validation without a library

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:

  • Set 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.
  • Want a different validation library? Wire it up yourself — pass its value/onChange/error string into these props. bestax does not prescribe one.
  • For grouped controls, render the <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.

Reuse the shipped components

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.

What happens after submit

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:

  • Confirmation — 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.
  • "Are you sure?" — 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.

Visually inspect it in a browser

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.

Checklist

  • Built from the shipped form components (no hand-rolled inputs / reinvented controls).
  • Every label is programmatically associated: the convenience 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).
  • Controlled inputs have both value and onChange (or use defaultValue uncontrolled).
  • Error state shows via color="danger" + message + messageColor="danger".
  • Grouped/addon layouts use explicit Field + Control composition.
  • No assumption of a built-in validation/form library — state is owned by the app.
  • Submit feedback is a Toast and any "are you sure?" is a Dialog — not a hand-placed Notification or a Modal you filled in yourself.
  • Rendered and visually inspected in a browser — layout and the error/validation states look right, not just green tests. No browser available? The renderToString fallback above counts only if you grepped the emitted classes/states and said the visual pass is owed.
Repository

allxsmith/bestax

Path

skills/bestax-form/SKILL.md

Last updated
First committed

Is this your skill?

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.