目次を開く(全 14 章)
01
完成したサイトを見る
ひだまりフラワーは、架空の花屋の紹介サイトです。1 ページの中に、お店の紹介、商品、注文の流れ、料金、写真、よくある質問、お問い合わせが上から順に並んでいます。
hidamari-flower/index.html をダブルクリックすると、ブラウザで開けます。サーバーは要りません。ただし、地図とお問い合わせの送信は外部のサービスを使うので、インターネットにつながっている必要があります。
ブラウザの幅を少しずつ狭めながら見ると、どこで並び方が変わるかが分かります。その仕組みは 06 章と 09 章で見ます。
02
ファイルの役割を知る
HTML は内容と構造、CSS は見た目、JavaScript は動き、と役割ごとにファイルを分けています。直したい所があるとき、どのファイルを開けばよいかが名前から分かります。
hidamari-flower/
├── index.html … ページの本体(内容と構造)
├── thanks.html … お問い合わせを送ったあとに表示するページ
├── stylesheets/
│ ├── style.css … CSS の入口。下の 4 つを読み込む
│ ├── base.css … リセット・色などの変数・要素の土台・並べ方
│ ├── components.css … ボタン・カードなど、何度も使う部品
│ ├── sections.css … セクションごとの見た目
│ └── utilities.css … スクロールの演出・印刷用の指定
├── javascripts/
│ └── main.js … メニューの開閉、料金の計算、写真の拡大など
└── images/ … 写真とロゴ(WebP 形式)
index.html の <head> には、ページの説明文、スマートフォンのアドレスバーの色、SNS で共有したときの題名(OGP)、タブのアイコンをまとめて書いています。CSS と JavaScript を読み込むのも、ここです。
<!-- 検索結果に表示される、ページの説明文 -->
<meta name="description" content="ひだまりフラワーは、季節の切り花や花束、植物と自然素材の鉢カバーを揃えた街角の花屋です。一輪から気軽にお立ち寄りください。">
<!-- スマートフォンのブラウザで、アドレスバーなどに使われる色 -->
<meta name="theme-color" content="#416b49">
<!-- SNS でページを共有したときに表示される題名と説明(OGP) -->
<meta property="og:title" content="ひだまりフラワー|日常に、小さな「ひだまり」を。">
…
<!-- 見た目は style.css にまとめて書く -->
<link rel="stylesheet" href="stylesheets/style.css">
<!-- defer を付けると、HTML を最後まで読み込んでから JavaScript が実行される -->
<script src="javascripts/main.js" defer></script>
03
意味のあるタグで骨組みを作る
見た目を整える前に、内容に合ったタグで骨組みを作っています。どこが見出しで、どこが本文で、どこがメニューかを、ブラウザや読み上げソフトが正しく受け取れるようにするためです。
<a class="skip-link" href="#main">本文へスキップ</a>
<header class="site-header" id="header">
…ロゴとメニュー(nav)…
</header>
<main id="main">
<section class="hero" id="hero">…h1 はここに 1 つだけ…</section>
<section class="section" id="about">…</section>
…
</main>
<footer class="site-footer dark">…</footer>
セクションの中身も、内容に合わせてタグを選んでいます。
| 内容 | 使ったタグ | 選んだ理由 |
|---|---|---|
| 花束を選ぶ流れ | <ol> | 手順の順番に意味があるので、番号付きのリストにする |
| 料金の表 | <table> <caption> scope | 表の題名と、見出しが行か列かを伝える |
| 営業時間と定休日 | <dl> <dt> <dd> | 「項目名と値」の組を並べる |
| よくある質問 | <details> <summary> | 押すと開閉する。JavaScript が要らない |
| 商品のカード | <article> | 1 枚ずつが独立した内容のまとまり |
よくある質問は、同じ name を付けた <details> を並べています。こうすると、1 つを開いたときにほかが自動で閉じます。
<div class="faq-list" data-reveal>
<details class="card faq" name="faq" open><summary>一輪だけでも買えますか?</summary><p>もちろんです。お部屋の雰囲気や飾る場所に合う一輪を、一緒に選びましょう。</p></details>
<details class="card faq" name="faq"><summary>花束の予算が決まっていません。相談できますか?</summary>…</details>
…
</div>
アイコンは、ページの先頭で <symbol> として一度だけ定義し、使う場所で <use href="#i-flower"/> のように呼び出しています。線の色を currentColor にしてあるので、アイコンの色は CSS の color で変えられます。
Tab キーを押すと、左上に「本文へスキップ」が現れます。キーボードで操作する人が、メニューを飛ばして本文へ移るためのリンクです。飾りのアイコンには aria-hidden="true" を付け、読み上げの対象から外しています。
04
CSS を役割ごとに分け、層で順番を決める
CSS は 4 つのファイルに分け、入口の style.css から読み込んでいます。そして @layer で、どの指定を優先するかを先に宣言しています。
@layer reset, tokens, base, layout, components, sections, utilities;
@import url("base.css"); /* reset・tokens・base・layout */
@import url("components.css"); /* ボタン・カードなどの共通部品 */
@import url("sections.css"); /* ページの各セクション */
@import url("utilities.css"); /* 表示の演出・印刷 */
@layer の宣言では、後ろに書いた層ほど強くなります。層の強さは、セレクタの詳細度よりも先に比べられます。たとえば sections 層の指定は、components 層の同じ指定に必ず勝ちます。そのため、セレクタを長くして詳細度を上げる必要がありません。
| 層 | 書いてあるもの | ファイル |
|---|---|---|
reset | ブラウザごとの差をそろえる | base.css |
tokens | 色・文字・余白の変数(05 章) | |
base | body や見出しなど、要素そのものの見た目 | |
layout | 幅をそろえる入れ物と、並べるためのグリッド(06 章) | |
components | ボタン・カード・アイコンなどの部品 | components.css |
sections | セクションごとの見た目 | sections.css |
utilities | スクロールの演出・印刷(いちばん強い) | utilities.css |
いちばん弱い reset 層では、さらに :where() で詳細度を 0 にしています。あとから書くどの指定でも上書きできる土台にするためです。
@layer reset {
:where(body, h1, h2, h3, p, figure, dl, dd, ul, ol, fieldset) {
margin: 0;
padding: 0;
}
:where(img, svg) {
display: block;
max-inline-size: 100%;
block-size: auto;
}
/* 上の display: block に負けないよう、hidden 属性の付いた要素は必ず隠す */
[hidden] { display: none; }
}
hidden 属性で要素が隠れるのは、ブラウザ標準の [hidden] { display: none; } のおかげです。この標準の指定は、自分で書いた CSS に必ず負けます。:where(img, svg) { display: block; } を書くと、アイコン置き場の <svg hidden> が表示されてしまうので、最後の 1 行で隠し直しています。
部品の CSS は、ネスト記法で書いています。.btn { svg { … } } は「.btn の中の svg」、&:hover は「.btn 自身にマウスが乗ったとき」です。
@import で読み込むファイルは、style.css を読んだあとに取りに行きます。ファイルがたくさんある大きなサイトでは、そのぶん表示が遅れることがあります。このサイトの規模なら、気になるほどの差はありません。
05
色と余白を変数に集める
サイトで使う色・文字・余白・角丸・影は、base.css の tokens 層にカスタムプロパティとしてまとめています。色を変えたくなったら、ここを 1 か所直せばサイト全体が変わります。
@layer tokens {
:root {
/* 葉の緑。数字が大きいほど濃い */
--leaf-800: #284831;
--leaf-600: #416b49;
…
/* 花びらの黄色 */
--sun-500: #e2b65e;
…
/* 役割で名前を付けた色。ページではこちらを使う。
暗い背景の .dark では、この値だけを差し替える(components.css) */
--text: #2d3b31;
--heading: var(--leaf-800);
--surface: var(--white); /* カードの地 */
--icon-bg: var(--leaf-600);
…
--radius: 18px;
--shadow: 0 12px 32px -14px rgb(30 56 39 / .3); /* 横 縦 ぼかし 広がり 色 */
}
}
変数は 2 段に分けています。--leaf-600 のような色そのものの名前と、--text・--heading・--surface のような役割の名前です。部品の CSS は役割の名前だけを使います。
この分け方が生きるのが、暗い背景の区画です。.dark は、役割の変数の値を差し替えるだけのクラスです。
.dark {
--text: rgb(255 253 250 / .92);
--text-soft: rgb(255 253 250 / .75);
--heading: var(--white);
--accent: var(--sun-500);
…
--surface: rgb(255 255 255 / .08); /* 暗い背景の上のカードは、白をうっすら重ねる */
--icon-bg: var(--sun-500);
--icon-fg: var(--leaf-900);
color: var(--text);
background-color: var(--leaf-800);
}
カスタムプロパティは、親から子へ引き継がれます。class="section dark" を付けたセクションの中では、カードもアイコンも見出しも、差し替えた値で描かれます。部品ごとに「暗い背景用」の CSS を書き足す必要はありません。ガイドのダークモードも、同じ考え方で作っています。
06
1 つの .grid で何でも並べる
商品のカード、写真、料金表とシミュレーター、フッターの 3 列。このサイトで横に並ぶものは、ほとんどが同じ .grid クラスで並んでいます。メディアクエリを書かなくても、画面の幅に合わせて列の数が変わります。
.grid {
display: grid;
gap: clamp(1rem, 2vw, 1.5rem);
grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--min, 15rem)), 1fr));
}
3 行目を、内側から順に読みます。
var(--min, 15rem)… 1 列の最小幅。--minが無ければ 15remmin(100%, …)… 画面が最小幅より狭いときは、はみ出さずに画面の幅に収めるminmax(最小, 1fr)… 各列は最小幅以上で、余った幅は均等に分けるrepeat(auto-fit, …)… 入るだけの数の列を作る
使う側は、HTML で class="grid service-grid" のように 2 つのクラスを並べ、CSS では最小幅の --min だけを変えます。
.service-grid { --min: 24rem; }
.reason-grid { --min: 15rem; }
.reason-feature {
grid-column: 1 / -1; /* 1 列目から最後の列まで。つまり横いっぱい */
…
}
「お花と雑貨」は最小幅を 24rem と大きめにしたので、広い画面でも 2 列 × 2 段になります。「こだわり」は先頭の 1 枚だけに grid-column: 1 / -1 を付けて、横いっぱいに広げています。
同じ考え方で、写真の枠には縦横比の --ratio、カードには内側の余白の --card-pad を用意しています。部品の CSS は共通のまま、使う場所で変数の値だけを変えます。
07
一列に並べるなら Flexbox を使う
行と列をそろえるカードの一覧は Grid、ボタンの中の文字とアイコンや、長さの違うタグのように、中身の大きさのまま一列に並べるものは Flexbox で並べています。
.tag-list {
display: flex;
flex-wrap: wrap;
gap: .4rem;
margin-block-start: 1rem;
…
}
「一輪から」「季節の花」のような札は、文字の数で幅が変わります。flex-wrap: wrap を付けると、入りきらない札は次の行へ送られます。
店主のご挨拶では、写真と文章を Flexbox で横に並べ、flex で伸び縮みの具合を決めています。
.profile {
--card-pad: clamp(1.5rem, 4vw, 3rem);
display: flex;
flex-wrap: wrap;
align-items: center;
gap: clamp(1.5rem, 4vw, 3rem);
/* flex: 0 1 260px … 広がらない・縮んでよい・基本の幅は 260px */
.profile-photo {
…
flex: 0 1 260px;
…
}
/* flex: 1 1 22rem … 余った幅まで広がる・縮んでよい・基本の幅は 22rem */
.profile-body { flex: 1 1 22rem; }
…
}
flex には「伸びるか・縮むか・基本の幅」の 3 つの値を書きます。写真は 260px のまま、文章は余った幅いっぱいに広がります。幅が足りなくなると文章が次の行へ回り、写真と文章が縦に並びます。
料金シミュレーターの「合計金額」の行は、justify-content: space-between で、項目名を左端に、金額を右端に置いています。
08
写真の枠をそろえ、重ねて飾る
写真は縦長・横長が混ざっていても、枠の形をそろえて並べています。写真の上に文字やカードを重ねる工夫も、いくつか使っています。
.photo {
overflow: hidden; /* 角丸の外にはみ出した写真を隠す */
border-radius: var(--radius-l);
box-shadow: var(--shadow);
img {
inline-size: 100%;
aspect-ratio: var(--ratio, 4 / 3); /* --ratio が無ければ 4 / 3 */
object-fit: cover;
}
}
aspect-ratio で枠の縦横比を決め、object-fit: cover で写真を枠いっぱいに広げます。はみ出した部分は切り取られます。ヒーローの写真は --ratio: 16 / 10、季節の花たちは --ratio: 3 / 4 のように、使う場所で縦横比だけを変えています。
写真の上に文字を載せる
「こだわり」の先頭のカードは、写真を背景に敷き、その上に暗いグラデーションを 1 枚重ねています。写真のどこに文字が来ても読めるようにするためです。
.reason-feature {
…
/* 写真の上に暗いグラデーションを 1 枚重ね、文字を読みやすくする
カンマで区切った背景は、先に書いたものが手前に重なる */
background-image:
linear-gradient(90deg, rgb(30 56 39 / .92) 35%, rgb(30 56 39 / .35)),
url("../images/image.webp");
background-size: cover; /* 写真を、枠いっぱいに広げる */
background-position: center 58%; /* 写真のどこを見せるか(横 中央・縦 58%) */
…
}
カードを傾けて重ねる
ヒーローの紹介カード(01 章の画面の右下)は、上の余白をマイナスにして写真に食い込ませ、rotate で少し傾けています。マウスを乗せると、まっすぐに戻ります。
.hero-card {
position: relative; /* 写真より手前に重ねる */
inline-size: min(80%, 300px);
margin-block-start: -3rem; /* 上へ食い込ませて、写真に重ねる */
margin-inline: auto 1.5rem; /* 右寄せ */
text-align: center;
rotate: -2deg; /* マイナスで反時計回りに傾く */
transition: rotate .4s ease;
&:hover { rotate: 0deg; }
…
}
見出しの「ひだまり」の下の黄色い線も、グラデーションで描いています。linear-gradient(transparent 62%, rgb(226 182 94 / .4) 62%) は、上から 62% までが透明、そこから下が黄色という背景です。同じ位置で色を切り替えると、ぼかしのない線になります。
aspect-ratio と object-fit グラデーション translate・rotate・scale aspect-ratio object-fit background-image rotate
09
ヘッダーを固定し、狭い画面ではメニューを開閉する
ヘッダーは画面の上に固定され、スクロールしても見えています。広い画面ではメニューを横一列に並べ、狭い画面ではボタンで開くメニューに切り替えます。
.site-header {
position: fixed;
inset-block-start: 0;
inset-inline: 0; /* 左右とも 0。画面の横幅いっぱいに広げる */
z-index: 100;
border-block-end: 1px solid var(--line);
background-color: rgb(252 249 243 / .85); /* 少し透ける紙の色 */
backdrop-filter: blur(10px); /* 後ろに流れる内容をすりガラスのようにぼかす */
…
}
固定したヘッダーは、本文の上に重なります。そのままでは、メニューからページ内リンクで飛んだとき、セクションの見出しがヘッダーの下に隠れてしまいます。そこで各セクションに scroll-margin-block-start: var(--header-h) を付け、ヘッダーの高さぶんの余白を空けて止まるようにしています(base.css)。
狭い画面のメニュー
切り替えの境目は 60rem(960px)です。狭い画面では、メニューを display: none で隠しておき、.is-open が付いたときだけ表示します。
.gnav {
…
/* 狭い画面:ボタンで開くまで隠しておく
開くと、JavaScript が .is-open を付ける。ヘッダーのすぐ下に、画面幅いっぱいで表示する。 */
@media (width < 60rem) {
display: none;
position: fixed;
inset: var(--header-h) 0 auto; /* 上 左右 下 の順。上はヘッダーの高さの位置から */
…
&.is-open {
display: grid;
gap: 1.2rem;
}
…
}
}
JavaScript は、クラスと属性を付け替えるだけです。ボタンの aria-expanded は、読み上げソフトにメニューが開いているかどうかを伝える属性です。CSS はこの属性を見て、三本線と × のアイコンを切り替えています。
function setMenuOpen(isOpen) {
navToggle.setAttribute('aria-expanded', String(isOpen)); // 属性の値は文字列なので String() で "true" / "false" にする
nav.classList.toggle('is-open', isOpen);
}
// メニューボタンを押したら、今の状態と逆にする(開いていれば閉じ、閉じていれば開く)
navToggle.addEventListener('click', () => {
setMenuOpen(navToggle.getAttribute('aria-expanded') !== 'true');
});
// Esc キーでも閉じる
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
setMenuOpen(false);
}
});
このほかに、メニューの中のリンクを押したときと、メニューの外側を押したときにも閉じるようにしています。
ページ内リンク 配置と重なり filter と backdrop-filter イベント position scroll-margin-block-start aria-expanded
10
スクロールに合わせて表示を変える
ヘッダーの下端の線は、ページをどこまで読んだかに合わせて伸びます。各セクションは、画面に入ったときにふわっと現れます。どちらも、JavaScript は値やクラスを渡すだけで、見た目は CSS が決めています。
読み進めた割合を線で示す
JavaScript が割合を計算し、カスタムプロパティ --progress として要素に渡します。CSS は、その値を線の幅に使います。
// スクロールできる長さ = ページ全体の高さ − 画面の高さ
const scrollableHeight = document.documentElement.scrollHeight - window.innerHeight;
// 「条件 ? A : B」は、条件が正しければ A、そうでなければ B になる書き方
const progress = scrollableHeight > 0 ? Math.min(scrollPosition / scrollableHeight, 1) : 0;
scrollProgress.style.setProperty('--progress', (progress * 100).toFixed(1) + '%');
.scroll-progress {
position: absolute;
inset-block-end: -1px;
inset-inline-start: 0;
inline-size: var(--progress, 0%);
block-size: 3px;
background-image: linear-gradient(90deg, var(--leaf-600), var(--sun-500)); /* 左から右へ、緑から黄色へ */
}
scroll イベントは 1 秒に何十回も起きます。main.js では、そのたびに計算せず、requestAnimationFrame で次の描画の直前に 1 回だけ計算するようにしています。
画面に入ったら表示する
HTML で data-reveal を付けた要素が対象です。IntersectionObserver は、要素が画面に入ったかどうかを検出する仕組みです。入ったら .is-revealed を付け、その要素の監視をやめます。
[data-reveal] {
opacity: 0;
translate: 0 24px;
transition: opacity .7s ease, translate .7s ease;
&.is-revealed {
opacity: 1;
translate: 0 0;
}
}
/* JavaScript が動かないときも、内容は必ず見えるようにする
<html class="no-js"> の no-js は、JavaScript が動くと main.js が取り除く。
残っているのは JavaScript が動かなかったときなので、最初から表示しておく。 */
.no-js [data-reveal] {
opacity: 1;
translate: 0 0;
}
最初に透明にしておく演出は、JavaScript が動かないと内容が見えないままになります。そこで <html class="no-js"> と書いておき、main.js の先頭でこのクラスを外しています。no-js が残っているのは JavaScript が動かなかったときなので、最初から表示します。
動きを減らす設定の人には動かさない
OS で「視差効果を減らす」「動きを減らす」を選んでいる人には、prefers-reduced-motion で動きを止めます。変化の時間をほぼ 0 にすると、1 回で終わる動きは一瞬で終わります。
ただし、ヒーローの上を流れ続けるキャッチコピー(infinite のアニメーション)は、時間を短くすると高速で回り続けてしまいます。そのため、この要素だけは animation: none で止めています。
@media (prefers-reduced-motion: reduce) {
…
/* すべての要素のアニメーションと変化の時間を、ほぼ 0 にする */
*, *::before, *::after {
animation-duration: .01ms;
transition-duration: .01ms;
}
…
/* 流れ続ける動きは、時間を短くすると高速で回ってしまうので、止める */
.marquee p { animation: none; }
}
動きをつける DOM 操作 状態と画面を分ける IntersectionObserver requestAnimationFrame() prefers-reduced-motion
11
選んだ内容から料金を計算する
サイズ・点数・オプションを選ぶと、合計金額がその場で変わります。入力部品は HTML の標準のものを使い、選ばれている行の強調は CSS の :has()、計算は JavaScript が受け持っています。
<fieldset class="sim-field">
<legend>サイズを選ぶ</legend>
<label class="choice"><input type="radio" name="size" value="3000">S(3,000円)</label>
<label class="choice"><input type="radio" name="size" value="5000" checked>M(5,000円)</label>
<label class="choice"><input type="radio" name="size" value="8000">L(8,000円)</label>
</fieldset>
<div class="sim-field">
<label for="simQuantity">点数:<output id="simQuantityOut" for="simQuantity">1 点</output></label>
<input type="range" id="simQuantity" min="1" max="5" step="1" value="1">
</div>
…
<p class="sim-result">合計金額<output class="sim-amount" id="simAmount" aria-live="polite">5,300 円</output></p>
<fieldset>と<legend>で、関係のある選択肢をまとめ、見出しを付けます。<label>で入力部品を包むと、文字を押しても選べます。- 金額は
valueに数字だけで書いておき、JavaScript が読み取ります。 <output>は計算結果を表示する要素です。aria-live="polite"を付けたので、値が変わると読み上げソフトが読みます。
選ばれている行の強調には、JavaScript を使っていません。:has(input:checked) は「チェックされた input を中に持つ要素」を選ぶセレクタです。
.choice {
…
input {
…
accent-color: var(--sun-500);
}
&:has(input:checked) {
border-color: var(--sun-500);
background-color: rgb(226 182 94 / .15);
}
}
accent-color は、ラジオボタンやチェックボックス、スライダーの色を変えるプロパティです。
計算は「点数 ×(サイズの価格 + 選んだオプションの価格の合計)」です。
const yen = new Intl.NumberFormat('ja-JP'); // 5300 を「5,300」のように 3 桁ごとに区切る書式
function updateEstimate() {
// 入力欄の値は文字列なので、Number() で数に直してから計算する
const quantity = Number(quantityInput.value);
const selectedSize = sizeInputs.find((input) => input.checked); // 選ばれているラジオボタン
const basePrice = Number(selectedSize.value);
// チェックされたオプションだけを取り出し(filter)、価格を足し合わせる(reduce)。0 は足し始めの値
const optionPrice = optionInputs
.filter((input) => input.checked)
.reduce((sum, input) => sum + Number(input.value), 0);
const total = quantity * (basePrice + optionPrice);
quantityOutput.textContent = quantity + ' 点';
amountOutput.textContent = yen.format(total) + ' 円';
}
// スライダーは動かしている最中(input)、ラジオとチェックは選んだとき(change)に計算し直す
quantityInput.addEventListener('input', updateEstimate);
オプションは、HTML の name="option" を持つチェックボックスをすべて集めて計算しています。HTML にチェックボックスを 1 行足せば、JavaScript を直さなくても計算に加わります(13 章)。
フォーム :is() :where() :has() 配列とメソッド イベント <output> accent-color Intl.NumberFormat
12
写真を拡大表示し、お問い合わせを受け付ける
季節の花たちの写真は、押すと大きく表示されます。ページの最後には、お問い合わせのフォームと地図を置いています。
写真を拡大表示する
<dialog>。左右の矢印で写真を切り替えます。写真のリンク先は、画像ファイルそのものです。JavaScript が動かなくても、押せば写真が開きます。JavaScript が動くときは、リンク本来の移動を止めて、<dialog> を showModal() で開きます。
link.addEventListener('click', (event) => {
// 新しいタブで開く操作(Ctrl キーを押しながらのクリックなど)は、通常のリンクとして扱う。
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
return;
}
event.preventDefault(); // リンク本来の動き(画像ファイルへの移動)を止める
opener = link;
showGalleryImage(index);
lightbox.showModal(); // dialog をページの手前に開く
});
…
// 閉じたら、開いたときのリンクにフォーカスを戻す。キーボードで操作する人が、元の場所から続けられる。
// ?. は、opener が null のときは何もしない書き方
lightbox.addEventListener('close', () => {
opener?.focus();
});
showModal() で開いた <dialog> は、ページの手前に重なり、Esc キーで閉じられます。後ろの暗い幕は、CSS の ::backdrop で色を付けています。スマートフォンのようにマウスを使わない端末では、写真がもともと画面いっぱいに見えるので、拡大表示を使わずに写真だけを残しています。
お問い合わせを受け付ける
HTML と CSS だけのサイトには、送られてきた内容を受け取ってメールで知らせる仕組みがありません。そこで、送信先に外部のサービス SSGform を使っています。SSGform でフォーム用の ID を発行し、action の URL の末尾を差し替えれば、送られた内容がメールで届きます。送信後に表示するページには、同じフォルダーの thanks.html を設定します。
<!-- action の仮 ID を SSGform で発行された ID に替え、転送先を thanks.html に設定する。 -->
<form class="card contact-form" action="https://ssgform.com/s/HidamariFlowerDemo" method="post" data-contact-form data-reveal>
…
<label for="contactEmail">メールアドレス <span class="required">必須</span></label>
<input id="contactEmail" name="メールアドレス" type="email" autocomplete="email" maxlength="254" required>
…
<!-- SSGform の簡易スパム対策用。人には見えない欄で、入力があれば迷惑メールとして扱われる。 -->
<p hidden>
<label>入力しないでください <input type="text" name="wana" tabindex="-1" autocomplete="off"></label>
</p>
<button class="btn" type="submit">送信する…</button>
</form>
入力の確かめは、まず HTML の属性に任せています。required は必須、type="email" はメールアドレスの形、maxlength は最大の文字数です。ここまでは JavaScript が無くても働きます。
HTML の属性では、空白だけの入力を防げません。そこで main.js が、送信の直前に空白だけの欄を探し、setCustomValidity() でエラーの文を付けてから、reportValidity() でブラウザ標準の吹き出しを出しています。
contactForm.addEventListener('submit', (event) => {
requiredFields.forEach((field) => {
field.setCustomValidity(field.value.trim() ? '' : 'この項目を入力してください。');
});
if (!contactForm.reportValidity()) {
event.preventDefault(); // 送信を止める
}
});
thanks.html には <meta name="robots" content="noindex"> を書き、検索結果に出ないようにしています。
フォーム ブラウザとサーバー フォームの検証 setCustomValidity()
地図を埋め込む
お店のご案内の地図は、Google マップを <iframe> で埋め込んでいます。title に地図の説明を書き、loading="lazy" で、画面に近づいてから読み込むようにしています。
13
自分で改造してみる
読むだけでなく、少しずつ書き換えてみると、仕組みがよく分かります。始める前に hidamari-flower フォルダーを丸ごとコピーしておくと、いつでも元に戻せます。
- サイトの色を変える
base.cssの--leaf-600を別の色にして、再読み込みします。ボタン、アイコン、表の見出しがまとめて変わります(05 章)。 - 列の数を変える
sections.cssの.service-grid { --min: 24rem; }を15remにすると、広い画面で 4 枚が横一列に並びます(06 章)。 - よくある質問を足す
index.htmlの<details>を 1 行コピーして、質問と答えを書き換えます(03 章)。 - 料金のオプションを足す
「花器」のチェックボックスを 1 行コピーし、文字と
valueの金額を書き換えます。JavaScript を直さなくても、合計に加わります(11 章)。 - 写真を 1 枚足す
季節の花たちの
<figure>を 1 つコピーし、画像のファイル名と説明を書き換えます。拡大表示の枚数も自動で増えます(12 章)。
14
次の一歩
このサイトは、ガイドの HTML・CSS・JavaScript の範囲だけで作っています。気になった書き方があれば、各章の「ガイド」のリンクから、該当する節を読み直してください。反対に、ガイドの節にある「作例」のリンクから、このページへ戻ってくることもできます。
身近なお店や、自分の好きなことを紹介する 1 ページのサイトを作ってみましょう。このサイトの base.css と components.css を土台に借りて、色の変数とセクションを書き換えるところから始めると、早く形になります。
作ったサイトを公開するなら、公開編へ進むこともできます。