Skip to content
ShineUI
Install

InputMask

Patterns typed clean.

<sui-input-mask>Text inputsStable

About

Text field that types into a pattern: phones, EINs, ZIPs. 9 takes a digit, a a letter, * either, ? makes the rest optional, and every other character is a literal the field inserts for you. Keys that do not fit are dropped without a flash. Form-associated: it posts name=value and binds to React, Vue (v-model) and Angular forms.

Usage

<sui-input-mask label="Phone" name="phone" mask="(999) 999-9999" value="8122820135"></sui-input-mask> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-input-mask"); el.addEventListener("suiChange", (e) => console.log(e.detail)); </script>
import { SuiInputMask } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { return <SuiInputMask label="Phone" name="phone" mask="(999) 999-9999" value="8122820135" onSuiChange={(e) => console.log(e.detail)} />; }
<script setup> import { SuiInputMask } from "@shineui/vue"; import "@shineui/core/shineui.css"; const onChange = (e) => console.log(e.detail); </script> <template> <SuiInputMask label="Phone" name="phone" mask="(999) 999-9999" value="8122820135" @sui-change="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiInputMask } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiInputMask], template: `<sui-input-mask label="Phone" name="phone" mask="(999) 999-9999" value="8122820135" (suiChange)="onChange($event)"></sui-input-mask>`, }) export class Example { onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

The placeholder is the mask with underscores in the slots. A "Format" line is described with the help text, so a screen reader hears the shape once the placeholder is gone.

<div style="max-width:340px"> <sui-input-mask label="Phone" name="phone" mask="(999) 999-9999" value="8122820135"></sui-input-mask> </div>

Patterns

9 is a digit slot, a is a letter slot and * takes either. Set placeholder to show something other than underscores.

<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px"> <sui-input-mask label="EIN" mask="99-9999999" help="Nine digits, no dashes needed."></sui-input-mask> <sui-input-mask label="Serial" mask="aa-9999" placeholder="XX-0000"></sui-input-mask> <sui-input-mask label="Batch" mask="****-****" value="A7K2Q9ZT"></sui-input-mask> <sui-input-mask label="Posting date" mask="99/99/9999" placeholder="MM/DD/YYYY"></sui-input-mask> </div>

Optional slots

Everything after ? is optional. suiComplete fires once the required slots fill, at five digits here, and the format hint read with the help text names the required part.

<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px"> <sui-input-mask label="ZIP" mask="99999?-9999" help="Five digits, or nine with the extension."></sui-input-mask> <sui-input-mask label="Part number" mask="aaa-9999?-aa" value="FCS1043" help="The revision after the second dash is optional."></sui-input-mask> </div>

Sizes

md is the default, one height with every select, picker and button, so a form built from defaults lines up. lg is the roomier field for a sign-in or marketing page; sm packs into table rows.

<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;align-items:end"> <sui-input-mask label="Large" size="lg" mask="(999) 999-9999"></sui-input-mask> <sui-input-mask label="Medium" size="md" mask="(999) 999-9999"></sui-input-mask> <sui-input-mask label="Small" size="sm" mask="(999) 999-9999"></sui-input-mask> </div>

States

The error line replaces help and carries role="alert". A read-only field still posts its value; a disabled one does not.

<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px"> <sui-input-mask label="Phone" mask="(999) 999-9999" required error="Enter a full ten-digit number."></sui-input-mask> <sui-input-mask label="Phone on file" mask="(999) 999-9999" value="6145551212" readonly help="Change it from the contact card."></sui-input-mask> <sui-input-mask label="Locked" mask="(999) 999-9999" value="6145551212" disabled></sui-input-mask> </div>

Half-typed values

While the required slots are not all filled the field reports patternMismatch with the format, so a form refuses a half-typed number the way it refuses an empty required phone. auto-clear drops the partial text when the field loses focus; without it the text stays and the message does.

Nothing checked yet.
<div style="display:grid;gap:14px;max-width:320px"> <sui-input-mask id="im-partial" label="Phone" name="phone" required help="Type three digits and press Tab."></sui-input-mask> <sui-input-mask id="im-clear" label="Phone" name="phone2" auto-clear help="The same, cleared on blur."></sui-input-mask> <output id="im-partial-out" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Nothing checked yet.</output> </div>
// setup(root): properties and events setup(root) { const partial = root.querySelector('#im-partial'); const out = root.querySelector('#im-partial-out'); partial.addEventListener('suiBlur', () => { out.textContent = partial.checkValidity() ? 'Valid.' : partial.validationMessage; }); }

In a form

The field posts what it shows, unless unmask asks for the bare characters. Reset restores the value each field mounted with.

Submit to see what posts.
<form id="mask-form" style="display:grid;gap:14px;max-width:340px"> <sui-input-mask label="Phone" name="phone" mask="(999) 999-9999" value="6145551212"></sui-input-mask> <sui-input-mask label="Phone, unmasked" name="phone_raw" mask="(999) 999-9999" value="6145551212" unmask></sui-input-mask> <div style="display:flex;gap:8px"> <sui-button type="submit" variant="brand" label="Submit"></sui-button> <sui-button type="reset" variant="outline" label="Reset"></sui-button> </div> <output id="mask-out" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Submit to see what posts.</output> </form>
// setup(root): properties and events setup(root) { const form = root.querySelector('#mask-form'); const out = root.querySelector('#mask-out'); form.addEventListener('submit', (e) => { e.preventDefault(); out.textContent = [...new FormData(form)].map(([k, v]) => `${k}=${v || '(empty)'}`).join(' '); }); form.addEventListener('reset', () => { out.textContent = 'Reset. Submit to see what posts.'; }); }

Events

suiInput fires on every keystroke and suiChange on commit at blur or Enter, both with the formatted value. suiComplete fires once, the moment the required slots fill. suiFocus and suiBlur follow the input.

Idle.
<div style="display:grid;gap:10px;max-width:340px"> <sui-input-mask id="mask-events" label="EIN" name="ein" mask="99-9999999"></sui-input-mask> <span id="mask-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Idle.</span> </div>
// setup(root): properties and events setup(root) { const log = root.querySelector('#mask-log'); const el = root.querySelector('#mask-events'); el.addEventListener('suiFocus', () => { log.textContent = 'suiFocus'; }); el.addEventListener('suiBlur', () => { log.textContent = 'suiBlur'; }); el.addEventListener('suiInput', (e) => { log.textContent = `suiInput ${e.detail || '(empty)'}`; }); el.addEventListener('suiChange', (e) => { log.textContent = `suiChange ${e.detail || '(empty)'}`; }); el.addEventListener('suiComplete', (e) => { log.textContent = `suiComplete ${e.detail}`; }); }

Keyboard

KeyAction
Digits and lettersAccepted when they fit the slot at the caret; the field inserts the next literal for you. Anything else is dropped.
Any character on a full fieldDropped: the mask has no room to shift.
Ctrl or Cmd shortcutsSelect all, copy, cut, paste and undo work as in any text field, on a full field too. Pasted text is fitted to the mask.

API

<sui-input-mask>

Properties

PropertyAttributeTypeDefaultDescription
autoClearauto-clearbooleanfalseDrops a half-typed value when the field loses focus, so a phone number with three digits in it leaves as an empty field rather than a wrong one. Off by default: the partial text stays, and the field reports it as patternMismatch until the required slots are full.
autocompleteautocompletestringstring | undefinedNative autocomplete token. Off by default: autofilled text is re-packed positionally, so a value carrying a country code or extension mis-formats (+1 812 282 0135 becomes (181) 228-2013). Opt in only where the mask matches the token exactly.
disableddisabledbooleanfalseDisables the field.
errorerrorstringstring | undefinedError message; paints the field in the danger tone and sets aria-invalid.
helphelpstringstring | undefinedHelp text under the field. Replaced by error when set.
invalidinvalidbooleanfalseDanger border without a message (validation handled elsewhere).
labellabelstringstring | undefinedField label. Without one the field takes the host's aria-label or aria-labelledby as its name; a visible label wins over both.
maskmaskstring'(999) 999-9999'Mask pattern: 9 digit, a letter, * alphanumeric, anything else literal. A ? marks everything after it optional, so 99999?-9999 takes a five or a nine digit ZIP and suiComplete fires at five.
namenamestringstring | undefinedForm control name. Reflected: form entry construction reads the content attribute, while all three framework bindings set properties, so an unreflected name posts nothing at all.
placeholderplaceholderstringstring | undefinedPlaceholder; defaults to the mask with slots shown as underscores and the ? dropped.
readOnlyreadonlybooleanfalseDisplay only: the text can be selected and copied but not edited; the field still posts.
requiredrequiredbooleanfalseMarks the field required (asterisk + valueMissing validity when empty).
sizesize"lg" | "md" | "sm"'md'Control height: sm 28 for table rows, md 34 for forms and toolbars (the default, shared with the select family and the buttons, so a form built from defaults is one height), lg 42 for a sign-in or marketing page.
unmaskunmaskbooleanfalsePost the typed characters instead of the formatted value: 6145551212 rather than (614) 555-1212. Off by default: the field posts exactly what it shows, which is what value, suiInput and suiChange all carry. Turn it on when the endpoint wants bare characters. getUnmasked() reads the same string without changing what the form posts.
valuevaluestring''The formatted value, as shown. Mutable; a raw write is re-formatted against the mask. getUnmasked() returns the typed characters alone.

Events

EventDetailReactVueDescription
suiBlurvoidonSuiBlur@sui-blurFires when the input loses focus; no detail. Does not bubble.
suiChangestringonSuiChange@sui-changeFires on commit (blur or Enter, native change); detail is the formatted value.
suiCompletestringonSuiComplete@sui-completeFires once when the required slots fill: every slot, or those before a ?. Detail is the formatted value.
suiFocusvoidonSuiFocus@sui-focusFires when the input takes focus; no detail. The same pair is on every text field. Does not bubble.
suiInputstringonSuiInput@sui-inputFires on every keystroke; detail is the formatted value.

Methods

MethodSignatureDescription
getUnmasked()getUnmasked() => Promise<string>Returns the typed characters without mask literals, e.g. "8122820135".
select()select() => Promise<void>Selects the field text.
setFocus()setFocus() => Promise<void>Moves keyboard focus into the field.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also