Inputs with style. Use this web component, and it will forward any relevant
attributes to the inner input element, like an id or name, for example.
aria-* attributes are handled intelligently too.
npm i -S @substrate-system/inputimport '@substrate-system/input/css'Or minified:
import '@substrate-system/input/min/css'Set these CSS variables to theme the input. Each one has a fallback, so the component works without any of them.
| Variable | Used for | Default |
|---|---|---|
--substrate-text |
Input text color | FieldText |
--substrate-surface |
Input background | Field |
--substrate-border |
Input border color | currentcolor |
--substrate-radius |
Input border radius | 0 |
--substrate-focus |
Focus ring color | currentcolor |
--substrate-focus-width |
Focus ring width | 2px |
--substrate-focus-offset |
Focus ring offset | 2px |
:root {
--substrate-focus: var(--color-focus);
--substrate-focus-width: 0.1875rem;
--substrate-focus-offset: 0.125rem;
}This calls the global function customElements.define. Just import, then use
the tag in your HTML.
import { SubstrateInput } from '@substrate-system/input'
// TAG property
document.body.innerHTML += `
<${SubstrateInput.TAG}></${SubstrateInput.TAG}>
`<div>
<substrate-input></substrate-input>
</div>If the element already has an inner <input> when it connects, it
keeps the existing markup instead of replacing it. Then a framework
like Preact can render every node itself, and the element only adds
behavior. It leaves the label alone, and every attribute the app already
set on the input. Host attributes only fill in what the input is
missing.
Put id, tabindex, autofocus, and every aria-* straight on the
inner input. Then the framework updates and removes them on a node it
owns. Keep the value and state attributes (name, value, disabled,
required, form) on the host.
Use these classes so the CSS applies:
<substrate-input name="email" required>
<div class="substrate input email">
<label class="label-content" for="email">Email</label>
<input id="email" type="email" aria-describedby="email-hint">
</div>
</substrate-input>The element does not render while it is detached from the document. A framework can set attributes and append children first, and the element renders, or adopts the markup, when it connects.
This package exposes minified JS and CSS files too. Copy them to a location that is accessible to your web server, then link to them in HTML.
cp ./node_modules/@substrate-system/input/dist/index.min.js ./public/substrate-input.min.js
cp ./node_modules/@substrate-system/input/dist/style.min.css ./public/substrate-input.css<head>
<link rel="stylesheet" href="./substrate-input.css">
</head>
<body>
<!-- ... -->
<script type="module" src="./substrate-input.min.js"></script>
</body>These are handled by substrate-input directly.
| Attribute | Description |
|---|---|
label |
Text content for the <label> element rendered above the input. When omitted, no label is rendered. |
id |
Moved to the inner <input> element (see Moved attributes). When omitted, a random id is generated for the input. Use the inputId property to change or clear it later. |
These are copied to the inner <input> element and kept on the host.
Removing one from the host removes it from the input too. The exceptions
are autofocus and tabindex, which are moved instead.
accept, alt, autocomplete, autocapitalize, autocorrect,
autofocus, capture, dirname, disabled, enterkeyhint, form,
inputmode, list, max, maxlength, min, minlength, multiple,
name, pattern, placeholder, readonly, required, size,
spellcheck, step, tabindex, title, type, value
id, tabindex, autofocus, and every aria-* attribute describe
the focusable control, so they are moved: copied to the inner <input>
and removed from the host. Left on the host they would cause problems.
An aria-label on an element with no role fails axe's
aria-prohibited-attr, aria-hidden hides the whole control, and a
tabindex adds a second tab stop.
Because the host no longer has the attribute, removing it from the host
later does nothing. So don't set these on the host from a framework.
Either render the inner input yourself
and put them there, pass them in the
controlAttributes property, or change them
through the control property:
const el = document.querySelector('substrate-input')
el.control.setAttribute('aria-invalid', 'true')
el.control.removeAttribute('aria-invalid')Setting a moved attribute on the host again does work. It moves the new
value to the input. If controlAttributes had set
that name, the host value takes it over once the element has rendered.
Before render, controlAttributes is applied last and still wins.
These are observed, so setting one on the host after connect moves it
to the inner input. Any other aria-* attribute on the host moves when
the element renders.
aria-activedescendant, aria-atomic, aria-autocomplete,
aria-braillelabel, aria-brailleroledescription, aria-busy,
aria-checked, aria-colcount, aria-colindex, aria-colindextext,
aria-colspan, aria-controls, aria-current, aria-describedby,
aria-description, aria-details, aria-disabled, aria-dropeffect,
aria-errormessage, aria-expanded, aria-flowto, aria-grabbed,
aria-haspopup, aria-hidden, aria-invalid, aria-keyshortcuts,
aria-label, aria-labelledby, aria-level, aria-live, aria-modal,
aria-multiline, aria-multiselectable, aria-orientation, aria-owns,
aria-placeholder, aria-posinset, aria-pressed, aria-readonly,
aria-relevant, aria-required, aria-roledescription, aria-rowcount,
aria-rowindex, aria-rowindextext, aria-rowspan, aria-selected,
aria-setsize, aria-sort, aria-valuemax, aria-valuemin,
aria-valuenow, aria-valuetext
| Property | Description |
|---|---|
control |
The inner <input> element, or null before render. Read only. |
value |
Reads and writes the inner input's live value. |
disabled |
Boolean. Sets or removes the disabled attribute. |
inputId |
The inner input's id. Set it to null or '' to clear it; the input then gets a generated id. |
controlAttributes |
The inner input's aria-*, tabindex, and autofocus attributes, as an object. See below. |
label |
Sets or removes the label attribute. |
value, disabled, inputId, controlAttributes, and label can be
set before the element is defined. They apply when it upgrades.
A framework that renders before the definition loads, such as Preact,
writes an inputid attribute instead. Its value becomes the input id,
unless the host also has an id or inputId was set as a property. It
also writes a controlattributes attribute, but that value is
[object Object], so the controlAttributes value cannot be recovered.
The element removes the attribute on upgrade, and the host ends with
controlAttributes set to {}. Preact sets the property again only
when the object changes identity. An inline object is missing until the
next render. A memoized or module-level object is missing until it
changes. To avoid this, define the element before the first render.
Use this property to set and clear the inner input's aria-*,
tabindex, and autofocus from a framework. Pass every one the input
should have, on every render. A name you passed before and leave out
now is removed from the input, so clearing an error clears its
aria-invalid and aria-describedby too.
import { html } from 'htm/preact'
html`<substrate-input
name="email"
label="Email"
controlAttributes=${{
'aria-invalid': bad ? 'true' : undefined,
'aria-describedby': bad ? 'email-error' : undefined
}}
></substrate-input>`Values follow Preact's rules for attributes. null and undefined
remove the attribute. For aria-*, true and false become "true"
and "false". For tabindex and autofocus, true is an empty
attribute and false removes it. Any other value becomes a string.
Setting the property to null or '' clears every attribute it set.
Other names, including id, are ignored. Use inputId for the id.
Names are lowercased, so tabIndex and autoFocus work too. If two
keys lowercase to the same name, the later one wins.
controlAttributes only removes attributes it set itself, so an
attribute already on an input you
rendered yourself stays.
A host aria-*, tabindex, or autofocus attribute and
controlAttributes write to the same attribute on the input. A host
attribute set after the element has rendered takes the name over, and
after that controlAttributes stops removing it. Listing the name again
with a value takes it back. A null or undefined value, or false
for tabindex or autofocus, does not remove a host-owned attribute.
For aria-*, false is the value "false", so it takes the name
back. A host attribute set before the
element renders is forwarded first, and then controlAttributes is
applied, so controlAttributes wins for a name it lists. Setting
autofocus after the element has rendered does not move focus.
Preact sets an inline object prop on every render. If controlAttributes
still lists the name, the next render overwrites the host value, so stop
listing a name once the host owns it.
substrate-input does not emit any custom events. All native <input>
events (change, input, focus, blur, etc.) bubble up through the
component as normal.
const el = document.querySelector('substrate-input')
el.addEventListener('input', ev => {
console.log(ev.target.value)
})