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. Itsdetailis the current value.valueSubmit— fires when the field loses focus or the user presses Enter. Itsdetailis{ 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-selectinstead 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 totruefor 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/end— inside the field's border, on the leading and trailing edges. Good for icons, units, and adornments.before/after— outside 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
dropdownslot 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
| Property | Attribute | Description | Type | Default |
|---|---|---|---|---|
autoCapitalize | auto-capitalize | Passed to native input | string | 'off' |
autoComplete | auto-complete | Passed to native input | string | 'off' |
autoCorrect | auto-correct | Passed to native input | "off" | "on" | 'off' |
autoFocus | auto-focus | Passed to native input | boolean | undefined |
debounce | debounce | Optional debounce of the didInput event | number | 0 |
disabled | disabled | Renders input as disabled and prevents changes | boolean | false |
error | error | Shows 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 validation | any | undefined |
hidePasswordIcon | hide-password-icon | Whether to hide the password icon | boolean | false |
hidePicker | hide-picker | Whether to hide the calendar icon | boolean | false |
inputMode | input-mode | Passed to native input | string | undefined |
label | label | Text above the control | string | '' |
list | list | Passed to native input | string | undefined |
max | max | Passed to native input | any | undefined |
maxlength | maxlength | Passed to native input | number | undefined |
min | min | Passed to native input | any | undefined |
minlength | minlength | Passed to native input | number | undefined |
multiline | multiline | Whether the control is a multiline textarea | boolean | false |
note | note | Informational message directly below the control | string | undefined |
offsetX | offset-x | X offset of the dropdown | number | 0 |
offsetY | offset-y | Y offset of the dropdown | number | 10 |
originalValue | original-value | The default value the control will reset to in a form. If not set, will default to the inital value of the "value" property. | any | undefined |
pattern | pattern | Passed to native input | string | undefined |
pending | pending | Shows a loading indicator in the end slot when true | boolean | false |
placeholder | placeholder | Input placeholder text | string | '' |
readonly | readonly | Renders input as read only and prevents changes | boolean | false |
required | required | Marks as required in form and adds asterisk to the end of the label | boolean | false |
size | size | Container size | "large" | "medium" | "small" | 'small' |
spellcheck | spellcheck | Passed to native input | boolean | false |
step | step | Passed to native input | string | undefined |
success | success | Shows a success icon in the end slot when true. Has no effect on form validation | boolean | false |
timeZone | time-zone | Time 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. | string | undefined |
transform | transform | Transforms 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" | any | undefined |
type | type | Passed 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 participation | ValidationFn[] | undefined |
value | value | Current value of the input | any | undefined |
wrap | wrap | Passed to native textarea | string | undefined |
Events
| Event | Description | Type |
|---|---|---|
dropdownDismiss | Emits when the dropdown is closed | CustomEvent<any> |
dropdownPresent | Emits when the dropdown is opened | CustomEvent<any> |
valueChange | Emits as the user types | CustomEvent<any> |
valueSubmit | Emits whenever the user hits enter or the control loses focus | CustomEvent<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
| Slot | Description |
|---|---|
"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
| Part | Description |
|---|---|
"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
| Name | Description |
|---|---|
--background | The background of the input |
--border | The border of the input |
--height | The height of the input |
--input-color | The color of the input |
--label-color | The color of the label |
--multiline-height | The height of the multiline input |
--padding-inline | The padding of the input |
--placeholder-color | The color of the placeholder |
Dependencies
Used by
Depends on
Graph
© 2025 United Systems & Software - All Rights Reserved.