ブラウザとリアルの連携、Web NFC APIを使ってみる

Blog
2025.08.22
技術紹介, NFC
エンジニア 石橋 賢治

はじめに

Web NFC APIを使ってブラウザからNFCタグの情報を読み取るサンプルプログラムを解説しつつ、ご自身の課題と照らし合わせた時に、なにか思いつく手助けになればと思います。

「思いついたけど作れない!」といった場合は、お気軽にご相談いただけたらと思います(営業スマイル

考えられる使用例として

  • 設備の点検作業支援
  • 宝探しゲーム、スタンプラリー
  • クーポン配布手段など

抽象化すると、その「物理的にNFCタグをタッチしたという証明」に絡んだサービスが考えられるかと思います。

今回は読み取る機能に絞っていますが、書き込み機能も絡めるとより柔軟なサービスが考えられ、さらに温度計などセンサー機能が付いたNTAGを利用することで、現実世界を管理するサービスがイメージできるかと思います。

NFCに関する技術的仕様や、NFCタグの作成方法は「スマホをかざすだけでサイト表示」にまとめてあります。
今回記事内で触れないNFCの書き換えなどを行う際は、公式ドキュメントのWeb NFC : Exampleにサンプルコードがあるので、後に紹介するサンプルプログラムを改修すれば対応できるかと思います。

今回の記事では以下の動画で動作しているものを作成したいと思います。

技術仕様参照先や残念な注意事項

技術仕様

Web NFC APIの技術的な仕様は、World Wide Web Consortium (W3C) のコミュニティグループによって策定が進められています。
Web NFC – https://w3c.github.io/web-nfc

一度ざーっと目を通しておくと、ほえーってなります。

動作環境

Web NFC APIの動作環境はMozillaのMDN Web Docsにまとめられています。
MDN Web Docs – https://developer.mozilla.org/ja/docs/Web/API/Web_NFC_API

残念な仕様として、2025年8月時点で今回のサンプルプログラム含めWeb NFCはiOSでは動作しません
実機を元にiPhoneのSafari、およびChromeにおいて動作しないことを確認しています。

iPhone端末自体はNFCには対応しているものの、ブラウザから直接NFCタグ情報を取得することができません。
Core NFCというネイティブアプリ向けのフレームワークが用意されているので、ネイティブアプリとして取得して、アプリ内ブラウザでデータを渡すなどの迂回方法を取る必要があります。

Web NFC自体がまだ実験的な機能なのでしょうがないですが、日本での普及率の高い端末が対応していないのは広く展開するサービスでは痛いところです。
限られた範囲で展開するサービスであれば、iPhoneであっても迂回方法があり、利用される端末をAndroidに制限できるなどの環境であれば、候補としてあげてもよい技術かと思います。

お出かけ前チェックリストを作る

NFCタグ情報をブラウザ上で読み込むにあたって、想定として「お出かけ前のチェックリスト」をデジタル化したいと思います。

お出かけ前のチェックリストということで、冷蔵庫の中身やガスコンロの火の元確認などが項目として想定でき、NFCタグの貼り付け先は以下が考えられます。

  • 冷蔵庫の中身確認: 冷蔵の扉内側にNFCタグを貼り付け
  • ガスコンロ: 燃えにくい場所&金属でない場所として着火のボタン箇所に貼り付け

ということで我が家の冷蔵庫とガスコンロを紹介します。

冷蔵庫とガスコンロ

古い社屋だった頃の名刺の裏にNFCタグを貼り付けました、字が汚いですが仕様です。

NFCタグ自体の作成方法については過去のブログ記事を参照ください。

NFC Toolsで「書く」>「レコードの追加」>「データー」に進んで「Content-type」に「application/json」となるように入力。
「データ」にはJSON形式で「tid」というキーに、「reizouko」という値を指定します。
同様にガスコンロ用のNFCタグには「{“tid”: “gas-stove”}」としています。

あとはNFCタグにこれらを書き込んでチェックリスト用のNFCタグの完成です。

ページ作成と動作確認

ソースコードの解説はこの後行います。

ページ作成

まずはソースコードを貼り付けます。
適当な名前で保存して、Web上にファイルをアップロードしてください。

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web NFC チェックリスト</title>
    <style>
        body {
            font-family: sans-serif;
            max-width: 600px;
            margin: 2em auto;
            padding: 0 1em;
        }

        #scanButton {
            display: block;
            width: 100%;
            padding: 1em;
            font-size: 1.2em;
            cursor: pointer;
            margin-bottom: 1em;
        }

        #checklist {
            list-style: none;
            padding: 0;
        }

        #checklist li {
            font-size: 1.2em;
            padding: 0.5em;
            border: 1px solid #ccc;
            margin-bottom: 0.5em;
            border-radius: 5px;
        }

        #checklist input[type="checkbox"] {
            margin-right: 0.5em;
        }

        input[type="checkbox"]:checked+label {
            color: red;
            /* チェックされた時に赤色に変わる */
        }

        #log {
            margin-top: 1em;
            padding: 0.5em;
            background-color: #f0f0f0;
            border: 1px solid #ddd;
        }
    </style>
</head>

<body>
    <h1>お出かけ前のチェックリスト</h1>
    <p>下のボタンを押して、NFCタグをデバイスにタップしてください。</p>

    <button id="scanButton">お出かけ前チェック開始</button>

    <ul id="checklist">
        <li data-id="reizouko">
            <input type="checkbox" id="checkbox-1" disabled>
            <label for="checkbox-1">冷蔵庫の中身</label>
        </li>
        <li data-id="gas-stove">
            <input type="checkbox" id="checkbox-2" disabled>
            <label for="checkbox-2">ガスコンロの元栓</label>
        </li>
        <li data-id="room-lamp">
            <input type="checkbox" id="checkbox-3" disabled>
            <label for="checkbox-3">部屋の電気</label>
        </li>
    </ul>

    <div id="log"></div>

    <script>
        const scanButton = document.getElementById('scanButton');
        const log = document.getElementById('log');

        scanButton.addEventListener('click', async () => {
            log.textContent = "NFCタグを待機中です...";

            if (!('NDEFReader' in window)) {
                log.textContent = "このブラウザはWeb NFCに対応していません。";
                return;
            }

            try {
                const ndef = new NDEFReader();
                await ndef.scan();
                log.textContent = "> スキャンを開始しました。タグをタップしてください。";

                ndef.addEventListener('reading', ({ message }) => {
                    log.textContent = "> NFCタグを読み取りました。";

                    // NDEFメッセージ内の各レコードを処理
                    for (const record of message.records) {
                        if (record.recordType === "mime" && record.mediaType === "application/json") {
                            try {
                                // DataViewを文字列にデコード
                                const textDecoder = new TextDecoder();
                                const jsonString = textDecoder.decode(record.data);

                                // JSONをパース
                                const jsonData = JSON.parse(jsonString);

                                if (jsonData.tid) {
                                    log.textContent = `> 取得したTAG ID: ${jsonData.tid}`;
                                    // 対応するリストのチェックボックスをオンにする
                                    checkListItem(jsonData.tid);
                                } else {
                                    log.textContent = "> JSON内にIDが見つかりませんでした。";
                                }

                            } catch (error) {
                                log.textContent = `> JSONの処理中にエラーが発生しました: ${error}`;
                            }
                        }
                    }
                });

                ndef.addEventListener('readingerror', () => {
                    log.textContent = "NFCタグの読み取りに失敗しました。";
                });

            } catch (error) {
                log.textContent = `> スキャン開始時にエラーが発生しました: ${error}`;
            }
        });

        function checkListItem(tid) {
            // data-id属性が一致するli要素を検索
            const listItem = document.querySelector(`#checklist li[data-id="${tid}"]`);
            if (listItem) {
                // li要素内のチェックボックスを取得
                const checkbox = listItem.querySelector('input[type="checkbox"]');
                if (checkbox) {
                    checkbox.checked = true;
                    log.textContent += ` - 「${listItem.textContent.trim()}」をチェックしました。`;
                }
            } else {
                log.textContent += ` - TAG ID「${tid}」に一致するアイテムは見つかりませんでした。`;
            }
        }
    </script>
</body>

</html>

動作確認

最初にページにアクセスした画面です。
ユーザがチェックボックスを直接チェックすることはできないようにしてあります。

次に、Web NFCの仕様上ユーザの操作で読み取りイベントを起動しないといけないので「お出かけ前チェック開始」ボタンを設置して押した画面になります。
対応していない端末、ブラウザの場合は下部のログ出力にエラーの旨が表示されます。

最後の画面が、NFCタグをスキャンした状態です。
チェックボックスにチェックが入り、文字が赤く表示されます。

簡単なJavaScriptとNFCタグだけでチェックリストが作れました。

サンプルコードの解説

JavaScriptのサンプルコードについて簡単にカスタマイズするための箇所に触れつつ、解説したいと思います。

NFCタグ読み取り開始ボタンにクリックのイベントリスナーを登録しています。
if文でNFC読み取りAPIがブラウザで利用可能かチェックして、できない場合はログを出力しています。

scanButton.addEventListener('click', async () => {
    log.textContent = "NFCタグを待機中です...";

    if (!('NDEFReader' in window)) {
        log.textContent = "このブラウザはWeb NFCに対応していません。";
        return;
    }
    ...

try-catch文でエラーが発生したらエラーログを出力するようにしています。
NDEFReaderを初期化して変数へ格納し、その変数に対してイベントリスナーでスキャンされたか監視をしています。

try {
    const ndef = new NDEFReader();
    await ndef.scan();
    log.textContent = "> スキャンを開始しました。タグをタップしてください。";

    ndef.addEventListener('reading', ({ message }) => {
        log.textContent = "> NFCタグを読み取りました。";
        ...
} catch (error) {
    log.textContent = `> スキャン開始時にエラーが発生しました: ${error}`;
}

イベントでNFCタグのスキャンが確認された時にNDEFメッセージを取得し、各レコードごとに処理を行います。

各レコードにNFCタグのカスタムデータで指定したMIMEの「application/json」があるかを確認しています。
あれば、JSONをデコード後に「tid」が存在していれば、HTMLのチェックを入れるcheckListItem()を呼び出します。

NFCタグに別の意図で「application/json」使っている場合でも「tid」をキーに持つデータでなければ無視されます。
すでに別の用途で「tid」を使っていた場合はこの部分のコードと、NFCタグのカスタムデータ両方の命名を変えることで回避できます。

// NDEFメッセージ内の各レコードを処理
for (const record of message.records) {
    if (record.recordType === "mime" && record.mediaType === "application/json") {
        try {
            // DataViewを文字列にデコード
            const textDecoder = new TextDecoder();
            const jsonString = textDecoder.decode(record.data);

            // JSONをパース
            const jsonData = JSON.parse(jsonString);

            if (jsonData.tid) {
                log.textContent = `> 取得したTAG ID: ${jsonData.tid}`;
                // 対応するリストのチェックボックスをオンにする
                checkListItem(jsonData.tid);
            } else {
                log.textContent = "> JSON内にIDが見つかりませんでした。";
            }

        } catch (error) {
            log.textContent = `> JSONの処理中にエラーが発生しました: ${error}`;
        }
    }
}

最後に先ほど呼び出されたcheckListItem()ですが、DOM操作を行っているだけの処理です。
NFC読み取りの処理と、画面の処理を切り分けて管理するのが目的です。

#checklist li[data-id="${tid}"]から要素を全取得しているので、NFCタグで「tid」に指定した「reizouko」を変えたい場合は、HTMLの<li data-id="reizouko">の部分を変更してください。

チェック時に赤字になるのはCSSで指定しているので、表現を変えたい場合はそちらを修正してください。

function checkListItem(tid) {
    // data-id属性が一致するli要素を検索
    const listItem = document.querySelector(`#checklist li[data-id="${tid}"]`);
    if (listItem) {
        // li要素内のチェックボックスを取得
        const checkbox = listItem.querySelector('input[type="checkbox"]');
        if (checkbox) {
            checkbox.checked = true;
            log.textContent += ` - 「${listItem.textContent.trim()}」をチェックしました。`;
        }
    } else {
        log.textContent += ` - TAG ID「${tid}」に一致するアイテムは見つかりませんでした。`;
    }
}

さいごに

以上、NFCタグの読み取りに関するサンプルプログラムでした。

短いプログラムで実装できることが確認できたかと思います。

  • 読み取り後にサーバーに送信するような改修を行うことで、なにかの記録としたり
  • JSONではなくテキストデータや短い音楽データを入れ、カードに貼り付けることでオリジナルのカードゲームを作ったり

アイデア次第でなにかを作るきっかけの手助けになれば幸いです。

最近では小学生のプログラミング教育でも、今回扱ったJavaScriptを使用している学校もあるようです。
夏休みの自由研究として、NFCタグを読み取る応用を扱ってみるものいいかもしれないですね。

先駆ではWebとハードウェアを融合して、実際に使っていただけるシステムをお客様と一緒になって考えております。
「作ったはいいけど、現場がつかってくれない」などのお悩みがありましたら、お気軽にご相談ください。