Skip to content

HabInput ​

The outlined text input — or a <textarea> with multiline. Label, icons and helper text come with it.

We only use this for release notices.
Label, control and help are three elements — the help is a description, not part of the name
vue
<HabInput v-model="email" label="Email" help="We only use this for release notices." />

Anything not consumed as a prop falls through to the control, not the wrapper: placeholder, autocomplete, required, maxlength all land where you would expect. class and style are the exception and stay on the wrapper, since that is the field's footprint.

States ​

That address is incomplete.
Managed by your administrator.
error moves the border only; disabled takes the fill and the ink too

error moves the border only. disabled also takes the fill and the ink, and disables the control and any trailing button.

vue
<HabInput label="Email" state="error" help="That address is incomplete." />

There is deliberately no focus state. Focus is owned by .hab-input:focus-within in base.css, which paints the box border in --hab-accent. A prop that painted the focused look would be a second source of truth, and product code could leave a field looking focused when it is not.

That is also why the two class hooks matter: .hab-input on the wrapper and .hab-input-box on the box. The control inside is borderless, so the native focus indicator has nothing to draw on — without the hooks the documented ring never fires. If you import /theme directly rather than /styles, you need /base too.

The label, the box, its states and the help line are HabFieldBox's, which HabInput is drawn in. For a value whose control Habitus does not ship — a token editor, a code input — put that control in a HabFieldBox rather than copying this box with utilities.

Density ​

compact shrinks the box AND the text — 28px / 13px

compact shrinks the box to --hab-control-sm and the text to --hab-fs-sm. It is ignored when multiline.

Icons ​

A trailing icon with a handler is a real button, with its own accessible name

A leading icon is always decorative. A trailingIcon is decoration too — until you attach a @trailing-click listener, at which point it becomes a real <button> and needs its own name:

vue
<HabInput
	v-model="secret"
	label="Password"
	:type="revealed ? 'text' : 'password'"
	:trailing-icon="revealed ? mdiEyeOff : mdiEye"
	:trailing-icon-label="revealed ? 'Hide password' : 'Show password'"
	@trailing-click="revealed = !revealed"
/>

Give it trailing-icon-label. The button does something different from the field, so borrowing the field's label would announce a password reveal as "Password". Habitus warns when the label is missing.

A provisional value ​

From the AI, not verified
A provisional value, and two append affordances beside the control
vue
<HabInput v-model="term" label="Term" help="From the AI, not verified" marker />

marker says the value came from somewhere that has not been checked yet — drafted by an AI, carried in from an import, inherited from elsewhere. The box takes a faint accent fill and the help line takes accent ink.

It is a flag beside state, not a member of it, because nothing is wrong with the value and that can be true of a field which is also in error. error and disabled keep their own fill: a state the user has to act on outranks a note about where the value came from. It sets no aria-invalid.

It draws no glyph and writes no words. What the mark means is the host application's to say — in help, and in whatever it puts beside the label.

More than one trailing affordance ​

The append slot puts nodes at the end of the box, after the trailing button rather than inside it.

trailing replaces one glyph and keeps its button, which is what protects trailingIconLabel and the click handler from being silently dropped. But a field that needs a second and a third affordance — an info tooltip beside a generate button — cannot be expressed that way, because trailingIcon is one icon and no node. append is where those go, and each one carries its own accessible name; the component names none of them.

A control fused to the start ​

The prependAddon slot holds a control that belongs to the field rather than sitting beside it — where the value comes from, chosen from a menu. It sits flush at the start of the box, runs its full height and is divided from the value by a line, so the two read as one field. Put a HabFieldAddon in it; the same slot is on HabSelect, HabAutocomplete and HabTreeSelect. A multiline field ignores it.

A control fused to the end ​

appendAddon is prependAddon's counterpart at the end of the box: flush, full height, divided from the value by a line — for a toggle that changes how the value is read, such as reading it as an expression. Put a HabFieldAddon with pressed in it. It comes after append, which stays where it was: inside the padding, at glyph size. A multiline field ignores it.

Only HabInput has it. The other fields' boxes end in their chevron — HabSelect's inside its trigger button, which cannot hold a second button; HabTreeSelect's always; HabAutocomplete's when chevron is set — and none of them has needed an end segment.

Which end slot when ​

After the value, in this order:

Slot or propWhat it isUse it for
trailingIconOne glyph, as path data. Decoration — until a @trailing-click listener is attached, when it becomes a real <button> named by trailingIconLabel.A single action on the value: reveal a password.
#trailingAny node in the glyph's place. With @trailing-click it stays inside that button, so the handler and the name survive the swap.The same single action, drawn with your own node.
#appendNodes after the trailing glyph or button, inside the box's end padding, at glyph size. The component names none of them; each carries its own name.A second and third affordance: info tooltip, generate.
#appendAddonA segment at the very end: flush, full height, divided from the value by a line. Single-line fields only.A HabFieldAddon toggle on how the value is read.

Multiline ​

Markdown is not rendered here.
multiline tops-aligns the box and lets the control resize vertically
vue
<HabInput v-model="notes" multiline label="Notes" :rows="4" />

The box tops-aligns, re-pads to 8px/12px, and resizes vertically. The box carries the resize rather than the textarea, so the grip sits in the field's corner, outside the padding and after anything in append; the textarea stretches to fill it. A drag only makes the field bigger: the box never gets shorter than its own content, so it cannot clip what sits in append.

Because the box owns the resize, it also owns the clipping: anything in append that renders in flow and overflows is cut off. HabTooltip and HabPopover portal, so they are unaffected; a hand-rolled dropdown in that slot would not be.

auto-grow lets the field grow with its text instead of scrolling inside it; rows becomes the height it starts from, and the grip still makes it taller.

Grows with its text; the grip still makes it taller.
Stays at three rows and scrolls.
Same text, same rows — auto-grow on the left, the default on the right
vue
<HabInput v-model="description" multiline auto-grow :rows="3" label="Description" />

It uses CSS field-sizing: content. Where that is unsupported, the field keeps its fixed rows height and scrolls — the same as without the flag.

Code values ​

mono sets the value in the mono face — the <input>, or the <textarea> when multiline — for a value that is code rather than words: an expression, an environment variable's name, an attribute path. The label and help stay in the sans. It replaces the field's sans rather than adding to it, which a font-mono class reaching in from outside ([&_input]:font-mono) could only do by winning on stylesheet order.

vue
<HabInput v-model="name" label="Variable" mono />

Read-only ​

readonly shows the value to be read: full ink on the raised surface, no border to click into, no placeholder and no stepper. The field stays focusable and selectable and is announced as read-only — unlike state="disabled", which says "not now" and is dimmed. See HabReadonly for the difference, and for making a whole region read-only at once.

vue
<HabInput v-model="street" label="Street" readonly />

Error and disabled keep their own boxes when read-only.

Read-only covers the field, not what you put around the value. A trailing button with a @trailing-click listener and anything in append stay usable — right for "show password" or "copy", wrong for "clear". Anything there that changes the value is yours to hide while the field is read-only. A HabFieldAddon in prependAddon needs nothing: it follows the field.

Focusing it from code ​

focus() moves focus to the control — the <input>, or the <textarea> when multiline — where clicking the label puts it. That is not where Tab lands first when the field has a prependAddon: the addon comes before the value in the tab order, and focus() skips it. A disabled field does not take focus, as a disabled native control does not. It takes the browser's FocusOptions, such as { preventScroll: true }. Type the template ref with InstanceType:

vue
<script setup lang="ts">
import { useTemplateRef } from "vue";
import { HabInput } from "@livereader/habitus-vue";

const field = useTemplateRef<InstanceType<typeof HabInput>>("field");
// later: field.value?.focus();
</script>

Prefer this to querying the rendered markup for an element to focus: which element holds focus is the component's to decide.

Accessibility ​

  • The label is a real <label for> pointing at the control, so clicking it moves focus.
  • Help text is a description, not part of the name. It is wired with aria-describedby, so "Email" stays the control's name and the help — or the error message — is announced separately.
  • state="error" sets aria-invalid.
  • The focus indicator is painted on the box by base.css, because the control itself is borderless. No component resets an outline.
  • Icons are aria-hidden. An interactive trailing icon carries its own aria-label.
  • Ids are generated with Vue's useId(), which is unique per app instance. If your page mounts two Vue apps, set app.config.idPrefix on at least one of them so the generated ids cannot collide.
  • An error message that appears in response to a submit should be announced. This component renders static text; wrap the form region in a live region if the message arrives asynchronously.

API ​

Props ​

PropTypeDefaultDescription
autoGrowbooleanfalseLets a multiline field grow with its text instead of scrolling inside it. rows becomes the minimum, and the resize grip still makes it taller. Uses CSS field-sizing: content; where that is unsupported the field keeps its fixed rows height and scrolls, which is what it does without the flag. Ignored when not multiline.
density"default" | "compact""default"Shrinks the control to --hab-control-sm. Ignored when multiline.
helpstringundefinedHelper text below the box. Announced as the control's description, and rendered in --hab-error-text when state is error.
iconstringundefinedLeading icon as SVG path data (mdiMagnify from @mdi/js). Decorative.
labelstringundefinedVisible label. Associated with the control by for/id.
markerbooleanfalseMarks the value as provisional — drafted by an AI, carried in from an import, inherited from somewhere else. The box takes a faint accent fill and the help line takes accent ink. NOT a validation state, which is why it is a flag beside state rather than a member of it. Nothing is wrong with the value; it has simply not been confirmed by a person yet, and that can be true of a field that is also in error. error and disabled keep their own fill — a state the user has to act on outranks a note about where the value came from. It draws no glyph and writes no words. What the mark MEANS is the host application's to say, in help and in whatever it puts beside the label.
modelValuestringundefinedThe value, for v-model.
monobooleanfalseSets the value in the mono face: for a value that is code — an expression, an environment variable's name, an attribute path — not words. The <input> or <textarea> only; the label and help stay in the sans. One font, never both: a mono class reaching in from outside would compete with the field's own sans by stylesheet order.
multilinebooleanfalseRenders a <textarea> instead — descriptions, notes.
readonlybooleanundefinedThe value is shown to be read, not edited: full ink on the raised surface, no border to click into, no placeholder and no stepper. Still focusable and selectable, and announced as read-only — unlike state="disabled", which says "not now" and is dimmed. Unset, it follows a surrounding HabReadonly; false keeps this field editable inside one. A native readonly attribute counts too.
rowsnumber3Visible rows when multiline. With autoGrow, the height it starts from.
state"default" | "disabled" | "error""default"Validation and availability state. error also sets aria-invalid.
trailingIconstringundefinedTrailing icon as SVG path data — e.g. mdiEyeOff for a password reveal.
trailingIconLabelstringundefinedAccessible name for the trailing icon when it is interactive. Required whenever a @trailing-click listener is attached: the button does something different from the field, so it needs its own name rather than borrowing the field's label.
typestring"text"The input's type. Text-like types only — a checkbox or radio needs HabCheckbox or HabRadio, whose v-model semantics differ.

Events ​

EventPayloadDescription
update:modelValuevalue: string—

Slots ​

SlotBindingsDescription
appendanyExtra affordances at the end of the box, AFTER the trailing button rather than inside it. trailing replaces one glyph and keeps its button; this is for the second and third thing on the row — an info tooltip beside a generate button, say — which trailingIcon cannot express because it is one icon and no node. Each affordance carries its own accessible name; the component names none of them.
appendAddonanyA control fused to the END of the box — usually a HabFieldAddon toggle, such as reading the value as an expression. prependAddon's counterpart: flush, full height and divided from the value, where append sits inside the padding at glyph size. After append. Single-line fields only.
leadinganyLeading adornment, replacing the glyph. Takes precedence over icon.
prependAddonanyA control fused to the start of the box — usually a HabFieldAddon as a menu trigger, the kind of value the field holds. Flush, full height and divided from the value, unlike leading, which is a glyph inside the padding. Single-line fields only: a multiline box ignores it.
trailinganyTrailing adornment, replacing the trailingIcon glyph. Stays inside the button when @trailing-click is attached, so trailingIconLabel and the handler survive the swap.

Exposed ​

MemberTypeDescription
focus(options?: FocusOptions) => voidMoves focus to the control — the <input>, or the <textarea> when multiline — which is where clicking the label puts it, rather than to a prependAddon that Tab would reach first. A disabled field does not take focus, as a disabled native control does not.

Proprietary — internal use only.