1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
|
# cepis-autofill
Drop-in address autofill for Brazilian checkouts, on top of the
[public CEP API at cep.is](https://cep.is). The shopper types a CEP; the
widget fills the street, neighbourhood, city and state fields you point it
at. Official Correios data (DNE).
One `<script>` tag, no build step, no framework, no dependencies.
**3.3 KB minified and gzipped** (7.7 KB minified, 7859 bytes).
```html
<script src="cepis-autofill.js"></script>
<script>
CepisAutofill.attach({
cep: "#cep",
logradouro: "#endereco",
bairro: "#bairro",
localidade: "#cidade",
uf: "#estado",
numero: "#numero",
});
</script>
```
Nothing about any particular e-commerce platform is baked in: which field
is which is configuration. See
[Mapping your platform's fields](#mapping-your-platforms-fields).
## Install
Copy `dist/cepis-autofill.min.js` next to your theme's other assets and load
it with a `<script>` tag. It defines the global `CepisAutofill` and nothing
else.
Bundling instead? `dist/cepis-autofill.esm.js` is the same code as an ES
module:
```js
import { attach } from "./cepis-autofill.esm.js";
```
Requires a browser with `fetch` and `AbortController` — every browser since 2017. Run `make build` to produce `dist/`.
## Usage
Give `attach` the field the shopper types into, plus the fields to fill.
Every selector accepts a CSS string or an element:
```js
const handle = CepisAutofill.attach({
cep: "#cep",
logradouro: "#endereco",
bairro: "#bairro",
localidade: "#cidade",
uf: "#estado",
numero: "#numero",
focusNumero: true,
});
```
A lookup goes out once the field holds 8 digits and the shopper stops
typing. The `01001-000` mask is accepted. Repeat lookups of the same CEP are
served from memory, so going back to fix the house number costs nothing.
A `<select>` for the state is matched by option value or label, against both
the two-letter UF and the full state name — `SP` and `São Paulo` both work.
The `numero` field is never written to; it exists so `focusNumero` knows
where to send the cursor.
`attach` returns a handle:
```js
handle.lookup(); // run now, skipping the debounce
handle.lookup("01001-000"); // …for a specific CEP
handle.detach(); // remove listeners, abort anything in flight
```
Call `handle.lookup()` when your checkout writes into the CEP field without
firing an `input` event — a "use my saved address" button, for instance.
### Options
| option | default | what it does |
| ----------------------------------------------------------------------------------------------------------------------- | ---------------- | --------------------------------------------------------- |
| `cep` | — | the field the shopper types into (required) |
| `logradouro`, `complemento`, `unidade`, `bairro`, `localidade`, `uf`, `estado`, `regiao`, `ibge`, `gia`, `ddd`, `siafi` | — | fields to fill; map only the ones you have |
| `numero` | — | house-number field, for `focusNumero` |
| `baseUrl` | `https://cep.is` | API root; point it at your own instance |
| `debounceMs` | `250` | quiet period after the last keystroke |
| `timeoutMs` | `8000` | deadline for one request |
| `overwrite` | `"auto"` | what to do with a field that already has a value |
| `focusNumero` | `false` | move focus to `numero` after a successful lookup |
| `status` | auto | where to announce progress |
| `messages` | see below | override any status message |
| `onResult` | — | `(address, { cep }) => void`, after the fields are filled |
| `onError` | — | `(error, { cep }) => void` |
| `onStatus` | — | `(state, message) => void` for a custom UI |
`overwrite` decides what happens to a field that is not empty:
- `"auto"` — fill empty fields, replace values this widget wrote itself, and
leave anything the shopper typed alone. A manual correction survives the
next CEP.
- `false` — only ever fill empty fields.
- `true` — always overwrite.
The widget writes through the native value setter and dispatches `input` and
`change`, so validation in the page — including React's — sees the values.
It never reformats what the shopper typed in the CEP field.
### Status and accessibility
Unless you pass `status`, a `<div role="status" aria-live="polite">` is
inserted right after the CEP field and referenced from its
`aria-describedby`. It is created empty, at a fixed minimum height, so
announcing something never shifts the page. Focus is never taken, except by
`focusNumero`, which moves it only while the CEP field still has focus.
Style it through `.cepis-autofill-status[data-cepis-state="…"]`, where the
state is `idle`, `loading`, `found` or `error`. The message text carries the
meaning on its own — do not use colour as the only signal:
```css
.cepis-autofill-status[data-cepis-state="error"]::before {
content: "! ";
}
```
Pass `status: "#my-node"` to use an element of your own, or `status: false`
to render nothing and drive your own UI from `onStatus`.
### Messages and errors
Every failure has its own message, in Portuguese, and reaches both the
status region and `onError`:
| `error.code` | message | worth retrying? |
| ------------------ | ------------------------------------------------------------------------- | --------------- |
| `invalid` | CEP inválido. Digite os 8 dígitos. | no |
| `not_found` | CEP não encontrado. Confira o número ou preencha o endereço manualmente. | no |
| `rate_limited` | Muitas consultas em pouco tempo. Aguarde alguns segundos e tente de novo. | later |
| `server` | O serviço de CEP está indisponível. Preencha o endereço manualmente. | later |
| `network` | Não foi possível consultar o CEP. Verifique sua conexão e tente de novo. | later |
| `timeout` | A consulta do CEP demorou demais. Tente de novo ou preencha manualmente. | later |
| `invalid_response` | Resposta inesperada do serviço de CEP. Preencha o endereço manualmente. | probably not |
Override any of them with `messages`:
```js
CepisAutofill.attach({
cep: "#cep",
localidade: "#cidade",
messages: { not_found: "Não achamos esse CEP. Digite o endereço." },
});
```
`rate_limited` carries `retryAfterMs` and `server` carries `status`.
### Looking a CEP up yourself
`CepisAutofill.lookup` is the request layer on its own — same cache, same
errors, no DOM:
```js
const address = await CepisAutofill.lookup("01001-000");
address.logradouro; // "Praça da Sé"
address.uf; // "SP"
```
It resolves to the API's own JSON: Portuguese field names, empty strings
rather than nulls, and `geo` present only for the roughly 57% of CEPs that
have a coordinate.
## Mapping your platform's fields
Every checkout names its fields differently, and the same platform names
them differently between themes, versions and one-page checkout apps. **No
per-platform preset is shipped here, because none could be verified against
a live store.** Finding yours takes a minute:
1. Open your checkout, right-click the CEP field, Inspect.
2. Read the `id` or `name` on the `<input>`. An `id` gives you `#billing_postcode`;
a `name` gives you `[name="postalCode"]`.
3. Do the same for street, neighbourhood, city, state and number.
4. Paste this in the console to confirm every selector resolves:
```js
["#cep", "#endereco", "#bairro", "#cidade", "#estado", "#numero"].forEach((s) =>
console.log(s, document.querySelector(s)),
);
```
Anything printing `null` is wrong. A selector that resolves to nothing is
skipped silently, which is what makes a wrong one hard to spot.
Two things worth knowing for checkouts that re-render (VTEX, Shopify and
most React-based ones):
- Pass **CSS strings, not elements**. Strings are resolved on every use and
the listener is delegated from `document`, so a form that is destroyed and
rebuilt keeps working. Elements are held directly and stop working when
that node is replaced.
- Load the widget after the checkout has rendered its form once, or call
`attach` from whatever hook the platform gives you for "checkout ready".
Where the CEP field is a component that writes its value without firing
`input`, keep the handle and call `handle.lookup()` from the component's own
change hook.
## Development
```sh
make # list the targets
make test # bun test — drives a real Chromium against a fake API
make lint # prettier --check plus a bundle syntax check
make format # apply prettier
make build # bundle to dist/ and print the gzipped size
make check # lint, then test, then build
```
The tests stand up two `Bun.serve` instances — one serving the checkout
fixture with a freshly built bundle, one impersonating cep.is — and drive
Chromium through Playwright: real keystrokes, real `fetch`, real aborts.
They need a Chromium build; install one with
`bunx playwright install chromium`, or point `CEPIS_CHROMIUM` at an existing
binary.
`demo.html` runs against the real API. Run `make build` and open it.
## Licence
ISC. See [LICENSE](LICENSE).
|