HabFieldBox
The field's chrome without its control: label above, the bordered box with its states, the addon segments, and help below — around a control Habitus does not ship. It is what HabInput and the boxed HabSwitch are drawn in, so a control of your own in a column of fields looks and behaves like the fields around it.
<HabFieldBox label="Body" help="Use { } to insert a reference." multiline resizable>
<template #default="{ id, labelledby, describedby, invalid, readonly, disabled, controlClass }">
<div
:id="id"
role="textbox"
aria-multiline="true"
:contenteditable="readonly || disabled ? 'false' : 'true'"
:tabindex="disabled ? undefined : 0"
:aria-labelledby="labelledby"
:aria-describedby="describedby"
:aria-invalid="invalid || undefined"
:aria-readonly="readonly || undefined"
:aria-disabled="disabled || undefined"
:class="[controlClass, 'min-h-[3lh] whitespace-pre-wrap']"
/>
</template>
<template #append>…an insert button…</template>
</HabFieldBox>When to use it
For a value whose control Habitus does not have, but which sits among fields: a rich-text or token editor with inline chips, a code input, a value drawn by a third-party widget. The box gives it the label, the border, focus, error, read-only and disabled looks, density and the prependAddon / appendAddon segments — so none of it has to be copied by hand with utilities, where it drifts from HabInput the first time either changes.
Not for anything Habitus ships. A text value is a HabInput, a choice a HabSelect or HabAutocomplete, a true/false value a HabSwitch with boxed.
What the slot receives
The default slot is the control. The box renders no control of its own, so it hands the control everything it needs to be wired into the field:
| Slot prop | Put it on the control as |
|---|---|
id | id. The label's for points at it; pass control-id to keep your own. |
labelledby | aria-labelledby, for a control for cannot reach (see below). |
describedby | aria-describedby: the help line, so help and errors are announced. |
invalid | aria-invalid while state is error. |
readonly | aria-readonly, and refuse edits. Includes a surrounding HabReadonly. |
disabled | aria-disabled, no tabindex, and refuse edits while state="disabled". |
controlClass | class: borderless, transparent, as wide as the box, the density's type. |
for labels only native form controls. An <input> or <textarea> in the slot is labelled by for as on HabInput. A contenteditable or a role="textbox" div is not labelable, so once the box sees one it drops for from the label — invalid there, and it would focus nothing — and focuses the control itself when the label is clicked. Name such a control with aria-labelledby="labelledby", or it has no accessible name at all.
States
state and readonly are the same as HabInput's: error moves the border and inks the help line, disabled takes the fill and the ink, read-only takes the raised surface and drops the border. marker is the provisional fill. Error and disabled outrank read-only and the marker.
Focus is the box's: .hab-input:focus-within turns its border accent, whichever element inside has focus. The control draws no indicator of its own — base.css drops the native outline of an input, a textarea and a contenteditable in the box. A focusable control of another kind should draw none either.
Multiline
multiline top-aligns the box and lets it grow with its content, with vertical padding instead of a fixed height; controlClass then stretches the control to the box's height. The box's resting height is its content's, so a control that should start taller sets its own min-height — min-h-[3lh] is three lines of its own leading. resizable adds a grip at the corner (not while read-only). The addon segments are not drawn in a multiline box.
One line that may wrap
grow is for a value that is one line but cannot promise to stay one: a token editor whose references do not fit. At rest the box is exactly a one-line field's height, its value and anything in append sitting where HabInput's do, so it lines up in a column of fields; a wrapped value pushes the box taller instead of spilling out of it. Like multiline, it draws no addon segments.
Slots around the control
leading holds a glyph before the control and append affordances after it, both inside the box's padding in the quiet ink — an insert button, an info tooltip. Each carries its own accessible name. prependAddon and appendAddon are the flush segments at either end, for a HabFieldAddon, which follows the field's read-only and disabled state without being told.
Accessibility
What the box guarantees: a visible label with an id, the help line with an id, focus shown on the box (and as a system-colour outline under forced colors), and the field's state handed to the addons.
What the control must do, with the slot props above: carry id, be named by the label (for on a native control, aria-labelledby otherwise), be described by describedby, set aria-invalid, aria-readonly and aria-disabled from the state, and — for a custom widget — take its role (role="textbox", aria-multiline) and its keyboard behaviour itself. The box cannot see inside the control, so it cannot check any of this.
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
controlId | string | undefined | The control's own id, when it already has one. Otherwise the box generates one and hands it to the slot as id; either way the label and the slot agree on it. |
density | "default" | "compact" | "default" | Box height: --hab-control-md, or --hab-control-sm when compact. Ignored when multiline. |
grow | boolean | false | A one-line box that grows when its control wraps — a token editor whose references do not fit on one line. At rest it is exactly a one-line field's height, its value and anything in append sitting where a one-line field's do; a wrapped value pushes the box taller rather than overflowing it. The addon segments are not drawn: they are sized to one line. Ignored when multiline. |
help | string | undefined | Helper text below the box. Its id is the slot's describedby, and it is rendered in --hab-error-text when state is error. |
label | string | undefined | Visible label above the box. Points at the control by for when the control is a native labelable element; otherwise bind the slot's labelledby to the control's aria-labelledby. |
marker | boolean | false | Marks the value as provisional — drafted by an AI, carried in from an import. A faint accent fill and accent help ink, as on HabInput. Not a validation state: error and disabled keep their own look. |
multiline | boolean | false | A box for a control that wraps onto more lines: top-aligned, as tall as its content, with vertical padding instead of a fixed height. The addon segments are not drawn — they belong to a one-line value. |
readonly | boolean | undefined | The value is shown to be read, not edited: the read-only box, and the slot's readonly is true. Unset, it follows a surrounding HabReadonly; false keeps the field editable inside one. |
resizable | boolean | false | Lets the user drag a multiline box taller, by a grip at its corner. Not while read-only: there is nothing to make room for. Ignored when not multiline. |
state | "default" | "disabled" | "error" | "default" | Validation and availability state. The slot's invalid and disabled follow it. |
Slots
| Slot | Bindings | Description |
|---|---|---|
append | any | Affordances after the control, inside the padding and in the quiet ink — an insert button, an info tooltip. Each carries its own accessible name; the box names none of them. |
appendAddon | any | prependAddon's counterpart at the end of the box, after append. One-line boxes only. |
default | { id: string; labelledby: string; describedby: string; invalid: boolean; readonly: boolean; disabled: boolean; controlClass: string } | The control itself. It receives its wiring — see HabFieldBoxControl — and should take controlClass, which makes it borderless, transparent and as wide as the box allows. |
leading | any | A glyph before the control, inside the padding and in the quiet ink. |
prependAddon | any | A control fused to the start of the box — usually a HabFieldAddon. Flush, full height and divided from the value by a line; it follows the field's read-only and disabled state without being told. One-line boxes only: a multiline box does not draw it. |