CtrlK
BlogDocsLog inGet started
Tessl Logo

bestax-form

Build forms with @allxsmith/bestax-bulma — Field/Control/Label/Help composition, inputs, selects, checkboxes, radios, switches, and advanced controls (Autocomplete, Slider, Numberinput, 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.

76

Quality

95%

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 <Field.Label> when horizontal
└── 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" />
  </Control>
  <Control>
    <Button color="primary">Go</Button>
  </Control>
</Field>

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.
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.
SwitchToggle switch (isRounded, isThin, isOutlined, RTL).
FileFile upload input with label/message.
AutocompleteInput with filtered dropdown suggestions + keyboard nav.
SliderRange slider; single/dual thumbs, steps, tooltips, vertical.
NumberinputNumeric input with increment/decrement, min/max, step, stepper.
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.)

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.

The label prop on the single-control convenience inputs (Input, Select, TextArea, File, Numberinput, Slider, DateInput, TimeInput, DateTimeInput, Autocomplete, Taginput) wires htmlFor/id automatically — your id is used when provided, a generated one otherwise, and an explicit labelProps={{ htmlFor }} wins. The wiring only happens when the component renders its own Field (nested inside one, the label prop is dropped); the date/time pickers skip it in inline mode and Taginput skips it at maxTags (no visible input to label). A range Slider points the for at its low thumb and also puts the label at the start of both thumbs' names through aria-labelledby ("Price range Minimum value"). Its ariaLabel={[low, high]} replaces those names outright, so leave it off when a label already names the Slider. The group inputs (Checkboxes, Radios, Rate, DateRangeInput) associate their label too, but group-style: the wrapper gets role="group"/"radiogroup" and aria-labelledby pointing at the label. Composing yourself also associates: a labeled Field names the one control it holds, whether a composed InputBase/SelectBase/TextAreaBase or any input above (through the id), or a group (through aria-labelledby). Either way, an aria-label or aria-labelledby you give a group wins over the label. A Checkbox, Radio or Switch takes nothing from a Field: each is named by its own children, so put the text there. The association is skipped for grouped/hasAddons, and a nested Field starts its own scope, so a horizontal Field whose body holds an inner Field needs labelProps={{ htmlFor }} plus the control's id (for a group, labelProps={{ id, htmlFor: undefined }} plus its aria-labelledby, since nothing takes the label's for there). Pass labelProps={{ htmlFor }} plus a matching id only when you want a stable id, or labelProps={{ htmlFor: undefined }} to opt out.

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, Slider, Numberinput, Rate, Taginput, and the date/time inputs (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-base composition all do this automatically; pass labelProps={{ htmlFor }} plus a matching id only for a stable id, and label a multi-control Field's controls 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
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.