Skip to content
substrate-systemPublic

Repository files navigation

Input

tests types module install size semantic versioning Common Changelog license

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.

See a live demo

Contents

Install

npm i -S @substrate-system/input

CSS

Import CSS

import '@substrate-system/input/css'

Or minified:

import '@substrate-system/input/min/css'

Theme

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;
}

Use

This calls the global function customElements.define. Just import, then use the tag in your HTML.

JS

import { SubstrateInput } from '@substrate-system/input'

// TAG property

document.body.innerHTML += `
<${SubstrateInput.TAG}></${SubstrateInput.TAG}>
`

HTML

<div>
    <substrate-input></substrate-input>
</div>

Render the markup yourself

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.

pre-built

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.

copy

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

HTML

<head>
    <link rel="stylesheet" href="./substrate-input.css">
</head>
<body>
    <!-- ... -->
    <script type="module" src="./substrate-input.min.js"></script>
</body>

API

Attributes

Component attributes

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.

input element attributes

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

Moved attributes

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.

ARIA attributes

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

Properties

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.

controlAttributes

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.

Events

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)
})

Releases

Contributors

Languages