開発ブログ

BLOG

404 とエラーの画面を、Friko のドット絵で作り直した

ページが見つからないとき(404)と、表示の途中でエラーが起きたときの画面を作り直し、マスコットの Friko を輪郭だけの消えかけの姿で出すようにした。ドット絵を塗らずに枠だけの四角で描いて足元から薄くする方法、泣き顔の描き方、Next.js の global-error.tsx でも言語とダークモードを正しく出す方法を書く。

公開 2026年10月14日

ページが見つからないとき(404)と、表示の途中でエラーが起きたときの画面を作り直しました。どちらにも、マスコットの Friko が、輪郭だけで足元から薄れていく姿で出ます。404 では迷子の Friko、エラーでは泣き顔の Friko です。

404 の画面。上に輪郭だけの Friko と、その右にアクセントの色の「?」。下に「404 · NOT FOUND」のラベル、「ページを表示できません」の見出し、説明、ホームに戻るボタンとドキュメントを開くボタン
ページが見つからないときの画面

これまでの 404 は、アプリを今のデザインに切り替えたあとも、古いデザインの色のままでした。表示の途中でエラーが起きたときの画面はなく、Next.js が用意している英語の短い文だけの画面が出ていました。

輪郭だけで描く

Friko は、頭に振り子を載せてヘッドホンを付けた、ドット絵のキャラクターです。コードの中では、1 文字を 1 ドットとする文字の並びで絵を持っています。体のドットはアクセントの色(ユーザーが選べるテーマの色)で塗り、目と、振り子やヘッドホンの線は別の色にしています。

404 では、ドットを塗らずに、1 つずつ枠だけの四角(中抜きの四角)で描きました。輪郭だけの、透けたような見た目になります。

<rect
  x={pixel.x + inset}
  y={pixel.y + inset}
  width={1 - inset * 2}
  height={1 - inset * 2}
  fill="none"
  stroke="currentColor"
  strokeWidth={STROKE}
/>

最初は、体の外周の辺だけを線でなぞる方法で作りました。ところが、ヘッドホンと体のあいだのすき間にも内側の輪郭ができて、線が入り組み、Friko に見えませんでした。ドットごとに四角を描く方法なら、もとのドット絵の形がそのまま残ります。

消えかけに見せる

消えかけに見せるために、3 つのことをしています。404 とエラーの画面のどちらも同じです。

  • 足元ほど薄くする。不透明度を頭の上で 1、足元で 0.12 にし、そのあいだは高さの 2 乗に沿って下げる。下から先に消えていく
  • 体のドットの 6% を描かず、欠けたように見せる。振り子とヘッドホンの線は細く、欠けると何の形かわからなくなるので、欠けさせるのは体だけにした
  • ドットの 35% を、ゆっくり明滅させる。周期は 2.4〜6.4 秒で、ドットごとにずらす
404 の Friko を拡大したもの。中抜きの四角のドットで描かれ、足元に行くほど薄い。頭の右にアクセントの色の「?」
迷子の Friko

どのドットを欠けさせ、どれを明滅させるかは、乱数ではなくドットの座標から決めています。乱数で選ぶと、サーバーで描いた HTML とブラウザで描き直した結果が食い違います。座標から決めれば両方が同じになり、開くたびに欠け方が変わることもありません。

const hashOf = ({ x, y }: FrikoPixel) => {
  const seed = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
  return seed - Math.floor(seed);
};

目だけは塗りのまま残し、5 秒に 1 回まばたきさせています。頭の横に「?」を浮かべて、全体を 4 秒の周期で上下に漂わせ、迷子らしくしました。

動きは SVG の <animate> で付けています。CSS に @keyframes を書かなくても、コンポーネントの中だけで動きを書けるためです。ただし、<animate> は動きを減らす設定(prefers-reduced-motion)を見ません。そこでブラウザの設定を調べ、オンのときは <animate> を描かないようにしました。設定がわかるまでのあいだ(サーバーで描くときなど)は、止まった絵を出しています。

エラーの画面は泣き顔の Friko

表示の途中でエラーが起きたときは、目を「TT」(泣いている顔文字)にした泣き顔の Friko を出します。頭の横の印は「!」にしました。

エラーの画面。上に「TT」の目をした輪郭だけの Friko と、その右にアクセントの色の「!」。下に「500 · ERROR」のラベル、「問題が起きました」の見出し、説明、もう一度試すボタンとホームに戻るボタン
表示の途中でエラーが起きたときの画面

「TT」は、左右の目それぞれの位置に「T」を 1 つずつ置いて描いています。「T」は、もとの目のドットがある列を中心にした横棒 3 ドットと、そこから下へ伸びる縦棒 2 ドットです。目のまわりの 3×3 のマスには体のドットを描かず、目が中抜きの四角に埋もれないようにしました。

エラーの画面の Friko を拡大したもの。目が塗りのドットで描いた「TT」になっていて、そのまわりだけ体のドットがない。頭の右にアクセントの色の「!」
泣き顔の Friko

最初は目を「><」にしていましたが、「TT」に変えました。頭の横の印も、最初は汗のしずくでした。ところが汗のしずくは、数ドットの大きさでは炎に見えてしまいました。そこで、404 の「?」と対になるよう「!」に変えています。

エラーの画面の Friko は、404 のように漂わずにじっとしていて、3 秒に 1 回ぶるっと震えます。

エラーの画面は 2 つある

Next.js では、エラーの画面を 2 つ置けます。

  • app/error.tsx: ページの中で起きたエラーを受け止める。すべてのページを包むレイアウト(ルートのレイアウト)の内側に出るので、翻訳やテーマはふだんどおり使える
  • app/global-error.tsx: ルートのレイアウトの中でエラーが起き、レイアウトごと表示できなくなったときに出る。レイアウトの代わりに描くので、<html> と <body> を自分で置く

Next.js 16.3 からは、どちらにも retry が渡されます。そのページのデータを取り直して描き直す関数で、「もう一度試す」のボタンはこれを呼んでいます。

global-error.tsx は、翻訳(next-intl)とテーマ(next-themes)のプロバイダーの外で描かれます。そこで、文言・テーマ・アクセントの色を global-error.tsx の中で自分で決めるようにしました。

  • 文言: 2 つのエラーの画面の文言を、アプリ全体の翻訳ファイルから別の JSON に分け、global-error.tsx はその JSON を直接 import する。言語は、ユーザーがアプリで選んだ言語の Cookie で決め、なければブラウザの言語で決める
  • テーマ: next-themes が localStorage に覚えているテーマ(ライトかダークか)を取り出し、<html> の data-theme に入れる
  • アクセントの色: ユーザーが選んだ色の Cookie を見て、data-accent に入れる

文言を別の JSON にしたのは、アプリ全体の翻訳ファイルを global-error.tsx で読み込むと、エラーの画面のためだけに大きなファイルを読み込むことになるためです。なお、プロバイダーの内側で描く error.tsx では、この JSON もほかの翻訳と一緒に読み込んで使っています。

エラーが起きると、error.tsx と global-error.tsx は、エラーの内容をログのサービスに送ります。

404 と、error.tsx・global-error.tsx の 3 つの画面は、同じ部品で作っています。違うのは、Friko の表情と文言、いちばん目立つボタンがホームへのリンクか「もう一度試す」かだけです。

この記事を共有

X でポストはてなブックマーク