Skip to content

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.

Use { } to insert a reference.
A contenteditable in HabFieldBox, under an ordinary HabInput: the same label, box, focus border and help. Click the label to focus the editor.
vue
<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 propPut it on the control as
idid. The label's for points at it; pass control-id to keep your own.
labelledbyaria-labelledby, for a control for cannot reach (see below).
describedbyaria-describedby: the help line, so help and errors are announced.
invalidaria-invalid while state is error.
readonlyaria-readonly, and refuse edits. Includes a surrounding HabReadonly.
disabledaria-disabled, no tabindex, and refuse edits while state="disabled".
controlClassclass: 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 ​

Hello {name}
A reference to {nmae} does not exist.
Hello {name}
Published templates are read-only.
Hello {name}
Choose a channel first.
The box's states around the same contenteditable, each wired through the slot.

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 ​

PropTypeDefaultDescription
controlIdstringundefinedThe 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.
growbooleanfalseA 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.
helpstringundefinedHelper text below the box. Its id is the slot's describedby, and it is rendered in --hab-error-text when state is error.
labelstringundefinedVisible 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.
markerbooleanfalseMarks 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.
multilinebooleanfalseA 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.
readonlybooleanundefinedThe 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.
resizablebooleanfalseLets 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 ​

SlotBindingsDescription
appendanyAffordances 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.
appendAddonanyprependAddon'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.
leadinganyA glyph before the control, inside the padding and in the quiet ink.
prependAddonanyA 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.

Proprietary — internal use only.