Skip to content
CodeAtlas UI

Inputs

MaskedField

Text input with live formatting for Brazilian documents, phone numbers, CEP and dates.

<script>
  import { MaskedField, maskCPF, isValidCPF } from "@codeatlasdev/ui/forms";
  let cpf = $state("");
</script>

<MaskedField
  label="CPF"
  mask={maskCPF}
  validate={(v) => (isValidCPF(v) ? undefined : "Invalid CPF")}
  bind:value={cpf}
/>

These components live in the @codeatlasdev/ui/forms subpath. A mask is a pure function from raw text to formatted text, so you can write your own. MaskedField renders a TextField and forwards its props. Digit-only masks set inputmode="numeric" automatically.

The package never makes network requests. CepField only calls the onlookup function you inject (for example a ViaCEP client) once 8 digits are typed.

Documents try these values: CPF 123.456.789-09 is valid and 111.111.111-11 is invalid. Validation runs on blur.

Documents and dates

CPF, CNPJ, a mask that switches after 11 digits, a birth date and RG.

Switches mask after 11 digits.

Code
svelte
<script lang="ts">
  import {
    MaskedField,
    isValidCNPJ,
    isValidCPF,
    isValidDateBR,
    maskCNPJ,
    maskCPF,
    maskCPFCNPJ,
    maskDateBR,
    maskRG,
  } from '@codeatlasdev/ui/forms';

  let cpf = $state('');
  let cnpj = $state('');
  let doc = $state('');
  let birth = $state('');
  let rg = $state('');
</script>

<div class="col">
  <MaskedField
    label="CPF"
    placeholder="000.000.000-00"
    mask={maskCPF}
    validate={(v) => (isValidCPF(v) ? undefined : 'Invalid CPF')}
    bind:value={cpf}
  />
  <MaskedField
    label="CNPJ"
    placeholder="00.000.000/0000-00"
    mask={maskCNPJ}
    validate={(v) => (isValidCNPJ(v) ? undefined : 'Invalid CNPJ')}
    bind:value={cnpj}
  />
  <MaskedField label="CPF or CNPJ" description="Switches mask after 11 digits." mask={maskCPFCNPJ} bind:value={doc} />
  <MaskedField
    label="Birth date"
    placeholder="dd/mm/yyyy"
    mask={maskDateBR}
    validate={(v) => (isValidDateBR(v) ? undefined : 'Invalid date')}
    bind:value={birth}
  />
  <MaskedField label="RG" placeholder="00.000.000-0" mask={maskRG} bind:value={rg} />
</div>

<style>
  .col {
    display: flex;
    flex-direction: column;
    gap: var(--ca-space-4);
    width: 100%;
    max-width: 400px;
  }
</style>

Phone and CEP

PhoneField binds E.164 (+55...) while showing the masked form. CepField resolves an address through onlookup. Try 01310-100.

value = –

Try 01310-100.

–

Code
svelte
<script lang="ts">
  import { CepField, PhoneField, type CepAddress } from '@codeatlasdev/ui/forms';

  let phone = $state('');
  let cep = $state('');
  let pending = $state(false);
  let address = $state<CepAddress | null>(null);

  // Demo lookup. Real apps inject ViaCEP (or any provider) here.
  async function lookup(raw: string): Promise<CepAddress | null> {
    await new Promise((r) => setTimeout(r, 600));
    return raw === '01310100'
      ? { logradouro: 'Avenida Paulista', bairro: 'Bela Vista', localidade: 'São Paulo', uf: 'SP' }
      : null;
  }
</script>

<div class="col">
  <PhoneField label="WhatsApp" bind:value={phone} />
  <p class="out">value = {phone || '–'}</p>
  <CepField
    label="CEP"
    description="Try 01310-100."
    bind:value={cep}
    bind:lookupPending={pending}
    onlookup={lookup}
    onresolve={(a) => (address = a)}
  />
  <p class="out">
    {#if pending}Looking up…{:else if address}{address.logradouro}, {address.bairro} · {address.localidade}/{address.uf}{:else}–{/if}
  </p>
</div>

<style>
  .col {
    display: flex;
    flex-direction: column;
    gap: var(--ca-space-4);
    width: 100%;
    max-width: 400px;
  }

  .out {
    margin: 0;
    font-family: var(--ca-font-mono);
    font-size: var(--ca-font-size-caption);
    color: var(--ca-text-secondary);
  }
</style>

use:mask action

Apply any pure mask to a plain input. The caret stays on the same digit.

value = –

Code
svelte
<script lang="ts">
  import { mask, maskCPF } from '@codeatlasdev/ui/forms';

  let value = $state('');
</script>

<div class="col">
  <label class="lbl" for="mask-action-input">Plain input with <code>use:mask</code></label>
  <input id="mask-action-input" class="native" use:mask={maskCPF} bind:value placeholder="000.000.000-00" inputmode="numeric" />
  <p class="out">value = {value || '–'}</p>
</div>

<style>
  .col {
    display: flex;
    flex-direction: column;
    gap: var(--ca-space-2);
    width: 100%;
    max-width: 400px;
  }

  .lbl {
    font-size: var(--ca-font-size-body2);
    color: var(--ca-text-secondary);
  }

  .native {
    height: 44px;
    padding: 0 var(--ca-space-3);
    border: 1px solid var(--ca-outline-default);
    border-radius: var(--ca-radius-md);
    background: transparent;
    color: var(--ca-text-primary);
    font: inherit;
  }

  .out {
    margin: 0;
    font-family: var(--ca-font-mono);
    font-size: var(--ca-font-size-caption);
    color: var(--ca-text-secondary);
  }
</style>

MaskedField props

PropTypeDefaultDescription
mask(raw: string) => string–Required. Pure formatter applied while typing, for example maskCPF.
validate(value: string) => string | undefined–Returns an error message on blur for non-empty values, or undefined when valid.
inputmodeHTMLInputAttributes['inputmode']autoDefaults to 'numeric' for digit-only masks and 'text' otherwise.
valuestring''Bindable masked value (what the user sees).
errorstring–External error. Takes precedence over the validate result.
labelstring–Label text.
placeholderstring–Input placeholder.
descriptionstring–Hint shown below the field.
disabledbooleanfalseDisables the input.
requiredbooleanfalseMarks the field as required.
readonlybooleanfalseMakes the input read-only.
autocompleteHTMLInputAttributes['autocomplete']–Native autocomplete hint.
leading / trailingSnippet–Content before or after the input.
size'sm' | 'md''md'Field height.
namestring–Native name, for form submission.
oninput / onchange(value: string) => void–Called with the masked value.
onblur(e: FocusEvent) => void–Called when the input loses focus.

PhoneField and CepField props

PropTypeDefaultDescription
PhoneField.valuestring''Bindable E.164 value (+5511987654321). Empty while the number is incomplete or empty.
PhoneField.country'BR''BR'Only BR is supported today.
PhoneField.placeholderstring'(11) 98765-4321'Input placeholder.
CepField.valuestring''Bindable masked CEP (01310-100).
CepField.onlookup(cep: string) => Promise<CepAddress | null>–Injected lookup. Receives 8 digits. Return null for an unknown CEP.
CepField.onresolve(address: CepAddress) => void–Called with the address after a successful lookup.
CepField.lookupPendingbooleanfalseBindable. True while onlookup is in flight.
CepField.placeholderstring'00000-000'Input placeholder.

Exports

PropTypeDefaultDescription
maskCPF / maskCNPJ / maskCPFCNPJ(raw: string) => string–Document masks. maskCPFCNPJ switches after 11 digits.
maskDateBR / maskRG / maskCEP / maskPhoneBR(raw: string) => string–Date (dd/mm/yyyy), RG, CEP and Brazilian phone masks.
isValidCPF / isValidCNPJ / isValidDateBR(value: string) => boolean–Validators for use in validate.
maskAction<HTMLInputElement, (raw: string) => string>–Svelte action that applies a mask to any input.