Tucano v0.37.2

Masks

Formats as you type, verifies the check digit of documents and hides sensitive data. It doesn't replace the field with another control: it's behavior on top of your <input>, which keeps its name and the styling the project already applies.

CPF is the Brazilian individual taxpayer ID, and the amount is in Brazilian reais. Type an invalid CPF and leave the field: the browser starts blocking the submit.

Examples

It's all the same data-tuc-mask with another format. The placeholder text comes from the pattern — nobody writes 000.000.000-00 by hand. The named formats follow Brazilian documents and conventions; the free pattern works for anything else.

CNPJ

The Brazilian company tax ID. Accepts the new format, with letters.

CPF or CNPJ

Switches pattern by length.

Phone

Brazilian landline with 8 digits or mobile with 9.

Mobile

Always with 9 digits.

CEP

cep, the Brazilian postal code.

Card

card, in groups of four.

Date and time, as text

date and time — just the mask, no calendar.

Currency without a symbol

currency, and three decimal places with data-decimals="3".

Another currency

data-currency="USD" on top of currency.

Free pattern

data-tuc-mask="##/##", card expiry date.

Pattern with letters

AAA-####: A is a letter, # is a digit. A free pattern doesn't convert to uppercase.

With a server error

aria-invalid="true" on the field.

Invalid CNPJ.

How to use

Mark the <input> with the format, and it initializes by itself on load and on every htmx:afterSwap. The component puts the .tuc-input class on the field and sets a numeric inputmode when the pattern doesn't accept letters, so phones open the number keypad.

<input name="cpf"      data-tuc-mask="cpf" data-validate="true">
<input name="document" data-tuc-mask="cpf-cnpj">
<input name="phone"    data-tuc-mask="phone">
<input name="amount"   data-tuc-mask="brl">
<input name="expiry"   data-tuc-mask="##/##">
FormatResult
cpf111.444.777-35
cnpj11.222.333/0001-81 — accepts letters in the first 12 positions
cnpj-numericThe same, rejecting letters
cpf-cnpjOne or the other, by length
phone(69) 3344-5566 or (69) 99988-7766
mobile(69) 99988-7766
cep69900-000
date time25/12/2026 · 14:30
card4111 1111 1111 1111
realR$ 1.234,50
currency1.234,50, no symbol — with data-currency, that currency's symbol
free pattern# digit, A letter, * either; everything else goes in as is
AttributeDefaultWhat for
data-tuc-mask—Format name or free pattern
data-validatefalseVerifies the check digit when leaving the field
data-error-textfrom the formatBrowser message when validation fails
data-decimals2Decimal places for currency
data-currency—Currency code, such as BRL or USD
data-tuc-reveal—Show and hide eye; the value picks the mode
data-reveal-visible2Characters left visible in end mode
data-reveal-modefrom the fieldend, email or all

In the Django form

class CustomerForm(forms.ModelForm):
    class Meta:
        model = Customer
        fields = ["document", "phone", "credit_limit"]
        widgets = {
            "document": forms.TextInput(attrs={"data-tuc-mask": "cpf-cnpj", "data-validate": "true"}),
            "phone": forms.TextInput(attrs={"data-tuc-mask": "phone"}),
            "credit_limit": forms.TextInput(attrs={"data-tuc-mask": "brl"}),
        }

The field posts the text as it appears on screen: 111.444.777-35, R$ 1.234,50. Clean it in clean_ — browser validation protects the screen, not the server.

def clean_document(self):
    return re.sub(r"[^0-9A-Z]", "", self.cleaned_data["document"].upper())

def clean_credit_limit(self):
    text = re.sub(r"[^\d,]", "", self.cleaned_data["credit_limit"])
    return Decimal(text.replace(",", ".")) if text else None

In JavaScript

const m = new Tucano.Mask('#amount', { format: 'brl' });
m.getRaw();          // '123450' — no punctuation
m.getNumber();       // 1234.5 — currency formats only; null for the others
m.setValue('99');    // formats and fires tucano:change
m.isValid();         // verifies the check digit, on formats that have one
m.destroy();

document.querySelector('#amount').addEventListener('tucano:change', (e) => {
  e.detail.value;    // 'R$ 1.234,50'
  e.detail.raw;      // '123450'
  e.detail.number;   // 1234.5
});

The utilities behind the mask are exposed too, to validate or format outside a field:

Tucano.mask.validateCPF('111.444.777-35')              // true
Tucano.mask.validateCNPJ('11.222.333/0001-81')         // true
Tucano.mask.validateCpfCnpj('123.456.789-01')          // false
Tucano.mask.format('12345678901', 'cpf')               // '123.456.789-01'
Tucano.mask.applyCurrency('12345', { currency: 'BRL' }) // 'R$ 123,45'
Tucano.mask.maskEmail('contato@empresa.com.br')        // 'c••••••@empresa.com.br'

Validation

With data-validate, the check digit is verified when leaving the field. If it fails, the field gets aria-invalid="true" and setCustomValidity, and the browser itself blocks the submit — without a line in the project.

And a correct value shows right away: as soon as the value is complete and right, the field turns green, without waiting for it to lose focus. The error waits — while typing, a half-typed CPF never turns red; it is only flagged when leaving the field, and clears again once the person goes back to fix it. The green state is the data-tuc-valid attribute, painted with --tuc-success.

It applies to cpf, cnpj, cnpj-numeric and cpf-cnpj; the other formats have no digit to verify. An empty field passes: requiredness belongs to required. While typing the error goes away, and when coming back to the field too — including the flag that came from the server, because the person is fixing it.

<input name="cpf" data-tuc-mask="cpf" data-validate="true" data-error-text="Check the CPF">

Alphanumeric CNPJ

The new format keeps the 14 positions and the same mask: the first 12 accept letters, the last 2 stay numeric, and the check digit uses the ASCII code minus 48 — which keeps the old calculation valid for all-digit CNPJs. cnpj accepts both and converts letters to uppercase; cnpj-numeric rejects letters, for those who can't receive them yet. Confirm the effective date in the Brazilian Federal Revenue's Technical Note before requiring it in production.

Sensitive field

data-tuc-reveal adds an eye to show and hide. A field that arrives already filled starts hidden, with only the end visible; an empty field starts visible, because whoever is typing needs to see. Passwords are the exception: they always start hidden.

CPF from a record

end mode, with two visible.

Card

data-reveal-visible="4" keeps the last four.

Email

On type="email", keeps the domain.

API key

data-tuc-reveal="all" shows nothing.

Password

On type="password" the eye only switches the type.

Empty

Starts visible; the eye hides it after typing.

ModeWhenResult
end, the defaultDocument, card, account, phone•••• •••• •••• 1234
emailAutomatic on type="email"j•••••••••@empresa.com.br
allToken, API key••••••••••••••••••••
<input name="cpf" value="111.444.777-35" data-tuc-mask="cpf" data-tuc-reveal>
<input name="card" data-tuc-mask="card" data-tuc-reveal data-reveal-visible="4">
<input name="api_key" data-tuc-reveal="all">
<input type="email" name="email" data-tuc-reveal>
<input type="password" name="password" data-tuc-reveal>

What the form posts doesn't change because it's hidden. Except for type="password", the name moves to an <input type="hidden"> with the real value, and the visible field shows the dots. While hidden, the field is read-only: typing over the dots would overwrite the value without the person noticing. The separators stay in place, so the shape remains recognizable: •••.•••.•••-35.

With a mask, the hidden field carries the value without punctuation

In a field with data-tuc-mask and data-tuc-reveal, what reaches the server is 11144477735, not 111.444.777-35 — it's getRaw(). Without a mask, the text goes as it was typed.

Why start hidden, and why email is the other way around

Screenshots, support recordings, someone looking over your shoulder: the full data showed up without anyone asking. Starting hidden is the difference between exposing by default and exposing by choice. In an email the domain is what helps recognize the account and the local part is what identifies the person — keeping the end would reveal om.br and hide the useful part. Without an @, it hides everything.

Loose text on the page

Not all sensitive data lives in a field: the CPF on a customer screen, the card number in a table cell, the API key in a paragraph. data-tuc-reveal works on any element, in the same modes, and together with data-tuc-format. The text starts hidden, with the eye next to it.

CustomerCPFCardEmail
John Smith 11144477735 4111 1111 1111 1234 john.smith@company.com

API key: sk_live_a1b2c3d4e5f6

<td><span data-tuc-format="cpf" data-tuc-reveal>{{ customer.cpf }}</span></td>
<td><span data-tuc-reveal data-reveal-visible="4">{{ card.number }}</span></td>
<p>API key: <span data-tuc-reveal="all">{{ api_key }}</span></p>

On the page, hiding is visual only

The full value is still in the HTML: anyone who opens the page source sees it. It protects against screenshots, recordings and someone looking over your shoulder. If the data must not reach the browser, hide it on the server and send only what may be shown, such as the card's last four digits.

Display only

For what already comes from the database and isn't edited, you don't need a field: data-tuc-format formats the text of the element itself.

MarkupOn screen
data-tuc-format="cpf" · 1234567890112345678901
data-tuc-format="cnpj" · 1122233300018111222333000181
data-tuc-format="cnpj" · 12ABC34501DE3512ABC34501DE35
data-tuc-format="phone" · 6999988776669999887766
data-tuc-format="cep" · 6990000069900000
data-tuc-format="brl" · 1234.51234.5
data-tuc-format="currency" data-decimals="3" · 12.512.5
<span data-tuc-format="cpf">{{ customer.cpf }}</span>
<span data-tuc-format="brl">{{ order.total }}</span>
<span data-tuc-format="phone" data-value="{{ customer.phone }}"></span>

Accepts cpf, cnpj, cpf-cnpj, phone, mobile, cep, card, real, currency and free patterns, with data-decimals and data-currency for currency. The value comes from the element's text or from data-value. Currency reads the database's decimal point (1234.5) and also a comma. Content that doesn't fill the pattern comes back as it was, instead of coming out half done and looking corrupted. For HTML inserted by some other path than HTMX, use Tucano.autoFormat(element).

Keyboard and accessibility

The mask uses the browser's keyboard handling, with no shortcuts of its own. What it takes care of is keeping the cursor from getting lost.

KeyAction
Backspace DeleteOver a separator, deletes the neighboring character, instead of getting stuck
PasteDiscards what doesn't fit: abc111x444.777--35 becomes 111.444.777-35
Typing in currencyFills from right to left, with the cursor always at the end
Tab to the eyeIt's a <button>: Enter and Space toggle

The eye announces "Show" or "Hide" and its state through aria-pressed. When showing, focus goes to the field. When it fails validation, the field gets aria-invalid="true", which the screen reader reads as invalid.

API

Generated from the code on every build — if something is not here, it does not exist.

Markup
[data-tuc-mask], [data-tuc-reveal] [data-tuc-format]:not([data-tuc-formatted])
In JS
new Tucano.Mask(alvo, opcoes) Tucano.autoFormat()
Attributes
data-currency data-decimals data-error-text data-reveal-mode data-reveal-visible data-tuc-format data-tuc-mask data-tuc-reveal data-validate data-value
Methods
getRaw getNumber setValue isValid destroy
Events
tucano:change

Options

The notes in this table come from comments in the source code, which are written in Portuguese.

OptionDefaultWhat for
formatnullnome de FORMATS ou gabarito livre
validatefalsevalida no blur e bloqueia o submit
decimals2
currencynull'BRL' formata com R$
revealfalseolhinho para mostrar e ocultar
revealVisible2quantos caracteres ficam a mostra no modo 'end'
revealModenull'end' | 'email' | 'all'. null decide pelo campo
localeundefined
errorTextnull
onChangenull