Uint8Array. It imports nothing from
React Native, nothing from Expo, and nothing native — a lint rule fails the build
if that ever changes. As a result it works in a bundler, in Node and on the web,
and it is tested at 100% branch coverage rather than on a device.
You can use it without the rest of the library: to build a message on a server
before sending it to a phone, to parse bytes captured from a reader, or to write
tests for your own tag logic.
Reading a tag
Writing a tag
Encoding an empty list produces the canonical empty message — a single record with TNF
0x00 —
which is what erasing a tag writes. Producing zero bytes instead would leave whatever was
previously on the tag partially readable.Record types
Which one to use for your own data
Use an external record. Its type name is namespaced by a domain you control, so it cannot collide with another app’s records the way a bare MIME type can:URI prefix abbreviation
A URI record stores a one-byte prefix identifier plus the remainder, saving 7–12 bytes. That is a meaningful fraction of a small tag: an NTAG213 has 144 usable bytes. The longest matching prefix always wins:Text records and UTF-16
A text record’s status byte selects the encoding. Bit 7 means UTF-16, and plenty of Windows and Java writers set it:Smart Posters
A Smart Poster’s payload is itself a complete NDEF message, with its own record framing. That is why a decoder built only for flat records mangles them.unknown rather
than dropped, so a lossy read never looks like a complete one.
Chunked records
A record can set the Chunk Flag to continue its payload in the records that follow.decodeMessage reassembles those transparently: you get one record with
the full payload, and never see CF.
The encoder deliberately never emits chunks. Chunking exists so a writer can
stream a payload whose length it does not yet know, which never applies once the
payload is a Uint8Array in memory.
Tag-level framing
The NDEF message is usually not the first thing in tag memory.Type 2 tags (NTAG, MIFARE Ultralight)
The message sits inside a TLV block, alongside lock and memory control blocks the manufacturer wrote when the tag was formatted. Feeding a raw data area todecodeMessage fails, because the first byte is a TLV tag, not a record header.
findNdefMessageTlv returns undefined when the tag is formatted but holds no
NDEF block — deliberately distinct from holding an empty message, because you may
want to write rather than report a read failure.
Type 4 tags (DESFire, Java Card applets)
The message lives in its own elementary file, and the capability container says which file, how large it may grow, and whether it is readable and writable.writable being false covers two different situations, and the difference
matters when you are deciding what to tell the user:
Byte helpers
fromHex tolerates spaces, colons and dashes because that is how AIDs and keys
appear in datasheets.
UTF-8 and UTF-16 are implemented here rather than delegating to
TextEncoder/TextDecoder, so no polyfill is ever required on any engine. UTF-8
decoding is strict: overlong forms, encoded surrogates and out-of-range code
points are rejected rather than replaced with U+FFFD, because for tag data a
replacement character is a silent corruption you discover much later.
Errors
Everything throwsNfcError with a machine-readable code:
NfcError.is() rather than instanceof: it brands on the error’s name, so it
keeps working when two copies of the package end up in one bundle. Every code is in
the error reference.
Strictness, and where it is deliberately relaxed
Decoding is strict where leniency would hide corruption, and lenient where strictness would make valid tags unreadable. Rejected:- A message with no Message Begin or no Message End flag
- Trailing bytes after the last record
- A chunk chain that never closes, or a continuation carrying a type or ID
- A payload length that runs past the available bytes
- Overlong UTF-8, encoded surrogates, odd-length UTF-16
- A TLV using the 3-byte length form for a length that fits in one byte
- A capability container whose declared length exceeds what was read
- Zero bytes, decoded as an empty message — what a formatted but never-written tag reads back as
- A Type 2 data area that ends with no terminator TLV — a tag whose data area is exactly full has no room for one
- A text record with the reserved status bit set
- Reserved URI prefix identifiers and unknown TNF values
- Unrecognised records inside a Smart Poster, returned rather than dropped
Next
Tags
readNdef, writeNdef, getNdefStatus and formatting.Tag protocols
Below NDEF: ISO 7816, ISO 15693, FeliCa, NTAG.