Skip to content

Instantly share code, notes, and snippets.

@tak-dcxi
Created December 30, 2020 19:04
Show Gist options
  • Save tak-dcxi/9c85c2617b8fdcb18c76784c6a4d2f34 to your computer and use it in GitHub Desktop.
Save tak-dcxi/9c85c2617b8fdcb18c76784c6a4d2f34 to your computer and use it in GitHub Desktop.
フォームテンプレート
<!-- autocomplete属性が設定されていない場合にname値を見てブラウザで自動入力する情報を判断しているように見受けられるため、name値は原則的にautocomplete属性で定義されている名称で設定します。 -->
<!-- @see https://developer.mozilla.org/ja/docs/Web/HTML/Attributes/autocomplete -->
<!-- pattern属性を指定する際には、title属性に書式の説明を記述することが推奨されています。 -->
<!-- @see https://html.spec.whatwg.org/multipage/input.html#the-pattern-attribute -->
<form id="contant-form" class="form" name="contact-form">
<h1 class="form__headline">お問い合わせフォーム</h1>
<div class="form__description">
<p>必要事項をご入力のうえ、確認画面へお進みください。</p>
<p><span class="form__required-label">必須</span>の項目はお手数ですが必ず入力をお願いいたします。</p>
<p>レスポンシブ対応面倒なのでまだやっていません。そのうちやります。</p>
</div>
<fieldset class="form__group">
<legend class="form__group-header">
<span class="form__group-label">お名前</span>
<strong class="form__required-label">必須</strong>
</legend>
<div class="form__group-content">
<div class="form__name">
<div class="form__text-input">
<label class="visually-hidden" for="family-name">姓</label>
<p class="text-input">
<input id="family-name" class="text-input__body" type="text" name="family-name" autocomplete="family-name" placeholder="姓" title="姓の入力は必須です" required>
<span class="text-input__validator"></span>
</p>
</div>
<div class="form__text-input">
<label class="visually-hidden" for="given-name">名</label>
<p class="text-input">
<input id="given-name" class="text-input__body" type="text" name="given-name" autocomplete="given-name" placeholder="名" title="名の入力は必須です" required>
<span class="text-input__validator"></span>
</p>
</div>
</div>
<p class="form__validation" data-validation="family-name" role="alert" aria-live="assertive"></p>
<p class="form__validation" data-validation="given-name" role="alert" aria-live="assertive"></p>
</div>
</fieldset>
<fieldset class="form__group">
<legend class="form__group-header">
<span class="form__group-label">お名前(フリガナ)</span>
<strong class="form__required-label">必須</strong>
</legend>
<div class="form__group-content">
<div class="form__name">
<div class="form__text-input">
<label class="visually-hidden" for="family-name-furigana">セイ</label>
<p class="text-input">
<input id="family-name-furigana" class="text-input__body" type="text" name="family-name-furigana" placeholder="セイ" pattern="(?=.*?[\u30A1-\u30FC])[\u30A1-\u30FC\s]*" title="姓のフリガナは全角カタカナで記入してください" required>
<span class="text-input__validator"></span>
</p>
</div>
<div class="form__text-input">
<label class="visually-hidden" for="given-name-furigana">メイ</label>
<p class="text-input">
<input id="given-name-furigana" class="text-input__body" type="text" name="given-name-furigana" placeholder="メイ" pattern="(?=.*?[\u30A1-\u30FC])[\u30A1-\u30FC\s]*" title="名のフリガナは全角カタカナで記入してください" required>
<span class="text-input__validator"></span>
</p>
</div>
</div>
<p class="form__notion">フリガナは全角カタカナで記入してください</p>
<p class="form__validation" data-validation="family-name-furigana" role="alert" aria-live="assertive"></p>
<p class="form__validation" data-validation="given-name-furigana" role="alert" aria-live="assertive"></p>
</div>
</fieldset>
<div class="form__group">
<p class="form__group-header">
<label class="form__group-label" for="tel">電話番号</label>
</p>
<div class="form__group-content">
<div class="form__text-input">
<p class="text-input">
<input id="tel" class="text-input__body -placeholder-visible" type="tel" name="tel" placeholder="(例) 090-0000-0000" autocomplete="tel" pattern="\d{2,4}-?\d{2,4}-?\d{3,4}" title="電話番号は正しく記入してください">
<span class="text-input__validator"></span>
</p>
</div>
<p class="form__notion">電話番号はハイフン(-)ありなしどちらでもご記入頂けます</p>
<p class="form__validation" data-validation="tel" role="alert" aria-live="assertive"></p>
</div>
</div>
<div class="form__group">
<p class="form__group-header">
<label class="form__group-label" for="email">メールアドレス</label>
<strong class="form__required-label">必須</strong>
</p>
<div class="form__group-content">
<div class="form__text-input">
<p class="text-input">
<input id="email" class="text-input__body" type="email" name="email" placeholder="(例) example@example.com" autocomplete="email" title="メールアドレスは正しく記入してください" required>
<span class="text-input__validator"></span>
</p>
</div>
<p class="form__validation" data-validation="email" role="alert" aria-live="assertive"></p>
</div>
</div>
<div class="form__group">
<p class="form__group-header">
<label class="form__group-label" for="postal-code">郵便番号</label>
<strong class="form__required-label">必須</strong>
</p>
<div class="form__group-content">
<div class="form__postal-code">
<div class="form__text-input -narrow">
<p class="text-input">
<input id="postal-code" class="text-input__body" type="text" name="postal-code" placeholder="000-0000" autocomplete="postal-code" pattern="\d{3}-?\d{4}" title="郵便番号はハイフンありなしの7桁で記入してください" required>
<span class="text-input__validator"></span>
</p>
</div>
<div class="form__address-search-button">
<button class="button -small js-address-search" type="button">郵便番号から住所を検索する</button>
</div>
</div>
<p class="form__notion">郵便番号はハイフン(-)ありなしどちらでもご記入頂けます</p>
<p class="form__validation" data-validation="postal-code" role="alert" aria-live="assertive"></p>
</div>
</div>
<div class="form__group">
<p class="form__group-header">
<label class="form__group-label" for="address-level1">都道府県</label>
<strong class="form__required-label">必須</strong>
</p>
<div class="form__group-content">
<div class="form__select-box">
<div class="select-box">
<select id="address-level1" class="select-box__body" name="address-level1" title="都道府県は必ず指定してください" required>
<option value="" disabled selected hidden>選択してください</option>
<option value="北海道">北海道</option>
<option value="青森県">青森県</option>
<option value="岩手県">岩手県</option>
<option value="宮城県">宮城県</option>
<option value="秋田県">秋田県</option>
<option value="山形県">山形県</option>
<option value="福島県">福島県</option>
<option value="茨城県">茨城県</option>
<option value="栃木県">栃木県</option>
<option value="群馬県">群馬県</option>
<option value="埼玉県">埼玉県</option>
<option value="千葉県">千葉県</option>
<option value="東京都">東京都</option>
<option value="神奈川県">神奈川県</option>
<option value="新潟県">新潟県</option>
<option value="富山県">富山県</option>
<option value="石川県">石川県</option>
<option value="福井県">福井県</option>
<option value="山梨県">山梨県</option>
<option value="長野県">長野県</option>
<option value="岐阜県">岐阜県</option>
<option value="静岡県">静岡県</option>
<option value="愛知県">愛知県</option>
<option value="三重県">三重県</option>
<option value="滋賀県">滋賀県</option>
<option value="京都府">京都府</option>
<option value="大阪府">大阪府</option>
<option value="兵庫県">兵庫県</option>
<option value="奈良県">奈良県</option>
<option value="和歌山県">和歌山県</option>
<option value="鳥取県">鳥取県</option>
<option value="島根県">島根県</option>
<option value="岡山県">岡山県</option>
<option value="広島県">広島県</option>
<option value="山口県">山口県</option>
<option value="徳島県">徳島県</option>
<option value="香川県">香川県</option>
<option value="愛媛県">愛媛県</option>
<option value="高知県">高知県</option>
<option value="福岡県">福岡県</option>
<option value="佐賀県">佐賀県</option>
<option value="長崎県">長崎県</option>
<option value="熊本県">熊本県</option>
<option value="大分県">大分県</option>
<option value="宮崎県">宮崎県</option>
<option value="鹿児島県">鹿児島県</option>
<option value="沖縄県">沖縄県</option>
</select>
</div>
</div>
<p class="form__validation" data-validation="address-level1" role="alert" aria-live="assertive"></p>
</div>
</div>
<div class="form__group">
<p class="form__group-header">
<label class="form__group-label" for="address-level2">以降の住所</label>
<strong class="form__required-label">必須</strong>
</p>
<div class="form__group-content">
<div class="form__text-input -wide">
<p class="text-input">
<input id="address-level2" class="text-input__body" type="text" name="address-level2" autocomplete="address-level2" title="住所の入力は必須です" required>
<span class="text-input__validator"></span>
</p>
</div>
<p class="form__validation" data-validation="address-level2" role="alert" aria-live="assertive"></p>
</div>
</div>
<fieldset class="form__group">
<legend class="form__group-header">
<span class="form__group-label">性別</span>
</legend>
<div class="form__group-content">
<!-- 性別のコードはISO 5218で定められたものに従う。 -->
<!-- @see https://ja.wikipedia.org/wiki/ISO_5218 -->
<ul class="form__group-list">
<li class="form__group-list-item">
<label class="radio">
<input class="radio__input" type="radio" name="sex" value="1">
<span class="radio__icon"></span>
<span class="radio__text">男性</span>
</label>
</li>
<li class="form__group-list-item">
<label class="radio">
<input class="radio__input" type="radio" name="sex" value="2">
<span class="radio__icon"></span>
<span class="radio__text">女性</span>
</label>
</li>
<li class="form__group-list-item">
<label class="radio">
<input class="radio__input" type="radio" name="sex" value="9">
<span class="radio__icon"></span>
<span class="radio__text">その他</span>
</label>
</li>
<li class="form__group-list-item">
<label class="radio">
<input class="radio__input" type="radio" name="sex" value="0">
<span class="radio__icon"></span>
<span class="radio__text">不明・回答しない</span>
</label>
</li>
</ul>
</div>
</fieldset>
<fieldset class="form__group">
<legend class="form__group-header">
<span class="form__group-label">当店をどこで知りましたか?</span>
</legend>
<div class="form__group-content">
<ul class="form__group-list">
<li class="form__group-list-item">
<label class="checkbox">
<input class="checkbox__input" type="checkbox" name="survey" value="twitter">
<span class="checkbox__icon"></span>
<span class="checkbox__text">Twitter</span>
</label>
</li>
<li class="form__group-list-item">
<label class="checkbox">
<input class="checkbox__input" type="checkbox" name="survey" value="facebook">
<span class="checkbox__icon"></span>
<span class="checkbox__text">Facebook</span>
</label>
</li>
<li class="form__group-list-item">
<label class="checkbox">
<input class="checkbox__input" type="checkbox" name="survey" value="instagram" disabled>
<span class="checkbox__icon"></span>
<span class="checkbox__text">Instagram</span>
</label>
</li>
<li class="form__group-list-item">
<label class="checkbox">
<input class="checkbox__input" type="checkbox" name="survey" value="other">
<span class="checkbox__icon"></span>
<span class="checkbox__text">その他</span>
</label>
</li>
</ul>
</div>
</fieldset>
<div class="form__group">
<p class="form__group-header">
<label class="form__group-label" for="datetime">来店予定時間</label>
</p>
<div class="form__group-content">
<div class="form__text-input">
<p class="text-input">
<input id="datetime" class="text-input__body" type="date" name="datetime" title="翌日から二週間後までで指定してください">
<span class="text-input__validator"></span>
</p>
</div>
<p class="form__notion">翌日から二週間後まで指定いただけます</p>
<p class="form__validation" data-validation="datetime" role="alert" aria-live="assertive"></p>
</div>
</div>
<div class="form__group">
<p class="form__group-header">
<span class="form__group-label">添付ファイル</span>
</p>
<div class="form__group-content">
<div class="form__file-select">
<div class="file-select js-flie-select">
<input id="file" class="file-select__input" type="file" name="file">
<label class="file-select__button" for="file">ファイルを選択する</label>
</div>
</div>
</div>
</div>
<div class="form__group">
<p class="form__group-header">
<label class="form__group-label" for="message">お問い合わせ内容</label>
<strong class="form__required-label">必須</strong>
</p>
<div class="form__group-content">
<div class="form__textarea">
<div class="textarea js-flexible-textarea">
<textarea id="message" class="textarea__body" name="message" title="お問い合わせ内容は必ず記入してください" required></textarea>
</div>
</div>
<p class="form__validation" data-validation="message" role="alert" aria-live="assertive"></p>
</div>
</div>
<div class="form__confirm">
<p class="form__confirm-text">
<a class="form__link" href="#privacy-policy">プライバシーポリシー</a>に同意の上、「送信する」ボタンからお進みください。
</p>
</div>
<div class="form__submit">
<p class="form__validation" data-validation="submit" role="alert" aria-live="assertive"></p>
<button class="button" type="submit">送信する</button>
</div>
</form>
document.addEventListener("DOMContentLoaded", () => {
const contactForm = document.getElementById("contant-form");
new Form(contactForm);
});
function Form(form) {
this.form = form;
this.inputElement = this.form.querySelectorAll("input,select,textarea");
this.textarea = this.form.querySelectorAll(".js-flexible-textarea");
this.fileSelect = this.form.querySelectorAll(".js-flie-select");
this.zipButton = this.form.querySelector(".js-address-search");
this.inputDate = this.form.querySelectorAll('[type="date"]');
this.submit = this.form.querySelector('[type="submit"]');
this.init();
this.handleEvent();
}
/**
* 初期化
*/
Form.prototype.init = function () {
this.validateSubmit();
this.inputDate.forEach(this.prepareDateSetting);
this.textarea.forEach(this.flexTextarea);
this.fileSelect.forEach(this.displaySelectedFilename);
};
/**
* イベントを登録する。
*/
Form.prototype.handleEvent = function () {
this.bindValidationHandler(this.inputElement);
this.bindSearchAddressHandler(this.zipButton);
};
/**
* バリデーションに関するイベントを登録する。
*/
Form.prototype.bindValidationHandler = function (target) {
target.forEach((element) => {
// 入力内容が変更されたらメッセージを表示
element.addEventListener("change", this.displayValidation.bind(this));
// 送信ボタンのバリデーション操作
element.addEventListener("change", this.validateSubmit.bind(this));
});
};
/**
* バリデーションに関するイベントを登録する。
*/
Form.prototype.bindSearchAddressHandler = function (target) {
target.addEventListener("click", this.searchAddress.bind(this));
};
/**
* バリデーションメッセージを表示します。
*/
Form.prototype.displayValidation = function (event) {
const targetInput = event.target;
const targetName = targetInput.getAttribute("name");
const invalidMessage = targetInput.getAttribute("title");
const messageArea = this.form.querySelector(
`[data-validation="${targetName}"]`
);
const hasValidateMessage =
messageArea !== null && targetInput.hasAttribute("title");
const isValid = targetInput.validity.valid;
targetInput.setAttribute("data-is-valid", isValid);
if (hasValidateMessage) {
messageArea.innerHTML = isValid ? "" : invalidMessage;
}
return;
};
/**
* フォームの内容に応じて送信ボタンの状態を変えます。
*/
Form.prototype.validateSubmit = function () {
const isValid = this.form.checkValidity();
const submitButton = this.submit;
const messageArea = this.form.querySelector('[data-validation="submit"]');
const disabledSubmit = submitButton.getAttribute("aria-disabled") === "true";
submitButton.setAttribute("aria-disabled", !isValid);
messageArea.innerHTML = isValid ? "" : "必須項目がすべて入力されていません";
if (disabledSubmit) {
submitButton.addEventListener("click", (event) => {
event.preventDefault();
});
}
return;
};
/**
* input[type="date"]の初期値に翌日の日付を指定する
* 指定可能日付を翌日から二週間後までにする。
* @use https://momentjs.com/
*/
Form.prototype.prepareDateSetting = function (target) {
const min = moment().add(1, "days").format("YYYY-MM-DD");
const max = moment().add(14, "days").format("YYYY-MM-DD");
target.value = min;
target.setAttribute("min", min);
target.setAttribute("max", max);
};
/**
* textareaを内容に応じて伸縮させます。
* @see https://qiita.com/tsmd/items/fce7bf1f65f03239eef0
*/
Form.prototype.flexTextarea = function (target) {
const textarea = target.querySelector("textarea");
const dummyBox = document.createElement("div");
dummyBox.className = "_dummy-box";
dummyBox.setAttribute("aria-hidden", true);
target.insertBefore(dummyBox, null);
textarea.addEventListener("input", (e) => {
dummyBox.textContent = e.target.value + "\u200b";
});
};
/**
* input[type=file]で選択されたファイル名を表示します。
*/
Form.prototype.displaySelectedFilename = function (target) {
const targetInput = target.querySelector('input[type="file"]');
const selectedFile = document.createElement("p");
selectedFile.className = "_selected-file";
target.insertBefore(selectedFile, null);
targetInput.addEventListener("input", (e) => {
selectedFile.innerHTML = e.target.files[0].name;
});
};
/**
* 郵便番号の入力で住所をオートコンプリートする時は明示的にボタンを押させて発動します。
* @see https://waic.jp/docs/WCAG21/Understanding/on-input.html
* @use https://github.com/ajaxzip3/ajaxzip3.github.io
*/
Form.prototype.searchAddress = function (event) {
const zip = "postal-code";
const address1 = "address-level1";
const address2 = "address-level2";
const zipInput = this.form.querySelector(`[name="${zip}"]`);
const address1Input = this.form.querySelector(`[name="${address1}"]`);
const address2Input = this.form.querySelector(`[name="${address2}"]`);
AjaxZip3.zip2addr(zip, "", address1, address2);
// 郵便番号検索成功時に実行する処理
AjaxZip3.onSuccess = () => {
address1Input.setAttribute("data-is-valid", true);
address2Input.setAttribute("data-is-valid", true);
address2Input.querySelector(`[name="${address2}"]`).focus();
};
// 郵便番号検索失敗時に実行する処理
AjaxZip3.onFailure = () => {
const messageArea = this.form.querySelector(`[data-validation="${zip}"]`);
zipInput.setAttribute("data-is-valid", false);
messageArea.innerHTML = "郵便番号に該当する住所が見つかりません";
};
return;
};
/**
* レスポンシブの360px未満対応を終わらせる。
*/
!(function () {
const viewport = document.querySelector('meta[name="viewport"]');
function adjustViewport() {
const value =
window.outerWidth > 360
? "width=device-width,initial-scale=1"
: "width=360";
if (viewport.getAttribute("content") !== value) {
viewport.setAttribute("content", value);
}
}
addEventListener("resize", adjustViewport, false);
adjustViewport();
})();
<script src="https://cdn.jsdelivr.net/npm/focus-visible@5.2.0/dist/focus-visible.min.js"></script>
<script src="https://ajaxzip3.github.io/ajaxzip3.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/moment.min.js"></script>
/**
* CSS設計は原則的にMindBEMdingで行う。
* バリエーション分け(Modifier)はハイフン始まりのマルチクラスで行う。
* 状態管理は原則的にWAI-ARIAおよびdata属性で行う。
* JSで生成する要素のclass名はアンスコ始まりにして疑似BEM風にする。
*/
/**
* inputのバリデーションは擬似クラスは使わずにJSで行うようにする
* 擬似クラスを使うと初期状態でバリデーションが表示されてユーザーの望む動きではないため。
*/
$breakpoints: (
"xs": (
min-width: 0
),
"sm": (
min-width: 576px
),
"md": (
min-width: 768px
),
"lg": (
min-width: 992px
),
"xl": (
min-width: 1200px
),
"xxl": (
min-width: 1400px
)
) !default;
@mixin responsive($breakpoint) {
@if map-has-key($breakpoints, $breakpoint) {
@media screen and #{inspect(map-get($breakpoints, $breakpoint))} {
@content;
}
}
// マップ型で定義されていない値が呼び出された時はエラーを返す
@else {
@error '指定されたブレークポイントは定義されていません。' + '指定できるブレークポイントは次のとおりです。 -> #{map-keys($breakpoints)}';
}
}
/**
* 各ブラウザのプレースホルダーのスタイルを一括指定します。
*/
@mixin placeholder() {
&:-ms-input-placeholder {
@content;
}
&::-webkit-input-placeholder {
@content;
}
&::placeholder {
@content;
}
}
@mixin visually-hidden() {
border: 0 !important;
clip: rect(0 0 0 0) !important;
clip-path: inset(50%) !important;
height: 1px !important;
margin: -1px !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
white-space: nowrap !important;
width: 1px !important;
}
html {
box-sizing: border-box;
-webkit-font-smoothing: subpixel-antialiased;
-moz-osx-font-smoothing: auto;
}
:root {
--base-color: #434a56;
--white-color-primary: #f7f7f7;
--white-color-secondary: #fefefe;
--gray-color-primary: #e4e7e9;
--gray-color-secondary: #c2c2c2;
--gray-color-tertiary: #676f79;
--active-color: #30a2d2;
--valid-color: #35ab7a;
--invalid-color: #f72f47;
--valid-icon: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%20%3Cpath%20d%3D%22M9.86%2018a1%201%200%200%201-.73-.32l-4.86-5.17a1%201%200%201%201%201.46-1.37l4.12%204.39%208.41-9.2a1%201%200%201%201%201.48%201.34l-9.14%2010a1%201%200%200%201-.73.33z%22%20fill%3D%22%2335ab7a%22%2F%3E%3C%2Fsvg%3E");
--invalid-icon: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%20%3Cpath%20d%3D%22M13.41%2012l4.3-4.29a1%201%200%201%200-1.42-1.42L12%2010.59l-4.29-4.3a1%201%200%200%200-1.42%201.42l4.3%204.29-4.3%204.29a1%201%200%200%200%200%201.42%201%201%200%200%200%201.42%200l4.29-4.3%204.29%204.3a1%201%200%200%200%201.42%200%201%201%200%200%200%200-1.42z%22%20fill%3D%22%23f72f47%22%20%2F%3E%3C%2Fsvg%3E");
}
*,
*::before,
*::after {
box-sizing: inherit;
margin: 0;
}
body {
background: var(--white-color-primary);
color: var(--base-color);
font-family: "Helvetica Neue", "Segoe UI", "Hiragino Sans",
"Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
font-size: 1em;
line-height: 1.5;
@include responsive(md) {
padding: 5%;
}
}
/**
* フォームのレイアウトを定義します。
* fieldset要素はflexもgridも使いにくいため、floatでレイアウトを組むようにします。
*/
.form {
background-color: var(--white-color-secondary);
@include responsive(xs) {
font-size: 0.75rem;
padding: 40px 5%;
}
@include responsive(md) {
font-size: 0.875rem;
padding: 40px;
}
@include responsive(lg) {
font-size: 1rem;
}
}
.form__headline {
font-weight: normal;
margin: 0;
@include responsive(xs) {
font-size: 1.75em;
}
@include responsive(md) {
font-size: 1.5em;
}
}
.form__description {
line-height: 1.75;
margin-top: 2em;
& > :not(:first-child) {
margin-top: 0.25em;
}
}
.form__group {
border: 0; // デフォルトのfieldsetのスタイルをリセットします
margin-top: 3em;
min-width: auto; // デフォルトのfieldsetのスタイルをリセットします
padding: 0; // デフォルトのfieldsetのスタイルをリセットします
& + & {
margin-top: 1.5em;
}
&::after {
clear: both;
content: "";
display: table;
}
}
.form__group-header {
align-items: center;
display: flex;
flex-wrap: wrap;
float: left;
max-width: 240px;
padding: 0.8em 1.2em 0.8em 0;
width: 100%;
@include responsive(xs) {
font-size: 1.333em;
}
@include responsive(md) {
font-size: 1em;
}
}
.form__group-label {
line-height: 1.5;
&:not(:only-child) {
margin-right: 0.8em;
}
}
.form__required-label {
background-color: var(--gray-color-primary);
border-radius: 3px;
font-size: 0.75em;
font-weight: normal;
letter-spacing: 0.08em;
padding: 0.35em 0.6em;
}
.form__group-content {
float: left;
max-width: 560px;
width: 100%;
}
.form__group-list {
border-bottom: 1px solid var(--gray-color-primary);
list-style: none;
padding: 0.8em 0 1em;
}
.form__group-list-item {
& + & {
margin-top: 1em;
}
}
.form__text-input {
max-width: 440px;
&.-narrow {
max-width: 240px;
}
&.-wide {
max-width: none;
}
}
.form__select-box {
max-width: 240px;
}
.form__name {
display: flex;
max-width: 440px;
& > .form__text-input {
flex: 1;
}
& > :not(:last-child) {
margin-right: 1em;
}
}
.form__postal-code {
@include responsive(xs) {
& > :not(:last-child) {
margin-bottom: 1em;
}
}
@include responsive(sm) {
align-items: center;
display: flex;
& > .form__text-input {
flex-basis: 100%;
}
& > :not(:last-child) {
margin-bottom: 0;
margin-right: 1em;
}
}
}
.form__notion {
margin-top: 1em;
@include responsive(xs) {
font-size: 1em;
}
@include responsive(md) {
font-size: 0.75em;
}
}
.form__link {
color: var(--active-color);
text-decoration: undeline;
text-underline-offset: 0.1em;
&:focus {
text-decoration: none;
}
@media (hover) {
&:hover {
text-decoration: none;
}
}
}
.form__confirm,
.form__submit {
max-width: 800px;
}
.form__confirm {
margin-top: 2.5em;
text-align: center;
}
.form__confirm-text {
display: inline-block;
text-align: left; // 複数行になった時は左寄せする
}
.form__validation {
color: var(--invalid-color);
margin-top: 1em;
&:empty {
display: none;
}
&:not(:empty) + & {
margin-top: 0.5em;
}
@include responsive(xs) {
font-size: 1em;
}
@include responsive(md) {
font-size: 0.75em;
}
}
.form__submit {
align-items: center;
display: flex;
flex-direction: column-reverse;
margin-top: 48px;
}
/**
* テキストインプットのコンポーネントです。
* @usage
* <p class="text-input">
* <input class="text-input__body" type="text">
* <span class="text-input__validator"></span>
* </p>
*/
.text-input {
font-size: 1rem;
position: relative;
z-index: 0;
}
.text-input__body {
appearance: none;
background-color: transparent;
border: 1px solid var(--gray-color-primary);
border-radius: 0;
height: 3.125em;
line-height: 3.125;
overflow: hidden;
padding: 0 1em;
text-overflow: ellipsis;
transition: background-color 0.3s;
width: 100%;
@include placeholder() {
color: var(--gray-color-secondary);
}
&:focus {
border-color: var(--active-color);
box-shadow: inset 0 0 0 1px var(--active-color);
outline: none; // フォーカスリングはbox-shadowで作るのでoutlineは無効化する
}
&[data-is-valid] {
// `[data-is-valid]`セレクタが付与している時はアイコン分右端の余白をあけます
padding-right: 2.5em;
}
&[data-is-valid="true"] {
border-color: var(--valid-color);
box-shadow: inset 0 0 0 1px var(--valid-color);
}
&[data-is-valid="false"] {
border-color: var(--invalid-color);
box-shadow: inset 0 0 0 1px var(--invalid-color);
}
// Google Chromeのオートコンプリート時の背景色を止める
&:-webkit-autofill {
transition-delay: 9999s;
transition-property: background-color;
}
}
.text-input__validator {
background-position: right 0.5em center;
background-repeat: no-repeat;
background-size: 1.5em;
display: inline-block;
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
z-index: -1;
.text-input__body[data-is-valid="true"] + & {
background-image: var(--valid-icon);
}
.text-input__body[data-is-valid="false"] + & {
background-image: var(--invalid-icon);
}
}
/**
* セレクトボックスのコンポーネントです。
* @usage
* <div class="select-box">
* <select class="select-box__body">
* <option></option>
* </select>
* </div>
*/
.select-box {
font-size: 1rem;
position: relative;
transition: background-color 0.5s ease-out;
&::after {
border-color: var(--gray-color-secondary) transparent transparent
transparent;
border-style: solid;
border-width: 6px 4px 0;
bottom: 0;
content: "";
display: inline-block;
height: 0;
margin: auto 0;
pointer-events: none;
position: absolute;
right: 12px;
top: 0;
width: 0;
z-index: 1;
}
}
.select-box__body {
appearance: none;
background-color: transparent;
border: 1px solid var(--gray-color-primary);
border-radius: 0;
cursor: pointer;
height: 3.125em;
line-height: 3.125;
padding-left: 1em;
padding-right: calc(1em + 16px);
width: 100%;
&.focus-visible {
border-color: var(--active-color);
box-shadow: inset 0 0 0 1px var(--active-color);
outline: none; // フォーカスリングはbox-shadowで作るのでoutlineは無効化する
}
&[data-is-valid="true"] {
border-color: var(--valid-color);
box-shadow: inset 0 0 0 1px var(--valid-color);
}
&[data-is-valid="false"] {
border-color: var(--invalid-color);
box-shadow: inset 0 0 0 1px var(--invalid-color);
}
// Google Chromeのオートコンプリート時の背景色を止める
&:-webkit-autofill {
transition-delay: 9999s;
transition-property: background-color;
}
}
/**
* チェックボックスのコンポーネントです。
* @usage
* <label class="checkbox">
* <input class="checkbox__input" type="checkbox">
* <span class="checkbox__icon"></span>
* <span class="checkbox__text">ラベルの文言</span>
* </label>
*/
.checkbox {
align-items: center;
cursor: pointer;
display: inline-flex;
font-size: 1rem;
}
.checkbox__input {
@include visually-hidden();
}
.checkbox__icon {
display: inline-block;
height: 1.5em;
position: relative;
width: 1.5em;
&::before,
&::after {
content: "";
position: absolute;
}
&::before {
border: 2px solid var(--gray-color-primary);
height: 100%;
left: 0;
top: 0;
transition: border-color 0.5s, opacity 0.5s,
transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
width: 100%;
// input[type=checkbox]がチェックされたとき
.checkbox__input:checked + & {
border-color: var(--valid-color);
opacity: 0;
pointer-events: none; // クリッカブルな箇所が広がってしまうのでクリックを無効にする
transform: rotate(45deg) scale3d(2, 2, 1);
}
}
&::after {
border-bottom: 2px solid transparent;
border-left: 2px solid transparent;
height: 0.5em;
left: 0.25em;
opacity: 0;
top: 0.375em;
transform: rotate(-225deg);
transition: opacity 0.5s, transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
width: 1em;
// input[type=checkbox]がチェックされたとき
.checkbox__input:checked + & {
border-bottom: 2px solid var(--valid-color);
border-left: 2px solid var(--valid-color);
opacity: 1;
transform: rotate(-45deg);
}
}
}
.checkbox__text {
display: inline-block;
letter-spacing: 0.01em;
line-height: 1.5;
position: relative;
transition: color 0.3s;
.checkbox__input:checked ~ & {
color: var(--valid-color);
}
// :focus-visibleで指定することでTab移動でfocusされた場合のみfocusを可視化する
.checkbox__input.focus-visible ~ & {
color: var(--active-color);
}
.checkbox__input:disabled ~ & {
color: var(--gray-color-secondary);
}
@media (hover) {
&:hover {
color: var(--active-color);
}
}
&::after {
background-color: var(--active-color);
bottom: -2px;
content: "";
display: inline-block;
height: 2px;
left: 0;
position: absolute;
transform: scale3d(0, 1, 1);
transform-origin: right;
transition: transform 0.3s;
width: 100%;
.checkbox__input:disabled ~ & {
content: none;
}
@mixin isActive {
transform: scale3d(1, 1, 1);
transform-origin: left;
}
// :focus-visibleで指定することでTab移動でfocusされた場合のみfocusを可視化する
.checkbox__input.focus-visible ~ & {
@include isActive;
}
@media (hover) {
.checkbox:hover & {
@include isActive;
}
}
}
.checkbox__icon + & {
margin-left: 1em;
}
}
/**
* ラジオボタンのコンポーネントです。
* @usage
* <label class="radio">
* <input class="radio__input" type="radio">
* <span class="radio__icon"></span>
* <span class="radio__text">ラベルの文言</span>
* </label>
*/
.radio {
align-items: center;
cursor: pointer;
display: inline-flex;
font-size: 1rem;
}
.radio__input {
@include visually-hidden();
}
.radio__icon {
border: 1px solid var(--gray-color-primary);
border-radius: 50%;
display: inline-block;
height: 1.5em;
position: relative;
transition: border-color 0.5s;
width: 1.5em;
.radio__input:checked + & {
border-color: var(--valid-color);
}
&::before,
&::after {
border-radius: 50%;
bottom: 0;
content: "";
height: 0.75em;
left: 0;
margin: auto;
opacity: 0;
position: absolute;
right: 0;
top: 0;
width: 0.75em;
}
&::before {
background-color: var(--gray-color-secondary);
transition: background-color 0.5s, opacity 0.5s;
// input[type=radio]がチェックされたとき
.radio__input:checked + & {
background-color: var(--valid-color);
opacity: 1;
}
}
&::after {
// input[type=radio]がチェックされたとき
.radio__input:checked + & {
animation: radio-ripple 0.8s cubic-bezier(0.075, 0.82, 0.165, 1) forwards;
background-color: var(--valid-color);
pointer-events: none; // クリッカブルな箇所が広がってしまうのでクリックを無効にする
}
}
}
@keyframes radio-ripple {
0% {
opacity: 0.8;
transform: scale3d(1, 1, 1);
}
100% {
opacity: 0;
transform: scale3d(4, 4, 1);
}
}
.radio__text {
display: inline-block;
letter-spacing: 0.01em;
line-height: 1.5;
position: relative;
transition: color 0.3s;
.radio__input:disabled ~ & {
color: var(--gray-color-secondary);
}
.radio__input:checked ~ & {
color: var(--valid-color);
}
// :focus-visibleで指定することでTab移動でfocusされた場合のみfocusを可視化する
.radio__input.focus-visible ~ & {
color: var(--active-color);
}
@media (hover) {
&:hover {
color: var(--active-color);
}
}
&::after {
background-color: var(--active-color);
bottom: -2px;
content: "";
display: inline-block;
height: 2px;
left: 0;
position: absolute;
transform: scale3d(0, 1, 1);
transform-origin: right;
transition: transform 0.3s;
width: 100%;
.radio__input:disabled ~ & {
content: none;
}
@mixin isActive {
transform: scale3d(1, 1, 1);
transform-origin: left;
}
// :focus-visibleで指定することでTab移動でfocusされた場合のみfocusを可視化する
.radio__input.focus-visible ~ & {
@include isActive;
}
@media (hover) {
.radio:hover & {
@include isActive;
}
}
}
.radio__icon + & {
margin-left: 1em;
}
}
/**
* テキストエリアのコンポーネントです。
* JSを利用することで自動伸縮するテキストエリアになります。
* その場合、高さを調整する要素`._dummy-box`がJSにより生成されます。
* @usage
* <div class="textarea js-flexible-textarea">
* <textarea class="textarea__body"></textarea>
* </div>
*/
.textarea {
font-size: 1rem;
line-height: 1.5;
position: relative;
}
.textarea__body {
appearance: none;
background-color: transparent;
border: 1px solid var(--gray-color-primary);
border-radius: 0;
box-sizing: border-box;
font: inherit;
height: 100%;
left: 0;
letter-spacing: inherit;
overflow: hidden;
padding: 1em;
position: absolute;
resize: none;
top: 0;
transition: background-color 0.5s ease-out;
width: 100%;
// JSで自動伸縮しない場合はある程度の高さで固定する。
&:only-child {
min-height: 240px;
position: relative;
resize: vertical;
}
&:focus {
border-color: var(--active-color);
box-shadow: inset 0 0 0 1px var(--active-color);
outline: none; // フォーカスリングはbox-shadowで作るのでoutlineは無効化する
}
&[data-is-valid="true"] {
border-color: var(--valid-color);
box-shadow: inset 0 0 0 1px var(--valid-color);
}
&[data-is-valid="false"] {
border-color: var(--invalid-color);
box-shadow: inset 0 0 0 1px var(--invalid-color);
}
}
.textarea ._dummy-box {
border: 1px solid;
box-sizing: border-box;
min-height: 240px;
overflow: hidden;
overflow-wrap: break-word;
padding: 1em;
visibility: hidden;
white-space: pre-wrap;
word-wrap: break-word;
}
/**
* ファイル選択のコンポーネントです。
* JSを利用することで選択されたファイル名を出力できます。
* その場合、ファイル名を出力する要素`._selected-file`がJSにより生成されます。
* @usage
* <div class="file-select js-flie-select">
* <input id="file" class="file-select__input" type="file">
* <label class="file-select__button" for="file">ファイルを選択する</label>
* </div>
*/
.file-select {
align-items: center;
display: flex;
font-size: 1rem;
}
.file-select__input {
@include visually-hidden();
}
.file-select__button {
// ボタンコンポーネントと指定が被っているが、デザインが似ているだけの別物なので重複を許容する。
background-color: var(--gray-color-tertiary);
border-radius: 3px;
box-shadow: 2px 2px 3px 0 var(--gray-color-primary);
color: var(--white-color-primary);
cursor: pointer;
display: inline-block;
flex-shrink: 0;
font-size: 0.75em;
letter-spacing: 0.08em;
padding: 1em 1.5em;
transition: background-color 0.3s;
.file-select__input.focus-visible ~ & {
background-color: var(--base-color);
outline: 3px solid var(--active-color);
}
@media (hover) {
&:hover {
background-color: var(--base-color);
}
}
}
.file-select ._selected-file {
background-color: var(--gray-color-primary);
border-radius: 3px;
font-size: 0.875em;
letter-spacing: 0.08em;
margin-left: 1em;
overflow: hidden;
padding: 0.35em 0.75em;
text-overflow: ellipsis;
white-space: nowrap;
&:empty {
display: none;
}
}
/**
* ボタンのコンポーネントです。
*/
.button {
appearance: none;
background-color: var(--gray-color-tertiary);
border: 0;
border-radius: 3px;
box-shadow: 2px 2px 3px 0 var(--gray-color-primary);
color: var(--white-color-primary);
cursor: pointer;
display: inline-flex;
letter-spacing: 0.08em;
line-height: inherit;
padding: 1em 4em;
transition: background-color 0.3s;
&.-small {
padding: 1em 1.5em;
@include responsive(xs) {
font-size: 1em;
}
@include responsive(md) {
font-size: 0.75em;
}
}
&[aria-disabled="true"] {
cursor: not-allowed;
opacity: 0.5;
}
&:not([aria-disabled="true"]):focus {
background-color: var(--base-color);
}
@media (hover) {
&:not([aria-disabled="true"]):hover {
background-color: var(--base-color);
}
}
}
.visually-hidden {
@include visually-hidden();
}
.js-focus-visible :focus:not(.focus-visible) {
outline: 0;
}
::-webkit-calendar-picker-indicator {
cursor: pointer;
}
<link href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment