Next.js + Formik で作るアクセシブルな問い合わせフォームを考えてみる

企業の Web サイトなどではほぼ確実に使用するお問い合わせフォームを制作する際に、Web アクセシビリティを確保しつつ実装する方法について考えてみようというお話。今回は JavaScript フレームワークとして、Next.js と React 向けのフォーム作成ライブラリ Formik を使用します。

この記事は 「Webアクセシビリティ Advent Calendar 2020」、2日目の記事です。

ここ最近、お仕事でもプライベートでも React、というか Next.js を扱うことが多くて、昨日も、「Movable Type Advent Calendar 2020」 向けの記事で、Next.js に関連したネタを使って書いたんですが、今回もその流れで。

JavaScript フレームワークは数多く、用途や好みなどによって何を使うかは分かれると思いますし、日本だと Vue.js や、それに関連して Nuxt.js の方が話題に挙がることは多いように感じますが、個人的には Next.js が扱いやすくて気に入っています。

で、この記事は 「Next.js Advent Calendar」 じゃなくて、「Webアクセシビリティ Advent Calendar」 向けの記事ですので、当然、Web アクセシビリティを主題に書かないといけません。そこで、今回は企業の Web サイトなどによくある簡単なお問い合わせフォームを Next.js を用いて制作するにあたって、どういう風に実装すればアクセシブルなフォームにできるかなというのをちょっと考えてみたいと思います。

目次

少し長いので最初に目次を。本題は 「基本的なフォームの作成」 セクションからですので、それ以外は興味なければ読み飛ばしていただいてよろしいかと。

前提

今回のサンプルで使用した環境は下記の通り。

  • Node.js v14.15.1 LTS
  • React 17.0.1
  • Next 10.0.3
  • Formik 2.2.6
  • Yup 0.32.8

フォームの作成には、React 向けのフォーム作成ライブラリ 「Formik」 を利用しています。また、フォーム入力項目のバリデーションには、検証用スキーマを簡単に作成できて便利、という理由で 「Yup」 を利用してます。React でフォームというときは、個人的にこの組み合わせをよく使います。

なお、本記事は、タイトルでも 「考えてみる」 と書いているように、アクセシブルなフォーム作成のベストプラクティスを提示するものではありません。あくまでこういう風に実装したらある程度のアクセシビリティを確保できるんじゃないかなという、ひとつの実装例として書いていますので、その点はあらかじめご了承ください。

動作サンプル

今回作ってみたフォームは、そんなに複雑なフォームではなく、冒頭にも書いたとおり、企業のコーポレートサイトなどでありがちな簡単なお問い合わせフォームをサンプルにしてみました。とはいえ、さすがにテキスト入力欄だけだと簡単すぎるので、わざとラジオボタンとチェックボックスも追加しています。

CodeSandbox に動作するサンプルを作っておきましたので、実際のソースコードや動作はそちらで確認してください。

また、サンプルのソースコードは GitHub にも置いてあります。

とりあえず手元で動かしてみたいという方は、リポジトリからソースコードを git clone → 必要なパッケージをインストール (npm i) → npm run dev ですぐに動作確認できます。便利な世の中になりましたね。

必要なパッケージのインストール

まず、必要な環境を作っていきます。Node.js は導入済みという前提ですが、まずは作業用ディレクトリを作成するため、適当な場所で npx create-next-app します。

npx create-next-app formik-sample

formik-sample ディレクトリに移動します。本記事内では、このディレクトリをルートディレクトリとして扱います。

cd formik-sample

そうしたら、必要なパッケージをインストールしていきましょう。

npm install formik Yup --save

とりあえず、formikYup が入れば問題ないです。

あと、これは必須ではありませんが、今回、エラーメッセージ内でアイコンが使いたかったため、Font Awesome を導入します。

npm install --save @fortawesome/fontawesome-svg-core
npm install --save @fortawesome/free-solid-svg-icons
npm install --save @fortawesome/react-fontawesome

これで、必要なパッケージはインストール完了しました。

ディレクトリ構成

ここまでの時点で、formik-sample ディレクトリ内は下記のような構成になっていると思います。

formik-sample
├ .next/
├ node_modules/
├ styles/
│ ├ globals.css
│ └ Home.module.css
├ pages/
│ ├ api/
│ │ └ hello.js
│ ├ _app.js
│ └ index.js
├ public/
│ ├ favicon.ico
│ └ vercel.svg
├ .gitignore
├ package-lock.json
├ package.json
└ README.md

このうち、今回のサンプルでは、下記のファイルは使用しないので削除してしまっても大丈夫です。

  • /styles/Home.module.css
  • /pages/api/hello.js
  • /public/vercel.svg

下準備

フォームを作る前に、ちょっとした下準備を。

今回はフォーム 1ページだけのサンプルなので簡単にやりたいんですが、フォーム部分のソースコードをなるべくシンプルにするため、ページのレイアウト部分は別のコンポーネントに分けます。

ルートディレクトリ直下に components ディレクトリを作成し、Layout.js というファイルを新規作成して、下記のソースコードを入力し保存します。レイアウトはサンプルなので適当です。

import Head from 'next/head';
import Link from 'next/link';

import styles from '../styles/layout.module.css';

const Layout = (props) => {

    const { title, description, children } = props
    const siteTitle = 'サンプル株式会社'

    return (
        <div className={styles.container}>
            <Head>
                <title>{title ? `${title} - ${siteTitle}` : siteTitle}</title>
                <meta name="description" content={description} />
            </Head>

            <header className={styles.header}>
                <h1 className={styles.siteTitle}>
                    <Link href="/">{siteTitle}</Link>
                </h1>
            </header>

            <main>
                <div className={styles.main}>
                    {children}
                </div>
            </main>

            <footer>
                <div className={styles.footer}>
                    <div className={styles.copyright}>
                        <p><small>Sample Inc.</small></p>
                    </div>
                </div>
            </footer>

        </div>
    )
}

export default Layout

読み込んでいる layout.module.css は下記の通り。これも最低限の見た目を作るためなので、超適当です。

.container {
  background-color: #f4f7f6;
}

.header {
  background-color: #fff;
  padding: 1rem 2%;
}

.siteTitle {
  font-size: 1rem;
  margin: 0;
  padding: 0;
}

.siteTitle a {
  display: inline-block;
}

.main {
  padding: 2rem 2%;
}

.footer {
  background-color: #111;
  color: #fff;
  padding: 2rem 2%;
}

.copyright {
  color: rgba(255, 255, 255, 0.8);
  text-align: center;
}

.copyright small {
  font-size: 80%;
}

日本語対応

Next.js で生成される HTML は、そのままだと html 要素に lang="en" が指定されてしまいます。今回作成するのは日本語サイトという前提ですので、その辺を設定します。

pages ディレクトリ内に _document.js ファイルを新規作成し、下記のようにソースコードを記述して保存しましょう。

import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
    render() {
        return (
            <Html lang="ja" dir="ltr">
                <Head />
                <body>
                    <Main />
                    <NextScript />
                </body>
            </Html>
        )
    }
}
export default MyDocument

これで下準備は完了です。

基本的なフォームの作成

それでは本題のフォームに入ります。まずは基本的なフォームを作っていきましょう。pages ディレクトリ内に index.js ファイルがありますが、今回はこのページをフォームに書き換えます。

ソースコード全体を書いていくと長くなるので、一部分ずつ抜粋して行きます。また、この記事は Formik や Yup の使い方を解説するのが主目的ではないため、その辺の説明は端折ります。

テキスト入力欄の実装例

まず一番わかりやすい普通の入力欄。入力必須という前提です。

<div className={styles.formField}>
    <div className={styles.formFieldName}>
        <label htmlFor="company">
            御社名
            <span className={styles.formInputRequisite}>必須</span>
            <ErrorMessage name="company">
                {msg => <span className={styles.invalidForm} aria-live="polite"><FontAwesomeIcon icon={faExclamationTriangle} />{msg}</span>}
            </ErrorMessage>
        </label>
    </div>
    <div className={styles.formFieldInput}>
        <Field
            name="company"
            id="company"
            type="text"
            placeholder="会社名や団体名をご記入ください"
            aria-required="true"
            aria-invalid={errors.company ? 'true' : 'false'}
        />
    </div>
</div>

Formik の Field を使用することで、入力コントロールを簡単に作成する事ができます。ここでは普通のテキスト入力欄を作りたいので、下記のように、type="text" を指定しています。

<Field
    name="company"
    id="company"
    type="text"
    placeholder="会社名や団体名をご記入ください"
    aria-required="true"
    aria-invalid={errors.company ? 'true' : 'false'}
/>

アクセシビリティに関連するポイントとしては次の通り。

  • 入力必須の入力コントロールに関しては、HTML の required 属性を使用することでブラウザ側にバリデーションを任せることができますが、ブラウザごとのエラーメッセージの表示差異などを考慮して今回は使用しません。そのかわり、入力が必須であることを aria-required="true" によって支援技術に伝えます。
  • バリデーションの結果、入力エラーがあることがわかった場合は、aria-invalidtrue にすることで支援技術に伝えます。

次にこの入力コントロールに対するラベルですが、下記のように実装します。

<div className={styles.formFieldName}>
    <label htmlFor="company">
        御社名
        <span className={styles.formInputRequisite}>必須</span>
        <ErrorMessage name="company">
            {msg => <span className={styles.invalidForm} aria-live="polite"><FontAwesomeIcon icon={faExclamationTriangle} />{msg}</span>}
        </ErrorMessage>
    </label>
</div>

アクセシビリティに関連するポイントとしては次の通り。

  • まず、label 要素を適切に使用しましょうというのは基本のキ。for 属性 (React で書いているため htmlFor ですが) を使用して、上で書いた入力コントロールと紐付けています。
  • 必須項目だとわかる表示も、label 要素内に含めます。
  • エラーメッセージ表示部分は Formik の ErrorMessage を使用しますが、エラーメッセージも label 要素内に含めることで、エラーメッセージ表示時に入力コントロールのラベルとして支援技術に伝わるようにします。
  • エラーメッセージには aria-live="polite" を附与することで、エラーメッセージが表示されていることを (控えめに) 支援技術に伝えます。
  • エラーメッセージにはアイコンを合わせて表示していますが、これは視覚的にエラーがあることをよりわかりやすくするためです。
  • ちなみに、エラー表示を赤の文字色、もしくは背景色で表示するケースが多いと思います。赤背景に白文字 (その逆もですが) みたいな組み合わせはコントラスト比が不足するケースが多いので要チェックです。

ラジオボタンやチェックボックスの実装例

ラジオボタン部分の実装も見てみましょう (チェックボックスの部分も基本的には同じです)。

<div className={styles.formField}>
    <fieldset aria-required="true" aria-invalid={errors.inquiryType ? 'true' : 'false'}>
        <legend className={styles.formFieldName} id="labelInquiryType">
            お問い合わせ種別
            <span className={styles.formInputRequisite}>必須</span>
            <ErrorMessage name="inquiryType">
                {msg => <span className={styles.invalidForm} aria-live="polite"><FontAwesomeIcon icon={faExclamationTriangle} />{msg}</span>}
            </ErrorMessage>
        </legend>
        <div className={styles.formFieldInput}>
            <ul role="radiogroup" aria-labelledby="labelInquiryType">
                <li><Field name="inquiryType" id="inquiryType01" type="radio" value="見積もり依頼" /><label htmlFor="inquiryType01">見積もり依頼</label></li>
                <li><Field name="inquiryType" id="inquiryType02" type="radio" value="採用に関するお問い合わせ" /><label htmlFor="inquiryType02">試用版申込み</label></li>
                <li><Field name="inquiryType" id="inquiryType03" type="radio" value="その他" /><label htmlFor="inquiryType03">その他</label></li>
            </ul>
        </div>
    </fieldset>
</div>

アクセシビリティに関連するポイントとしては次の通り。

  • 各ラジオボタンと、対応するラベルの関連付けは label 要素で。これは当たり前の話。
  • ラジオボタンやチェックボックスなど、複数の入力コントロールがまとまってひとつの入力グループを構成する場合に、どういう風にラベルを付けるかという点ですが、今回は fieldset 要素と legend 要素を使用しました。
  • fieldset 要素に対して aria-required="true" として、この入力グループが必須項目であることを支援技術に伝えます。
  • ラジオボタンを ul 要素でリストとしてマークアップし、ul 要素には role="radiogroup"aria-labelledby="labelInquiryType" を附与します。
  • legend 要素に id 属性を附与して、ラジオボタンが含まれる ul をラベリングします。
  • バリデーションの結果、入力エラー (未選択) があることがわかった場合は、fieldset 要素に対して aria-invalidtrue にすることで支援技術に伝えます。
  • エラーメッセージは legend 要素内に含めることで、エラーメッセージ表示時に入力コントロールグループのラベルとして支援技術に伝わるようにします。

検証部分の実装例 (Yup)

入力内容の検証部分ですが、下記のように実装しています。Yup を使用しているのでメソッドチェーンで直感的に記述できます。

validationSchema={Yup.object({
    inquiryType: Yup.string()
        .required('お問い合わせ種別を選択してください'),
    service: Yup.array()
        .min(1, '検討中のサービスを1つ以上選択してください'),
    name: Yup.string()
        .required('ご担当者名は必須です'),
    company: Yup.string()
        .required('御社名は必須です'),
    email: Yup.string()
        .email('メールアドレスの形式に誤りがあります')
        .required('メールアドレスは必須です'),
    content: Yup.string()
        .required('お問い合わせ内容は必須です'),
})}

実装というより文言選択の話になりますが、アクセシビリティに関連するポイントとしては次の通り。

  • 読んだときに、どのようなエラーが出ていて、どう修正すればよいのかがわかりやすいエラーメッセージになるように考慮しましょう。
  • 例えば、「○文字以上、△文字以内」 で入力して欲しい入力欄があった場合など、その範囲に収まらなかった場合のエラーメッセージは、単に 「入力方式が正しくありません」 とするのではなく 「○文字以上入力してください」、「△文字を越えています」 といった感じでより具体的にするとよいと思います。

ここまでの実装で、未入力など、エラーがある状態のまま、別の入力コントロールにフォーカスを移動したり、送信ボタンを押すと、エラーメッセージが表示されます。Formik と Yup を使用することで、バリデーション含め、とても非常に簡単に書くことができて楽ですね。

追加の実装

もうひと工夫ということで、バリデーションによってエラーが発生した場合に、ページのタイトル (title 要素) でもエラーがあることを伝えてみます。

フォーム内では下記の部分で Props を受け取っていますので、

{({ isSubmitting, isValid, errors }) =>
...
}

これを利用して、isValidfalse の時は、Head コンポーネントでページタイトルにエラーの件数を表示してあげます。

{!isValid && (
    <Head>
        <title>{Object.keys(errors).length}箇所の入力エラーがあります - {title}</title>
    </Head>
)}

個々のエラーに関しては、各入力コントロールと紐付く形で表示していますので、ページタイトルでは 「○件のエラーがありますよ」 というのを知らせる感じにしています。

ところでこの部分の実装、useEffect を使用した方がいいかなとも思ったんですがどうなんでしょう。


ということで、今回は、Formik を使用した問い合わせフォームの実装時に、こんな感じで実装するとアクセシビリティを確保することができるんじゃないでしょうか、というひとつの例を紹介してみました。

今回は JavaScript フレームワークを使用した例ですが、フレームワークを使用せず、HTML と JavaScript で実装する場合も概ねアプローチとしては同様の感じになるかと思います。何かしら実装時の参考になれば幸いです。

さて、今回参加させて頂いた 「Webアクセシビリティ Advent Calendar 2020」 ですが、もうひと枠、4日目の枠もいただいていますので、会社のサイトの方のコラムでもう 1記事書くと思います。そっちはもう少し簡単、というかソースコードをあまり書かなくてよい内容にします。他の方の記事も色々と参考になるものばかりだと思いますので、ぜひチェックしてみてください。

記事をここまで御覧頂きありがとうございます。
この記事が気に入ったらサポートしてみませんか?