Masked input ​

Pattern-masked text input. The field shows a formatted display string; onValueChange / v-model receive the raw alphanumeric characters kept by the mask (# digits, A letters, * alphanumeric; other characters are literals).

PreviewMasked display

Usage ​

tsx
import { useState } from "react";
import { MaskedInput } from "@sometic/react/input";

export function Example() {
    const [raw, setRaw] = useState("");
    return (
        <MaskedInput mask="(###) ###-####" value={raw} onValueChange={setRaw} placeholder="Phone" />
    );
}
vue
<script setup>
import { ref } from "vue";
import { MaskedInput } from "@sometic/vue/input";

const raw = ref("");
</script>

<template>
    <MaskedInput v-model="raw" mask="(###) ###-####" placeholder="Phone" />
</template>
js
import { createMaskedInputController } from "@sometic/dom/input-masked";

const input = document.querySelector("input");
const controller = createMaskedInputController({
    mask: "(###) ###-####",
    defaultValue: "",
    onValueChange(next) {
        console.log(next);
    },
});

const apply = () => {
    input.value = controller.getDisplayValue();
};
apply();

input.addEventListener("input", () => {
    controller.setFromDisplay(input.value);
    apply();
});
html
<script type="module">
    import { registerInputElements } from "@sometic/elements/input";
    registerInputElements();
</script>

<sometic-masked-input mask="(###) ###-####" placeholder="Phone"></sometic-masked-input>
html
<script src="https://cdn.jsdelivr.net/npm/@sometic/elements@1.1.6/dist/cdn/sometic-elements.iife.js"></script>

<sometic-masked-input mask="(###) ###-####" placeholder="Phone"></sometic-masked-input>
html
<script
    type="module"
    src="https://cdn.jsdelivr.net/npm/@sometic/elements@1.1.6/dist/cdn/sometic-elements.esm.js"
></script>

<sometic-masked-input mask="(###) ###-####" placeholder="Phone"></sometic-masked-input>

Vue ​

vue
<script setup>
import { ref } from "vue";
import { MaskedInput } from "@sometic/vue/input";

const raw = ref("");
</script>

<template>
    <MaskedInput v-model="raw" mask="(###) ###-####" placeholder="Phone" />
</template>

How it works ​

  1. Engine (@sometic/dom/input-masked): parseMask / formatMasked map raw characters onto mask tokens. createMaskedInputController holds controllable raw string state and resolves display via resolveInput (type: "text").
  2. Adapters: React MaskedInput requires mask: string; controlled value is raw. On change it strips to alphanumeric, then keeps only characters that fit the mask (formatMasked(...).raw). Vue mirrors with required mask and v-model (raw).
  3. Custom element: sometic-masked-input observes mask, value, disabled, readonly, invalid, placeholder, shadow, syncs the display string into a light-DOM <input>, and dispatches value-change with { value } (raw).

Masking is formatting, not validation or a security boundary.

Anatomy ​

PartRole
Native inputShows masked display; app owns raw

State attributes follow Input: data-disabled, data-readonly, data-invalid, data-filled, data-empty, optional data-size / data-variant.

Props / attributes ​

React MaskedInputProps ​

Omit<InputProps, "type" | "value" | "defaultValue"> plus:

PropTypeDefaultDescription
maskstringrequiredPattern tokens
valuestring,Controlled raw value
defaultValuestring,Uncontrolled initial raw
onValueChange(raw: string) => void,Emits raw characters
disabledbooleanfalseDisables input
readonlybooleanfalseRead-only
invalidbooleanfalseInvalid + aria-invalid
name / placeholderstring,Native association / hint
styling propsfrom Input,unstyled, classes, …
Native attrsremaining input HTML attrs,Forwarded; ref supported

Vue ​

Required mask, modelValue (raw string), disabled, readonly. Emits update:modelValue with raw. Use v-model.

Custom element (sometic-masked-input) ​

Observed: mask, value, disabled, readonly, invalid, placeholder, shadow. Event: value-change → { value: string } (raw).

Events / callbacks ​

SurfaceEventPayload
ReactonValueChangeraw string
Vueupdate:modelValueraw string
Custom elementvalue-change{ value }

Callbacks are ignored while disabled or readonly on the adapter path. Display string is never the event payload.

Controlled vs uncontrolled ​

Same pattern as Input, but the controlled value is raw, not display:

  • Controlled: pass value (raw) + onValueChange.
  • Uncontrolled: omit value, optional defaultValue.
  • Vue: v-model binds raw.

Form participation ​

Native name posts whatever is in the DOM input (display). Prefer reading Sometic raw state (or formatting on submit) when validating or storing. Pair with @sometic/validation on the raw string inside Form.

Accessibility ​

  • Label via Field; describe the expected format in description text (“US phone: (555) 123-4567”).
  • Masking does not replace validation; set invalid + error text when rules fail.
  • Keyboard behavior is native text input (Tab, caret, selection).
  • Do not use the mask as the only error announcement; use Field / live regions for failures.

Styling ​

Same input state attrs. Example:

tsx
<MaskedInput mask="###-##-####" unstyled classes={{ root: "field-control" }} invalid={hasError} />

Edge cases ​

  • Incomplete input, raw may be shorter than the pattern; display stops filling tokens.
  • Paste, alphanumeric characters are filtered then applied to the mask; excess is dropped by token walk.
  • Mask tokens, # = digit, A = letter, * = alphanumeric; everything else is a literal.
  • React strip vs CE controller, React adapters filter to [a-zA-Z0-9] before formatMasked; prefer the same mask tokens your CE demos use.
  • Not security, never treat masking as sanitization for secrets.
  • SSR, format helpers are pure; register the CE only in the browser.

Performance notes ​

formatMasked runs on each change. Keep masks modest in length. Import @sometic/react/input (or Vue/elements/dom masked subpaths) so unused specialized inputs tree-shake.

When to use / When not ​

Use for fixed patterns such as phone, ZIP, and simple ID formats.

Do not use for:

FAQ ​

Display vs raw? The UI shows the masked display; callbacks and controlled value use raw kept characters.

What are the mask tokens? # digit, A letter, * alphanumeric; other characters are literals (for example (, ), -, spaces).

Does paste work? Yes. Pasted text is filtered and walked through the mask; only matching characters are kept.

Is the mask validation? No. Pair raw output with @sometic/validation or your Form validators.

Why not CurrencyInput? Currency needs Intl formatting and numeric number | null values, not digit literals in a fixed pattern.

Can I change mask at runtime? Yes on CE (rebuilds controller) and React/Vue (re-format from current raw). Prefer stable masks per field to avoid surprising caret jumps.

Controlled empty string? value="" is controlled empty raw, not uncontrolled.

Ref forwarding? React forwards ref to the underlying <input>.

Light DOM or shadow? Light DOM default; shadow on the CE isolates styles.

Bundle tip? Import from @sometic/react/input or @sometic/dom/input-masked, not a mega barrel.