Vue3 で作る お問合せステップフォーム

|対象となる方

  • さくっとお問い合わせフォームを作りたい方
  • HTML/CSS/JavaScript を多少書いたことがある方
  • Vue.jsの学習者(初学者の方でもなんとなく理解はできるかと思います…)
  • Vue 3 の Composition API について軽く知りたい方

|概要

  • 複数の項目(10問程度を想定)を扱うお問い合わせフォームを作って欲しいという要望を受けまして、なるべく問い合わせる方が使いやすいフォームを作ってみようと思いページ遷移の発生しない、ステップフォームの構築を目指してみました。
  • 質問の回答内容の管理を行う際に、Vue.js側で情報を持ちながらフォームの進めていくようにし、いわゆる SPA(Single Page Application) での構築をしました。
  • 質問項目には回答必須の項目と任意の項目の2種類設定し、必須項目については回答していない場合は次の項目に進めないようにコントロールしてあります。
  • 入力内容のバリデーション機能も構築しました。主には必須のチェックとメールアドレスチェックです。

|環境構築

私がご依頼を受けて構築した際は、最終的にレンタルサーバー上へのデプロイを行いましたが、今回はサンプル的に作って行きますので、皆さんのローカルのPC上で動くように環境構築をやって行きたいと思います。
とはいえ、構築する上で必要なものは Node.js のみですので、「mac node インストール」「windows node インストール」などで検索して、使える環境を作っておいてください。(※使える状態とは… node --versionnpm -vなどと打ってバージョン情報が出る状態と捉えて良いかと思います。)

それでは、Vue.js のアプリの作成作業をやって行きましょう!ターミナルを開いて、cdコマンドを使って作成したい場所に移動してください。(私はデスクトップに構築しましたので cd Desktop でデスクトップに移動してます)
移動先で以下のコマンドを叩いてください。(叩くとは… どうやら業界用語のようです^^; コマンドを入力してEnterを押すです。)

npm install -g @vue/cli

 何をしたかと言うと、Vue-CLIのプロジェクトがいつでも作成できるようにローカルPCに用意するコマンドです。(今後プロジェクト作成時に叩く必要はありません)
 次に以下のコマンドでアプリの作成をします。アプリ名に関しては何でも良いのですが、今回は「contact-form」という名前にしますね。

vue create contact-form

何やら選択肢が現れて選ぶように促されます。Vue 3 を選択しEnterを押してください。

Vue CLI v4.5.12
? Please pick a preset: 
  Default ([Vue 2] babel, eslint) 
❯ Default (Vue 3 Preview) ([Vue 3] babel, eslint) 
  Manually select features 

するとインストールが始まり、最終的に

🎉  Successfully created project frontend.
👉  Get started with the following commands:

 $ cd contact-form
 $ npm run serve

と表示されればOKです!きちんとインストールできているか確認してみましょう!
cd contact-form で contact-form ディレクトリに移動して npm run serve でプロジェクトを起動してみてください。

 DONE  Compiled successfully in 2554ms

  App running at:
  - Local:   http://localhost:8080/ 
  - Network: http://192.168.1.5:8080/

  Note that the development build is not optimized.
  To create a production build, run npm run build.

などと表示されると思いますので http://localhost:8080/ にアクセスしてみてください!Vue.js の Welcome ページが表示されていれば環境構築は完了です!

|さっそく開発してみよう!

簡単な修正から

ではコードを書いていきましょう! contact-form ディレクトリ内は以下のようになっているかと思います。

/node_modules
/public
  |- favicon.ico
  |- index.html
/src
  |- /assets
  |- /components
       |- HelloWorld.vue
  |- App.vue
  |- main.js
.gitignore
babel.config.js
package-lock.json
package.json
README.md

App.vue ファイルを開いて以下のように編集してください!

<template>
  <Header />
  <Contact />
  <Footer />
</template>

<script>
import Header from './components/Header'
import Contact from './components/Contact'
import Footer from './components/Footer'

export default {
  name: 'App',
  components: {
    Header,
    Contact,
    Footer,
  },
}
</script>

<style>
</style>

次に components ディレクトリ内の HelloWorld.vue ファイルを削除して、新たに Header.vueContact.vueFooter.vueの3ファイルを作成して以下のようにそれぞれ編集してください。

<template>
  <header>
    <div class="container">
      <h1><img src="./../assets/logo.png" alt="logo"></h1>
    </div>
  </header>
</template>

<script>
export default {
  name: 'Header',
}
</script>

<style scoped></style>
<template>
  <p>Contact コンポーネント</p>
</template>

<script>
export default {
  name: "Contact",
}
</script>

<style scoped></style>
<template>
  <footer id="footer" class="mt20">
    <div class="container">
        <p>2021 株式会社◯◯</p>
    </div>
  </footer>
</template>

<script>
export default {
  name: 'Footer',
}
</script>

<style scoped></style>

ブラウザを確認してみましょう!以下のような画面になっていればOKです!

何の説明もなしに、4つのファイルを編集してしまったのですが、Vue.js はファイルのソースコードをご覧頂くとわかるかと思うのですが、3つのセクションで構成されています。HTMLを記述する<template>、JavaScriptを記述する<script>、CSSを記述する<style>の3つです。それぞれに、必要な記述をすれば必要なページやコンポーネントを作成することができますのでとても理解しやすいかと思います。このような構造を単一ファイルコンポーネント(Single File Component)と呼んでいるようです。詳しくは 公式サイト(単一ファイルコンポーネント) をご覧ください。

がっつりステップフォームの作成!

それではステップフォームの作成に移りましょう! と言っても完成版を載っけます!
フォームは全部で4項目で構成することとし、送信完了画面も含めて5ページの構成とします!
先程作成した Contact.vue を修正してください。一旦、デザインはあてないので不恰好だと思いますが、最終的には整えるので、ご容赦くださいね。
<template>部分を一気に書いてしまいます。以下のようにしてください

<template>
  <div class="form-section">
    <div class="container">
      <!-- 現在ページの表示 -->
      <div class="current-num">
        <div class="num-circle" :class="checkCurrentPage(1)">
          {{ checkCurrentPage(1)['passed-page'] ? "✔︎" : 1 }}
        </div>
        <div class="num-circle" :class="checkCurrentPage(2)">
          {{ checkCurrentPage(2)['passed-page'] ? "✔︎" : 2 }}
        </div>
        <div class="num-circle" :class="checkCurrentPage(3)">
          {{ checkCurrentPage(3)['passed-page'] ? "✔︎" : 3 }}
        </div>
        <div class="num-circle" :class="checkCurrentPage(4)">
          {{ checkCurrentPage(4)['passed-page'] ? "✔︎" : 4 }}
        </div>
      </div>
      <!-- 1ページ目のコンテンツの表示 -->
      <div class="contact-form" v-show="currentPage === 1">
        <div class="select">
          <h3 class="contact-title">
            ご相談内容をお選びください
            <span class="optional">任意</span>
          </h3>
          <p class="mt20"><span class="t12"><i class="fas fa-asterisk tblue fa-fw"></i>複数選択可能</span></p>
          <div class="selects sepa2">
            <label for="select-1" class="select-button" :class="selected(selectBox.homepage)">サイト制作</label>
            <input id="select-1" type="checkbox" name='select-1' v-model="selectBox.homepage">
            <label for="select-2" class="select-button" :class="selected(selectBox.system)">システム開発</label>
            <input id="select-2" type="checkbox" name='select-2' v-model="selectBox.system">
            <label for="select-3" class="select-button" :class="selected(selectBox.consul)">WEBコンサル</label>
            <input id="select-3" type="checkbox" name='select-3' v-model="selectBox.consul">
          </div>
        </div>
        <div class="btn_wrap">
          <button class="b-next" @click="nextCurrentPage()">次へ</button>
        </div>
      </div>
      <!-- 2ページ目のコンテンツの表示 -->
      <div class="contact-form" v-show="currentPage === 2">
        <div class="select">
          <h3 class="contact-title">
              ご相談したい内容を以下にご記入ください
              <span class="optional">任意</span>
          </h3>
          <p class="t12 mt20">ご記入内容はお打ち合わせの際に参考にさせていただきます。</p>
          <textarea class='mt20' cols="100" rows="8" maxlength="1000" name='contactDetail' v-model="contactDetail" placeholder="ホームページを新しく作成したなど。ご自由にご記入ください"></textarea>
        </div>
        <div class="btn_wrap">
          <button class="b-back" @click="prevCurrentPage()">戻る</button>
          <button class="b-next" @click="nextCurrentPage()">次へ</button>
        </div>
      </div>
      <!-- 3ページ目のコンテンツの表示 -->
      <div class="contact-form" v-show="currentPage === 3">
      <div class="select">
        <h3 class="contact-title">
            相談されたい希望日をご記入ください
            <span class="optional">任意</span>
        </h3>
        <p class="t12 mt20"><i class="fas fa-asterisk tblue fa-fw"></i>いつでも良い方は下記の「次へ」を押してください</p>
        <h4>曜日から選ぶ(複数回選択可)</h4>
        <div class="selects sepa3">
          <label for="week-select-1" class="select-button" :class="weekSelected(selectWeek.monday)">月曜</label>
          <input id="week-select-1" type="checkbox" name='week-select-1' v-model="selectWeek.monday">
          <label for="week-select-2" class="select-button" :class="weekSelected(selectWeek.tuesday)">火曜</label>
          <input id="week-select-2" type="checkbox" name='week-select-2' v-model="selectWeek.tuesday">
          <label for="week-select-3" class="select-button" :class="weekSelected(selectWeek.wednesday)">水曜</label>
          <input id="week-select-3" type="checkbox" name='week-select-3' v-model="selectWeek.wednesday">
          <label for="week-select-4" class="select-button" :class="weekSelected(selectWeek.thursday)">木曜</label>
          <input id="week-select-4" type="checkbox" name='week-select-4' v-model="selectWeek.thursday">
          <label for="week-select-5" class="select-button" :class="weekSelected(selectWeek.friday)">金曜</label>
          <input id="week-select-5" type="checkbox" name='week-select-5' v-model="selectWeek.friday">
        </div>

        <div class=" select-timezone">
            <h4>時間帯から選ぶ(いずれか1つを選択)</h4>
            <div class="selects sepa3">
              <input id="timezone-select-1" type="radio" name='timezone-select-1' v-model="timezone" value="午前">
              <label for="timezone-select-1" class="select-button" :class="timezoneSelected('午前')">
                  午前
              </label>
              <input id="timezone-select-2" type="radio" name='timezone-select-2' v-model="timezone" value="午後">
              <label for="timezone-select-2" class="select-button" :class="timezoneSelected('午後')">
                  午後
              </label>
              <input id="timezone-select-3" type="radio" name='timezone-select-3' v-model="timezone" value="いつでも">
              <label for="timezone-select-3" class="select-button" :class="timezoneSelected('いつでも')">
                  いつでも
              </label>
            </div>
        </div>
        <div class="select-day mt20">
            <h4>希望日程を選ぶ</h4>
            <input class='mt10 pointer' type="date" name='date' v-model="date">
        </div>
      </div>
        <div class="btn_wrap">
          <button class="b-back" @click="prevCurrentPage()">戻る</button>
          <button class="b-next" @click="nextCurrentPage()">次へ</button>
        </div>
      </div>
      <!-- 4ページ目のコンテンツの表示 -->
      <div class="contact-form" v-show="currentPage === 4">
      <div class="select">
        <h3 class="contact-title">
            お名前とメールアドレスを教えて下さい<span class="required">必須</span>
        </h3>

        <section class="mt20 w80">
            <!-- 名前 -->
            <p class=""><i class="fas fa-caret-right fa-fw"></i>名前</p>
            <div data-field="名前" data-validate="required">
              <input type="text" autocomplete="name" name='name' class="w100" v-model="customer.name" />
            </div>

            <!-- メールアドレス -->
            <p class=""><i class="fas fa-caret-right fa-fw"></i>メールアドレス</p>
            <div data-field="メールアドレス" data-validate="email">
                <input type="email" inputmode="email" name='email' autocapitalize="off" class="w100" v-model="customer.mail" />
            </div>
        </section>
      </div>

      <div class='error_msg' v-show="error_name">【お名前】必須項目です。入力内容をご確認ください。</div>
      <div class='error_msg' v-show="error_mail_require">【メールアドレス】必須項目です。入力内容をご確認ください。</div>
      <div class='error_msg' v-show="error_mail_format">【メールアドレス】フォーマットが不正です。入力内容をご確認ください。</div>

      <div class="btn_wrap">
        <button class="b-back" @click="prevCurrentPage()"><span><i class="fas fa-chevron-left"></i></span>戻る</button>
        <div v-if="disabledNext" class="b-disable" @click="displayError()">次へ<span><i class="fas fa-chevron-right"></i></span></div>
        <button v-else class="b-next" @click="nextCurrentPage()">次へ<span><i class="fas fa-chevron-right"></i></span></button>
      </div>
      </div>
      <!-- 送信完了ページの表示 -->
      <div class="contact-form"  v-show="currentPage === 5">
        <p>送信しました</p>
      </div>
    </div>
  </div>
</template>

細々と説明しないといけないことがあるかと思いますが、<script> 部分を書き終えてからにしましょう!では <script> 部分を以下のようにしてみてください。

<script>
import { ref, reactive, watch } from 'vue'
export default {
  setup () {
    // 現在のページ管理
    let currentPage = ref(1)
    const checkCurrentPage = number => {
      return {
        'current-page': currentPage.value === number,
        'passed-page': currentPage.value > number
      }
    }
    const nextCurrentPage = () => currentPage.value++
    const prevCurrentPage = () => currentPage.value--

    /* 1ページ目のデータ管理 */
    let selectBox = reactive({
      homepage: false,
      system: false,
      consul: false,
    })
    const selected = selected => {
      return { 'selected': selected }
    }

    /* 2ページ目のデータ管理 */
    let contactDetail = ref(null)

    /* 3ページ目のデータ管理 */
    let selectWeek = reactive({
        sunday: false,
        monday: false,
        tuesday: false,
        wednesday: false,
        thursday: false,
        friday: false,
    })
    let timezone = ref('いつでも')
    let date = ref(null)
    const weekSelected = radioValue => {
      return { 'selected': radioValue }
    }
    const timezoneSelected = radioValue => {
      return { 'selected': timezone.value === radioValue }
    }

    /* 4ページ目のデータ管理 */
    let customer = reactive({
        name: '',
        mail: '',
    })
    let disabledNext = ref(true)
    let error_name = ref(false)
    let error_mail_require = ref(false)
    let error_mail_format = ref(false)
    const checkDisabledNext = () => {
      // 入力チェックを行い、次へボタンを切り替える
      if (customer.name === '' || customer.mail === ''
          || !checkEmail(customer.mail)) {
        disabledNext.value = true
      } else {
        disabledNext.value = false
      }
    }
    const checkEmail = mail => {
      // 正規表現を使って xxx@xxx.xxxx のようなフォーマットになっているかをチェック
      const emailReg = /^[A-Za-z0-9]{1}[A-Za-z0-9_.-]*@{1}[A-Za-z0-9_.-]{1,}\.[A-Za-z0-9]{1,}$/
      return emailReg.test(mail)
    }
    const displayError = () => {
      if (customer.name === '') {
        error_name.value = true
      }
      if (customer.mail === '') {
        error_mail_require.value = true
      } else if (!checkEmail(customer.mail)) {
        error_mail_format.value = true
      }
    }
    watch(
      () => customer,
      () => {
        checkDisabledNext()
        error_name.value = false
        error_mail_require.value = false
        error_mail_format.value = false
      },
      { deep: true }
    )

    return {
      currentPage, checkCurrentPage, nextCurrentPage, prevCurrentPage,
      selectBox, selected, contactDetail, selectWeek, timezone, date,
      weekSelected, timezoneSelected, customer, disabledNext, checkDisabledNext,
      checkEmail, error_name, error_mail_require, error_mail_format, displayError
    }
  }
}
</script>

画面を確認してみてください! 次へボタンや戻るボタンで表示内容が切り替わる様子が見れると思います。
ステップの構築自体はこれで完成となるので、少し解説をしていこうと思います。

【template側の説明】

まずは<template>側から説明していきます。template は 概ね HTML 文書ですので、HTMLの説明は省いて、Vue に関係のある部分のみ説明しますね!ポイントは以下の5つです。

  1. v-show, v-if による表示の切り替え
  2. @click によるイベントの追加
  3. :class によるクラススタイルのバインディング
  4. v-model で入力内容を紐付ける
  5. {{ }} でコンテンツ表示

v-showv-ifについて説明します。該当のコードをピックアップすると、例えばv-show="currentPage === 1" という書き方がされていると思います。シンプルに捉えて頂くと、「currentPage が 1 の時にshow(表示)しちゃうよ!」という意味です。v-if も画面上では表示されたりされなかったりという面では同じです。ではv-ifと何が違うのかというと、非表示される時の方法が異なります。v-show の方は、条件を満たさない場合は CSSのスタイルとして display: none; を付与し、画面上で見えないようにします。一方v-if の方は、条件を満たさない場合、DOM自体を生成しません。CSSで消すのではなく、コード自体を書き出さないということです。割とハマりポイントなので、チェックしておいてくださいね!

@click について説明します。これは、記述されたタグの中に clickイベントを付与するというものです。イコール以降に記述されているイベントを click時に実行します。今回のコードでいうとprevCurrentPage()nextCurrentPage() といった記述があるかと思いますが、後述した <script>内の関数を呼び出しているという感じです。

:class について説明します。これは、v-bind の一種でv-bind:class の略記法になります。どのようなことをしているかというとイコール以降のオブジェクトまたは配列の真偽値によって動的にクラスを付与することができるというものになります。今回使ったものの例ですと、:class="checkCurrentPage(1)" の部分になるかと思いますが、checkCurrentPage() でやってることを言語化すると「現在ページが自分の数字と同じだったらcurrent-page というクラスを付与、現在ページが自分の数字よりも大きかったらpassed-pageというクラスを付与する」ということをやっています。現在CSSを当てていないのでイメージがしづらいかと思いますが、ページ遷移を演出するためです。検証ツールで付与されたりされなかったりする部分を確かめてみるとわかりやすいかもです。

v-model について説明します。少し専門的にいうと、v-onv-bindをまとめた操作になります。画面上で入力したり選択したりするデータをVue側でデータを持つことができるようになります。これによって後ほど説明する script 内で色んなデータの操作をすることができるようになります。

{{ }} について説明します。これはテンプレート構文と言うものの一種で、Vue が持っているデータなどをテンプレート内に出力するための記述方法になります。Vueがhoge という変数に"ホゲだよ" というテキストデータを持っている場合{{ hoge }} と記述するとブラウザ上には ホゲだよ と表示されます。変数だけでなく、今回のように JavaScript のコードを書くこともできます。今回のものでいうと{{checkCurrentPage(1)['passed-page'] ? "✔︎" : 1 }} という記述があるかと思います。これは何をしているかというと、三項演算子というものをものを用いて「checkCurrentPage(1)[‘passed-page’] が true の時は “✔︎” を表示、false の時は 1 を表示する」という操作になります。

【script側の説明】

次に<script>部分の説明をしていきます。今回は Vue.js のバージョン3を採用しているので、CompositionAPI を使いながら記述しています。今までのバージョン2で書き慣れている方は少し違う書き方をしますので、compositionAPI の部分は新鮮な気持ちで臨んでみてください!
ポイントは以下の4つです

  1. composition API の書き方
  2. watch で値を監視する
  3. (VueではなくJavaScriptですが)アローファンクションの書き方
  4. (同様にVueではないが)正規表現を使ってメールアドレスのバリデーションを行う

composition APIについて説明します。詳しく書きすぎるとそれだけで1本の記事ができそうなので避けますが、個人的な捉え方としては、記述が分散されていて、全体の可読性を落とす傾向にあったコードが、機能ごとに記述できるようになり、可読性が向上したという感じかな〜と思っています。記述方法はシンプルでsetup( ){ } の中に順番に書いていき、最終的に return で返してあげるというだけです。今回の部分でいうとref()reactive()というものが特徴的な部分かなと思います。ref() に関しては、変数の定義の仕方(例: let currentPage = ref(1))と、その変数の値を参照したり書き換えたい場合はcurrentPage.value という書き方をしないといけないという部分が大事なポイントでしょうか。ref や reactive はリアクティブな変数としてアプリ全体に安全に渡すことを可能とする関数です。(公式ドキュメントによると型による値の渡し方の差異無くし統一するためとのことです)

② watch() について説明します。これは、Vueのバージョン2の時と少しだけ書き方が変わっています。以下に示すコードの部分ですが 一つ目の引数には監視する対象の変数を指定し、二つ目で実行する関数や操作を書きます。3つ目は書かなくても良い部分ですが、1つ目で指定した変数の階層が深い場合などに、その深い部分までwatch の対象とするかどうかを指定することができます。

watch(
  () => customer,
  () => {
    checkDisabledNext()
    error_name.value = false
    error_mail_require.value = false
    error_mail_format.value = false
  },
  { deep: true }
)

③ 続いてアローファンクション について説明します。従来までの function の記述方法とは別に、JavaScript の新たな function の書き方です。挙動も多少異なる部分がありますが本記事では割愛します。一度ご自身で調べてみてください。
書き方はいたってシンプルでconst 関数名 = () => {関数の処理内容} という感じです。=> の部分が矢のように見えるからアローファンクションなんですかね…  ちなみに、()の部分には(hoge) といった具合に引数を受け取ることができます。また、引数や関数の処理が単一の場合はカッコを省略することもできて、物凄くシンプルなアローファンクションとしては次のような書き方ができます。 const myName = name => console.log(name)

④ 最後にバリデーションの部分を説明します。メールアドレスの入力内容について簡易的にではありますが、正規表現を使ってチェックをかけたいと思います。具体的には以下の関数の部分ですが、今回のチェック内容としては「先頭が半角英数字 + 0文字以上の半角英数字 or _ or . or – + @1回 + 1文字以上の半角英数字 or _ or . or – + . + 1文字以上の半角英数字」というフォーマットにしています。なんかややこしこと言うておりますが、「hoge.huga@hogehoge.jp」みたいなよくあるメールアドレスのフォーマットになっていれば通過します。

const checkEmail = mail => {
  // 正規表現を使って xxx@xxx.xxxx のようなフォーマットになっているかをチェック
  const emailReg = /^[A-Za-z0-9]{1}[A-Za-z0-9_.-]*@{1}[A-Za-z0-9_.-]{1,}\.[A-Za-z0-9]{1,}$/
  return emailReg.test(mail)
}

これで機能面は完成しました!!お疲れ様です!
最終的にデザインを反映したら終わりですので、以下のコードを <style> の中に書いてください。4ファイルあるので気をつけてください!
App.vueHeader.vueContact.vueFooter.vue

@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100;300;400;500;700;900&display=swap');
*,*::before,*::after{box-sizing:border-box}body,h1,h2,h3,h4,p,figure,blockquote,dl,dd{margin:0}ul[role="list"],ol[role="list"]{list-style:none}html:focus-within{scroll-behavior:smooth}body{min-height:100vh;text-rendering:optimizeSpeed;line-height:1.5}a:not([class]){text-decoration-skip-ink:auto}img,picture{max-width:100%;display:block}input,button,textarea,select{font:inherit}@media(prefers-reduced-motion:reduce){html:focus-within{scroll-behavior:auto}*,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important;scroll-behavior:auto !important}}button{ background-color: transparent;border: none;cursor: pointer;outline: none;padding: 0;appearance: none;}
html{
  font-size: 62.5%;
  scroll-behavior: smooth;
  font-family: 'Noto Sans JP', sans-serif;
}
.container{
  width: 100%;
  margin: auto;
  padding-left: 15px;
  padding-right: 15px;
}
h3,p{
  line-height: 1.62;
  text-align: justify;
  text-justify: inter-ideograph;
  font-feature-settings: "palt" 1;
}
h3{
  font-size: 16px;
  font-size: 1.6rem;
}
p{
  font-size: 14px;
  font-size: 1.4rem;
}
.mt10{ margin-top: 10px; }
.mt20{ margin-top: 20px; }
@media (min-width: 960px) {
  .container{
    width: 800px;
  }
}
header{
  border-bottom: 2px solid #0e271c;
}
.container h1{
  position: relative;
  text-align: center;
  height: 50px;
  line-height: 50px;
}
.container h1 img{
  display: inline;
  text-align: center;
  margin: auto;
  height: 50px;
}
.w80{
  width: 80%;
}
.w100{
  width: 100%;
}
.current-num {
  padding-top: 3rem;
  padding-bottom: 2rem;
  display: flex;
  justify-content: space-between;
}
.current-num .num-circle {
  display: inline-block;
  width: 30px;
  height: 30px;
  border: 3px solid #ddd;
  border-radius: 15px;
  background: #ffffff;
  text-align: center;
  line-height: 26px;
  color: #bbb;
  font-weight: bold;
}
.current-num .current-page {
  border: 3px solid #35495e;
  color: #35495e;
}
.current-num .passed-page {
  background-color: #41b883;
  border: 3px solid #41b883;
  color: #fff;
}
.contact-form{
  border: 2px solid #41b883;
  border-radius: 5px;
  -moz-box-shadow: 0px 0px 13px 5px rgba(0, 0, 0, 0.1);
  -webkit-box-shadow: 0px 0px 13px 5px rgba(0, 0, 0, 0.1);
  -ms-box-shadow: 0px 0px 13px 5px rgba(0, 0, 0, 0.1);
  box-shadow: 0px 0px 13px 5px rgba(0, 0, 0, 0.1);
}
.contact-form .select{
  width: 90%;
  margin: auto;
  position: relative;
}
.contact-form .select h3{
  width: 70%;
  border-left: 5px solid #41b883;
  margin: 1rem 0 0 0;
  padding: .5em 0 .5em .5em;
  color: #0e271c;
}
.contact-form .select h3 span{
  font-size: 14px;
  font-size: 1.4rem;
  font-weight: 400;
}
.contact-form .select .optional{
  position: absolute;
  top: .5em;
  right: 0;
  width: 20%;
  background: #7a7f7d;
  padding: .2em 0 .2em 0;
  border-radius: 5px;
  color: #fefefe;
  text-align: center;
}
.contact-form .select .required{
  position: absolute;
  top: .5em;
  right: 0;
  width: 20%;
  background: #b84841;
  padding: .2em 0 .2em 0;
  border-radius: 5px;
  color: #fefefe;
  text-align: center;
}

.contact-form.send{
  padding: 10px;
}
.contact-form.send p{
  text-align: center;
}

.selects{
  margin-top: 10px;
  display: grid;
  grid-template-columns: repeat(1,1fr);
  grid-gap: 8px 12px;
  position: relative;
}
.selects .select-button {
  width: 100%;
  display: inline-block;
  background-color: #fff;
  padding: .55em 0;
  border: 2px solid #ccc;
  text-align: center;
  font-size: 18px;
  cursor: pointer;
}
.selects .select-button.selected{
  color: #fff;
  background: #41b883;
}
.selects.sepa2 {
  grid-template-columns: repeat(2,1fr);
}
.selects.sepa3 {
  grid-template-columns: repeat(3,1fr);
}
.select input[type=checkbox],
.select input[type=radio] { display: none; }
.select-day input{
  text-align: center;
  display: block;
  width: 50%;
  margin-left: auto;
  margin-right: auto;
}
.select h4{
  background: #ecf8f3;
  font-size: 14px;
  font-size: 1.4rem;
  text-align: center;
  padding: 0.3rem;
}
.select section{
  margin-left: auto;
  margin-right: auto;
}
textarea{
  width: 100%;
}
.btn_wrap{
  text-align: center;
  padding-top: 1.3rem;
  padding-bottom: 1.3rem;
}
.btn_wrap .b-back{
  height: 40px;
  padding: 0 30px;
  font-size: 16px;
  font-size: 1.6rem;
  line-height: 30px;
  color: #35495e;
}
.btn_wrap .b-disable{
  display: inline;
  background: #e0e6ed;
  height: 40px;
  border-radius: 14px;
  padding: 0 30px;
  padding-top: 1.3rem;
  padding-bottom: 1.3rem;
  font-size: 16px;
  font-size: 1.6rem;
  line-height: 30px;
  color: #35495e;
}
.btn_wrap .b-next{
  background: #35495e;
  height: 40px;
  border-radius: 14px;
  padding: 0 30px;
  font-size: 16px;
  font-size: 1.6rem;
  line-height: 30px;
  color: #fff;
}
.error_msg{
  width: 80%;
  margin: 20px auto 0;
  text-align: center;
  padding: 10px 0;
  border: #b84841 solid 1px;
}
#footer{
  background: #35495e;
}
#footer p{
  padding: 10px 0;
  text-align: center;
  color: #fff;
}

以下のような画面が表示されればOKです!

まとめ

いかがでしたでしょうか。凄く簡易なものではありますが、ステップフォームを作ることができました!ご参考にしていただければ幸いです!
余談にはなりますが、私が依頼を受けたお問い合わせフォームは全部で10ページにわたり質問項目が続くものでした(汗) 中には今回扱っていないものにはなりますが、住所を入力いただく時に、郵便番号から自動で都道府県、市町村などが入力されるようなシステムも組み込んでおりますし、最終的なお問い合わせの送信部分についてもしっかりと構築してあります。(さらに話はそれてしまいますが、お問い合わせ送信後はお客様への受付メール自動送信機能やSpreadSheetへのデータ格納なども構築しました。今後記事にまとめますね^^v)
ではでは〜

よかったらシェアしてね!

この記事を書いた人

Web Developer / Educator