Skip to main content

BwInput | bw-input

Overview

Input is a form control that allows the user to enter text. It can be used as a single line input or a multiline input. It has a dropdown slot to transform the component into a combobox.

Usage

Basics

Every input pairs a label with a control. Add a placeholder for in-field guidance and a note for persistent helper text below the field. Set an initial value with the value attribute.

<div class="flex-column gap">
<bw-input label="Full name" placeholder="Jane Doe"></bw-input>
<bw-input label="Email" value="jane@example.com"></bw-input>
<bw-input label="Username" placeholder="jdoe" note="This is visible to other users."></bw-input>
</div>

Sizes

Use the size attribute to match the density of the surrounding UI. The default is small.

<div class="flex-column gap">
<bw-input size="small" label="Small" placeholder="Small"></bw-input>
<bw-input size="medium" label="Medium" placeholder="Medium"></bw-input>
<bw-input size="large" label="Large" placeholder="Large"></bw-input>
</div>

Disabled and read-only

disabled prevents all interaction and removes the field from form submission. readonly keeps the value visible and submittable but blocks editing.

<div class="flex-column gap">
<bw-input label="Disabled" value="Can't edit or submit" disabled></bw-input>
<bw-input label="Read only" value="Submitted, but not editable" readonly></bw-input>
</div>

Reading the value

bw-input emits two events:

  • valueChange — fires on every keystroke. Its detail is the current value.
  • valueSubmit — fires when the field loses focus or the user presses Enter. Its detail is { value, event }.

Both events are scoped to the element (they do not bubble), so attach listeners directly to the bw-input.

<div class="flex-column gap">
<bw-input id="basicsValue" label="Type something"></bw-input>
<bw-note id="basicsValueOut">valueChange: (empty)</bw-note>
</div>
<script>
const input = document.querySelector('#basicsValue');
const out = document.querySelector('#basicsValueOut');
input.addEventListener('valueChange', e => {
out.textContent = 'valueChange: ' + (e.detail || '(empty)');
});
</script>

Debouncing

Pass a debounce (in milliseconds) to throttle valueChange — useful when each change triggers an expensive operation such as a network request. The example below only reports a change once you stop typing for 500ms.

<div class="flex-column gap">
<bw-input id="basicsDebounce" label="Debounced (500ms)" debounce="500"></bw-input>
<bw-note id="basicsDebounceOut">Waiting…</bw-note>
</div>
<script>
const input = document.querySelector('#basicsDebounce');
const out = document.querySelector('#basicsDebounceOut');
input.addEventListener('valueChange', e => {
out.textContent = 'Settled on: ' + (e.detail || '(empty)');
});
</script>

Combobox

Place a bw-menu in the dropdown slot to turn the input into a combobox. The menu opens automatically when the field is focused, and choosing an option fills the input for you — no extra wiring needed. Keyboard users can press to move from the input into the list.

<bw-input label="Favorite fruit" placeholder="Choose one">
<bw-menu slot="dropdown" type="select">
<bw-option>Apple</bw-option>
<bw-option>Banana</bw-option>
<bw-option>Cherry</bw-option>
<bw-option>Date</bw-option>
<bw-option>Elderberry</bw-option>
</bw-menu>
</bw-input>

Autocomplete

Drive the options dynamically to build a search/autocomplete field. Set the menu's options to an array of { value, text }, and update it as the user types. Combine with debounce to limit how often you filter or fetch.

<bw-input id="comboSearch" label="Search fruit" type="search" debounce="200" placeholder="Type to filter…">
<bw-icon slot="start">search</bw-icon>
<bw-menu slot="dropdown" type="select"></bw-menu>
</bw-input>
<script>
const fruits = ['Apple', 'Apricot', 'Banana', 'Blueberry', 'Cherry', 'Date', 'Fig', 'Grape', 'Mango'];
const input = document.querySelector('#comboSearch');
const menu = input.querySelector('bw-menu');
const toOptions = list => list.map(f => ({ value: f.toLowerCase(), text: f }));

menu.options = toOptions(fruits);
input.addEventListener('valueChange', e => {
const q = (e.detail || '').toLowerCase();
menu.options = toOptions(fruits.filter(f => f.toLowerCase().includes(q)));
});
</script>

For a fully featured select with multi-select, search, and lazy loading built in, consider bw-select instead of assembling your own combobox.

Forms

bw-input is a form-associated control: give it a name and place it inside a bw-form (or a native <form>) and its value is included on submit. Use bw-button with type="submit" to submit and type="reset" to restore every control to its original value.

bw-form will not submit while any control is invalid, and it focuses the first invalid field.

<bw-form id="signupForm" class="flex-column gap">
<bw-input name="name" label="Name" required></bw-input>
<bw-input name="email" label="Email" type="email" required></bw-input>
<div class="horizontal-container">
<bw-button type="submit">Submit</bw-button>
<bw-button type="reset" variant="secondary">Reset</bw-button>
</div>
<bw-note id="signupOut">Submit the form to see its data.</bw-note>
</bw-form>
<script>
const form = document.querySelector('#signupForm');
const out = document.querySelector('#signupOut');
// dataSubmit emits the typed form data
form.addEventListener('dataSubmit', e => {
out.textContent = 'dataSubmit: ' + JSON.stringify(e.detail);
});
</script>

Edit forms and dataPatch

To edit an existing record, set each control's value to the current data. On submit, bw-form emits dataPatch with only the fields the user actually changed — ideal for sending a minimal update to your API. Change one field below and submit to see it.

<bw-form id="editForm" class="flex-column gap">
<bw-input name="firstName" label="First name" value="Jane"></bw-input>
<bw-input name="lastName" label="Last name" value="Doe"></bw-input>
<bw-button type="submit">Save changes</bw-button>
<bw-note id="editOut">Change a field, then save to see only what changed.</bw-note>
</bw-form>
<script>
const form = document.querySelector('#editForm');
const out = document.querySelector('#editOut');
form.addEventListener('dataPatch', e => {
out.textContent = 'dataPatch: ' + JSON.stringify(e.detail);
});
</script>

A control's reset/patch baseline is its originalValue, which defaults to the value present at first render. If you load data asynchronously after the inputs have rendered, set originalValue explicitly (or delay rendering the form until the data is ready) so dataPatch and reset behave correctly.

Styling validity with CSS states

Form controls expose CSS custom states you can target for styling: --required, --optional, --valid, --invalid, and — only after the user has interacted — --user-valid and --user-invalid.

<div>
<style>
@scope {
bw-input:state(--user-valid) {
--border: solid 1px var(--bw-green-500);
}
}
</style>
<bw-input label="Turns green once valid" required minlength="3" note="Type 3+ characters"></bw-input>
</div>

For multi-step forms, drawers, discard prompts, and the full event list (dataSubmit, dataPatch, submit), see the Forms tutorial.

Icons

bw-input can show status indicators inside the field. These are purely visual — they communicate state to the user but have no effect on form validation.

  • pending — shows a loading spinner (e.g. while checking availability).
  • success — shows a green check.
  • error — shows a red error icon. Set it to true for the icon alone, or to a string to show that message in a tooltip on hover.
<div class="flex-column gap">
<bw-input label="Checking…" value="jdoe" pending></bw-input>
<bw-input label="Available" value="newuser" success></bw-input>
<bw-input label="Taken" value="jdoe" error="true"></bw-input>
<bw-input label="Taken (with tooltip)" value="jdoe" error="That username is taken"></bw-input>
</div>

Putting it together

A typical availability check moves through pending → success/error as a request resolves.

<bw-input id="iconCheck" label="Username" note="Type a name — 'admin' and 'jdoe' are taken" debounce="400"></bw-input>
<script>
const input = document.querySelector('#iconCheck');
const taken = ['admin', 'root', 'jdoe'];
input.addEventListener('valueChange', e => {
const value = e.detail;
input.pending = false;
input.success = false;
input.error = false;
if (!value) return;

input.pending = true;
setTimeout(() => {
input.pending = false;
if (taken.includes(value)) {
input.error = `"${value}" is already taken`;
} else {
input.success = true;
}
}, 600);
});
</script>

Use these icons for transient or server-driven feedback. For validation errors tied to form constraints, rely on validation instead — the field renders its own error message automatically.

Masks

Masks format the value as the user types. Set type to one of the built-in masks — phone, currency, zip, ssn, or datetime. The displayed text is formatted, while the input's value stays clean (digits for phone/zip/ssn/datetime, a decimal string for currency).

NOTE: If you are using the currency mask, you will most likely want to also add the attribute transform="number". This will ensure that the value property of the input is a number instead of a string.

<div class="flex-column gap">
<bw-input type="phone" label="Phone" placeholder="(555) 123-4567"></bw-input>
<bw-input type="currency" transform="number" label="Amount"></bw-input>
<bw-input type="zip" label="Zip code"></bw-input>
<bw-input type="ssn" label="SSN"></bw-input>
</div>

For example, typing 5551234567 into the phone field shows (555) 123-4567 but value is 5551234567. Typing 1234 into the currency field shows $12.34 and value is 12.34.

Date & time

The datetime mask formats 12 digits as MM/DD/YYYY HH:mm (24-hour). It pairs well with bw-datepicker to make an editable date field.

<bw-input type="datetime" label="Starts" placeholder="MM/DD/YYYY HH:mm"></bw-input>
<div class="flex-column gap">
<bw-input id="maskValue" type="currency" label="Amount (watch the value)"></bw-input>
<bw-note id="maskValueOut">value: (empty)</bw-note>
</div>
<script>
const input = document.querySelector('#maskValue');
const out = document.querySelector('#maskValueOut');
input.addEventListener('valueChange', e => {
out.textContent = 'value: ' + (e.detail || '(empty)');
});
</script>

Masked (obscured) variants

The phone-mask, ssn-mask, and email-mask types hide earlier characters for sensitive data while keeping the real value available in code.

<div class="flex-column gap">
<bw-input type="phone-mask" label="Phone (masked)"></bw-input>
<bw-input type="ssn-mask" label="SSN (masked)"></bw-input>
<bw-input type="email-mask" label="Email (masked)" placeholder="name@example.com"></bw-input>
</div>

Custom masks

For formats the built-ins don't cover, assign an object to type (it must be set in JavaScript, since it's not a string). Provide:

  • formatter — turns the raw value into the displayed string.
  • extractor (optional) — turns typed text back into the raw value.
  • masker (optional) — obscures the displayed string, like the masked variants above.
<bw-input id="customMask" label="License key" placeholder="XXXX-XXXX-XXXX"></bw-input>
<script>
const input = document.querySelector('#customMask');
input.type = {
// keep up to 12 letters/digits, uppercased
extractor: v => v.replace(/[^a-z0-9]/gi, '').toUpperCase().slice(0, 12),
// group into blocks of four: ABCD-EFGH-IJKL
formatter: raw => (raw.match(/.{1,4}/g) || []).join('-'),
};
</script>

Multiline

Add the multiline attribute to render a textarea instead of a single-line input. It accepts the same label, note, value, sizing, and validation options as a regular input.

<bw-input
multiline
label="Comments"
placeholder="Tell us what you think…"
note="Press Shift + Enter for a new line."
></bw-input>

Inside a form, pressing Enter submits as usual, while Shift + Enter inserts a newline without submitting.

<bw-input multiline label="Bio" value="Designer and builder.
Currently exploring design systems." maxlength="280" note="Up to 280 characters"></bw-input>

Slots

Slots let you place content around the input. There are two groups:

  • start / endinside the field's border, on the leading and trailing edges. Good for icons, units, and adornments.
  • before / afteroutside the field, in the same row. Good for attached prefixes, suffixes, or buttons.
<div class="flex-column gap">
<bw-input label="Search" placeholder="Search…">
<bw-icon slot="start">search</bw-icon>
</bw-input>

<bw-input label="Price" type="number" placeholder="0.00">
<span slot="start">$</span>
<span slot="end">USD</span>
</bw-input>

<bw-input label="Subdomain" placeholder="my-team">
<span slot="before">https://</span>
<span slot="after">.example.com</span>
</bw-input>
</div>

Custom label and note

By default label and note render simple text. Use the label and note slots when you need richer markup — formatting, icons, or links.

<bw-input placeholder="Your handle">
<bw-label slot="label">
Username <bw-icon size="sm">alternate_email</bw-icon>
</bw-label>
<bw-note slot="note">
See our <a href="#">naming guidelines</a> for details.
</bw-note>
</bw-input>

The dropdown slot is reserved for turning the input into a combobox — see Combobox.

Transforms

A native input always reads and writes strings. The transform property lets the input's value be a richer type — a number, an ISO date string, or anything you define — while the field still displays text. This keeps your form data correctly typed without manual parsing.

Number

transform="number" makes value a number instead of a string. Pair it with type="number".

<div class="flex-column gap">
<bw-input id="tNumber" type="number" transform="number" label="Price" value="9.99"></bw-input>
<bw-note id="tNumberOut">value: 9.99 (number)</bw-note>
</div>
<script>
const input = document.querySelector('#tNumber');
const out = document.querySelector('#tNumberOut');
input.addEventListener('valueChange', e => {
out.textContent = 'value: ' + e.detail + ' (' + typeof e.detail + ')';
});
</script>

Date and datetime

transform="date" and transform="datetime" convert between the native date control and an ISO 8601 string. The displayed control uses the local date/time; value is the ISO string — convenient for sending to an API.

<div class="flex-column gap">
<bw-input id="tDate" type="date" transform="date" label="Start date"></bw-input>
<bw-note id="tDateOut">Pick a date to see its ISO value.</bw-note>
</div>
<script>
const input = document.querySelector('#tDate');
const out = document.querySelector('#tDateOut');
input.addEventListener('valueChange', e => {
out.textContent = 'ISO value: ' + (e.detail || '(empty)');
});
</script>

By default these use the browser's time zone. Override it per field with the time-zone attribute, or globally via window.blueWater.defaultTimeZone.

<bw-input type="datetime-local" transform="datetime" time-zone="America/New_York" label="Event start (Eastern)"></bw-input>

Custom transforms

Provide your own { to, from } pair. to converts the displayed string into your value type; from converts a value back into a string for display. This example treats a comma-separated field as an array.

<div class="flex-column gap">
<bw-input id="tCustom" label="Tags" value="alpha, beta" note="Comma separated"></bw-input>
<bw-note id="tCustomOut">value: ["alpha","beta"]</bw-note>
</div>
<script>
const input = document.querySelector('#tCustom');
const out = document.querySelector('#tCustomOut');
input.transform = {
to: str => str.split(',').map(s => s.trim()).filter(Boolean), // string -> array
from: arr => (Array.isArray(arr) ? arr.join(', ') : arr), // array -> string
};
input.addEventListener('valueChange', e => {
out.textContent = 'value: ' + JSON.stringify(e.detail);
});
</script>

Types

The type attribute accepts any native input type — text (default), email, number, password, search, url, tel, date, datetime-local, month, time, and week. The native type controls the on-screen keyboard, built-in validation, and any browser picker.

<div class="flex-column gap">
<bw-input type="text" label="Text"></bw-input>
<bw-input type="email" label="Email" placeholder="name@example.com"></bw-input>
<bw-input type="number" label="Number" placeholder="42"></bw-input>
<bw-input type="url" label="Website" placeholder="https://"></bw-input>
<bw-input type="date" label="Date"></bw-input>
<bw-input type="search" label="Search" placeholder="Search…">
<bw-icon slot="start">search</bw-icon>
</bw-input>
</div>

Looking for phone, currency, SSN, or zip formatting? Those are handled by masks, not native types.

Password

type="password" masks the value and adds a button to toggle visibility. Hide the toggle with hide-password-icon, or call the togglePassword() method to control it yourself.

<div class="flex-column gap">
<bw-input type="password" label="Password" value="hunter2"></bw-input>
<bw-input type="password" label="Password (no toggle)" value="hunter2" hide-password-icon></bw-input>
</div>

Hiding the picker

Some native types (date, time, number, etc.) render a browser control such as a calendar or stepper. Add hide-picker to suppress it while keeping the type's keyboard and validation.

<div class="flex-column gap">
<bw-input type="date" label="Date (default picker)"></bw-input>
<bw-input type="date" label="Date (picker hidden)" hide-picker></bw-input>
</div>

Validation

bw-input participates in native form validation. Error messages appear once the user has touched the field (focused and then left it, or attempted to submit the form) so the user isn't scolded before they start typing.

Built-in attributes

The standard constraint attributes work just like they do on a native input: required, minlength, maxlength, pattern, and min/max (for type="number"). bw-input generates an appropriate message for each.

<div class="flex-column gap">
<bw-input label="Required" required></bw-input>
<bw-input label="Username" minlength="3" maxlength="12" note="3–12 characters"></bw-input>
<bw-input label="Zip code" pattern="[0-9]{5}" inputmode="numeric" note="Exactly 5 digits"></bw-input>
<bw-input type="number" label="Quantity" min="1" max="10" note="Between 1 and 10"></bw-input>
</div>

A required field adds an asterisk to its label automatically.

Validator functions

For logic that the built-in attributes can't express — async checks, cross-field rules, custom messages — assign an array of functions to the validators property. Each validator receives the current value and returns an array of error messages (or a promise of one). An empty array means valid.

<bw-input id="vUsername" label="Username" note="Try typing 'taken'" debounce="300"></bw-input>
<script>
const input = document.querySelector('#vUsername');
input.validators = [
async (value) => {
// simulate an API lookup
await new Promise(r => setTimeout(r, 500));
return value === 'taken' ? ['That username is already in use'] : [];
},
];
</script>

You can supply multiple validators; all of their messages are collected.

<bw-input id="vPassword" type="password" label="New password" note="8+ characters, at least one number"></bw-input>
<script>
const input = document.querySelector('#vPassword');
input.validators = [
(v) => (v.length >= 8 ? [] : ['Must be at least 8 characters']),
(v) => (/\d/.test(v) ? [] : ['Must contain a number']),
];
</script>

Inspecting validity in code

Call the getErrors() method to read the current validity state without waiting for the UI. It resolves to an object describing each kind of error plus a hasError summary.

<div class="flex-column gap">
<bw-input id="vInspect" label="Required" required minlength="4"></bw-input>
<bw-note id="vInspectOut">errors: …</bw-note>
</div>
<script>
const input = document.querySelector('#vInspect');
const out = document.querySelector('#vInspectOut');
const report = async () => {
const errors = await input.getErrors();
out.textContent = 'errors: ' + JSON.stringify(errors);
};
input.addEventListener('valueChange', report);
report();
</script>

See the Forms tutorial for how validation integrates with bw-form, including how an invalid form blocks submission and focuses the first invalid control.

Properties

PropertyAttributeDescriptionTypeDefault
autoCapitalizeauto-capitalizePassed to native inputstring'off'
autoCompleteauto-completePassed to native inputstring'off'
autoCorrectauto-correctPassed to native input"off" | "on"'off'
autoFocusauto-focusPassed to native inputbooleanundefined
debouncedebounceOptional debounce of the didInput eventnumber0
disableddisabledRenders input as disabled and prevents changesbooleanfalse
errorerrorShows an error icon in the end slot when true. If a string is passed in, it will render the icon as a tooltip. Has no effect on form validationanyundefined
hidePasswordIconhide-password-iconWhether to hide the password iconbooleanfalse
hidePickerhide-pickerWhether to hide the calendar iconbooleanfalse
inputModeinput-modePassed to native inputstringundefined
labellabelText above the controlstring''
listlistPassed to native inputstringundefined
maxmaxPassed to native inputanyundefined
maxlengthmaxlengthPassed to native inputnumberundefined
minminPassed to native inputanyundefined
minlengthminlengthPassed to native inputnumberundefined
multilinemultilineWhether the control is a multiline textareabooleanfalse
notenoteInformational message directly below the controlstringundefined
offsetXoffset-xX offset of the dropdownnumber0
offsetYoffset-yY offset of the dropdownnumber10
originalValueoriginal-valueThe default value the control will reset to in a form. If not set, will default to the inital value of the "value" property.anyundefined
patternpatternPassed to native inputstringundefined
pendingpendingShows a loading indicator in the end slot when truebooleanfalse
placeholderplaceholderInput placeholder textstring''
readonlyreadonlyRenders input as read only and prevents changesbooleanfalse
requiredrequiredMarks as required in form and adds asterisk to the end of the labelbooleanfalse
sizesizeContainer size"large" | "medium" | "small"'small'
spellcheckspellcheckPassed to native inputbooleanfalse
stepstepPassed to native inputstringundefined
successsuccessShows a success icon in the end slot when true. Has no effect on form validationbooleanfalse
timeZonetime-zoneTime zone for the built in date and datetime transformers. Defaults to the browser's time zone if not set. You can set this value globally by setting window.blueWater?.defaultTimeZone to the desired time zone.stringundefined
transformtransformTransforms the value before it is passed to the input (from) and after the input emits a new value (to). There are built-in transformers for 'number', 'date', and 'datetime'."date" | "datetime" | "number" | anyundefined
typetypePassed to native input"currency" | "date" | "datetime" | "datetime-local" | "email" | "email-mask" | "month" | "number" | "password" | "phone" | "phone-mask" | "search" | "ssn" | "ssn-mask" | "text" | "time" | "url" | "week" | "zip" | ({ formatter: (str: string) => string; extractor?: (str: string) => string; masker?: (rawValue: string, formattedValue: string) => string; })'text'
validators--Validator functions for form participationValidationFn[]undefined
valuevalueCurrent value of the inputanyundefined
wrapwrapPassed to native textareastringundefined

Events

EventDescriptionType
dropdownDismissEmits when the dropdown is closedCustomEvent<any>
dropdownPresentEmits when the dropdown is openedCustomEvent<any>
valueChangeEmits as the user typesCustomEvent<any>
valueSubmitEmits whenever the user hits enter or the control loses focusCustomEvent<any>

Methods

dismissDropdown() => Promise<void>

Dismisses the dropdown

Returns

Type: Promise<void>

getErrors() => Promise<{ requiredError: boolean; minLengthError: boolean; maxLengthError: boolean; patternError: boolean; customErrors: string[]; badInputError: boolean; hasError: boolean; }>

Returns the errors of the input

Returns

Type: Promise<{ requiredError: boolean; minLengthError: boolean; maxLengthError: boolean; patternError: boolean; customErrors: string[]; badInputError: boolean; hasError: boolean; }>

getInputElement() => Promise<HTMLInputElement | HTMLTextAreaElement>

Returns the input element

Returns

Type: Promise<HTMLInputElement | HTMLTextAreaElement>

getIsTouched() => Promise<boolean>

Returns whether the input is touched

Returns

Type: Promise<boolean>

isDropdownOpen() => Promise<boolean>

Returns whether the dropdown is open

Returns

Type: Promise<boolean>

markAsTouched() => Promise<void>

Marks the input as touched

Returns

Type: Promise<void>

presentDropdown() => Promise<void>

Presents the dropdown

Returns

Type: Promise<void>

togglePassword() => Promise<void>

Toggles the password visibility

Returns

Type: Promise<void>

Slots

SlotDescription
"after"Elements placed here will be to the right of the input, outside the container
"before"Elements placed here will be to the left of the input, outside the container
"dropdown"Place a BwMenu here to use the input as a combobox
"end"Elements placed here will be to the left of the input, inside the container
"label"Goes above the input, intended for a BwLabel
"note"Goes below the input, intended for a BwNote
"start"Elements placed here will be to the right of the input, inside the container

Shadow Parts

PartDescription
"container"The container of the input
"dropdown"The dropdown container
"label"The label of the input
"native-input"The native input element
"native-textarea"The native textarea element
"note"The note of the input
"outer-container"The container where the before/after slots are.

CSS Custom Properties

NameDescription
--backgroundThe background of the input
--borderThe border of the input
--heightThe height of the input
--input-colorThe color of the input
--label-colorThe color of the label
--multiline-heightThe height of the multiline input
--padding-inlineThe padding of the input
--placeholder-colorThe color of the placeholder

Dependencies

Used by

Depends on

Graph


© 2025 United Systems & Software - All Rights Reserved.