令和のHTML / CSS / JavaScriptの書き方50選
Web制作の技術は日々進化しており、会社やプロジェクトによっては昨今の環境に適さない書き方をしているケースも時折見受けられます。
そこで今回は「2024年のWeb制作ではこのようにコードを書いてほしい!」という内容をまとめました。
質より量で、まずは「こんな書き方があるんだ」をこの記事で伝えたかったので、コードの詳細はあまり解説していません。なので、具体的な仕様などを確認したい方は参考記事を読んだりご自身で調べていただけると幸いです。
1. HTML
画像周りはサイトパフォーマンスに直結するので、まずはそこだけでも取り入れていただきたいです。また、コアウェブバイタルやアクセシビリティも併せて理解しておきたい内容です。
Lazy loading
<img>
にloading="lazy"
属性を付けると画像が遅延読み込みになり、サイトの読み込み時間が早くなります。
<img src="..." alt="" width="600" height="400" loading="lazy">
loading="lazy"
属性の補足です。
-
width
とheight
が必須(レイアウトシフト対策にもなるので必ず付けましょう) -
iframe
要素にも使える - 仲間的な
decoding="async"
はあまり意味がない
Picture要素
画面幅に応じて画像を出し分ける時は<picture>
を使います。
CSS側(display: none
など)で画像を出し分けると、小さい画面幅の時には不要な「大きい画面幅用の画像」も読み込まれるのでサイトパフォーマンスが悪くなります。
<picture>
<source media="(min-width:768px)" srcset="lerge.png" width="400" height="200">
<img src="small.png" alt="" width="80" height="40">
</picture>
Details要素
アコーディオンの実装には<details>
を使います。ページ内検索で閉じているアコーディオンの中身もヒットしたり、開閉処理が備え付けられているなどのメリットがあります。
<details>
<summary>タイトル</summary>
アコーディオンの中身
</details>
開閉処理のアニメーションには、GSAPやgrid-template-rows
を使った方法があります。
Dialog要素
モーダルウィンドウの実装には<dialog>
を使います。アクセシビリティに優れていたり、z-index
を使わなくても最上位に表示されるなどのメリットがあります。
<dialog open>
<div>モーダルのコンテンツ</div>
<form method="dialog">
<button>閉じる</button>
</form>
</dialog>
iOS Safariのバージョン15.3以下がサポート外なので、プロジェクトの要件に満たしているかを必ず確認しましょう。
Hgroup要素
見出しに複数の要素(主題+副題)がある場合は<hgroup>
でグルーピングします。
<hgroup>
<h2>DX支援事業</h2>
<p>経営課題をDXで解決</p>
</hgroup>
Dl要素
<dl>
の直下には<div>
を置き、その直下に<dt>
と<dd>
を置くことでスタイリングがしやすくなります。最新のWHATWGの仕様では<dl>
の直下に<div>
を置けるようになっています(<div>
を置かなくても仕様的には問題ありません)。
<dl>
<div>
<dt>クラウドコンピューティング</dt>
<dd>インターネット経由でコンピューターの資源を提供する...</dd>
</div>
<div>
<dt>API</dt>
<dd>Application Programming Interfaceの略で、ソフトウェア間で...</dd>
</div>
</dl>
<dl>
<dt>クラウドコンピューティング</dt>
<dd>インターネット経由でコンピューターの資源を提供する...</dd>
<dt>API</dt>
<dd>Application Programming Interfaceの略で、ソフトウェア間で...</dd>
</dl>
Button要素
<button>
はフォーム送信ボタンをマークアップする際に使いますが、フォーム以外の部分でも使えます。
「要素をクリックした時に特定の処理を実行する」のような処理を実装する場合、クリック対象の要素は<button>
か<a>
を使います。たまに<div>
や<p>
を使っているコードを見かけますが、本来クリックできない要素にクリック処理を施すと、ブラウザによってはクリックやタップが反応しなかったり、フォーカスが当たらなかったりなどのデメリットが生じます。
<button type="button" id="js-trigger-button">ボタン</button>
<p id="js-trigger-button">ボタン</p>
type="button"
を付けることで<button>
デフォルトの挙動の送信処理がストップします。
Search要素
サイト内検索や絞り込みを行うフォームの実装には<search>
を使うことで、スクリーンリーダーなどに「検索フォーム」ということを指し示せるようになります。
<search>
<form action="/search">
<label for="query">サイト内を検索</label>
<input type="search" name="q" id="query">
<button type="submit">検索</button>
</form>
</search>
iOS Safariのバージョン16.7以下がサポート外なので、プロジェクトの要件に満たしているかを必ず確認しましょう。
role属性、aria属性(WAI-ARIA)
アクセシビリティ向上の目的でW3Cが定めているWAI-ARIAという仕様の中にrole
属性とaria
属性があります。これらを駆使することでコンテンツの構造や機能に関する情報をスクリーンリーダーなどに適切に伝えることができます。
以下はアクセシビリティを意識したタブの実装例です。
<div role="tablist">
<a href="#tab-panel-1" id="tab1" role="tab" aria-controls="tab-panel-1" aria-selected="true" tabindex="0">タブ1</a>
<a href="#tab-panel-2" id="tab2" role="tab" aria-controls="tab-panel-2" aria-selected="false" tabindex="-1">タブ2</a>
</div>
<div id="tab-panel-1" role="tabpanel" aria-labelledby="tab1" tabindex="0">
コンテンツ1
</div>
<div id="tab-panel-2" role="tabpanel" aria-labelledby="tab1" tabindex="0">
コンテンツ2
</div>
<div>
<a href="#tab-panel-1" class="active">タブ1</a>
<a href="#tab-panel-2">タブ2</a>
</div>
<div id="tab-panel-1">
コンテンツ1
</div>
<div id="tab-panel-2">
コンテンツ2
</div>
rel="preload"
優先的に読み込みたいリソースがある場合は<link>
のrel="preload"
を使います。例えば、ファーストビューの画像や動画の表示が遅い時はrel="preload"
で優先的に読み込んでみると改善する可能性があります。
<link rel="preload" href="mv.webp" as="image" type="image/webp" />
CDN
CDN(Contents Delivery Network)でプラグインなどの外部ファイルを読み込むと「キャッシュサーバーを利用できるので読み込みが早くなる」という理由でよく使われていましたが、CDNで読み込むのは非推奨です。
参考:UNPKGの障害によって影響を受けたmicroCMSの投稿
NPMを使える環境の場合、NPMで読み込むことを推奨します。使えない場合はファイルをダウンロードして同プロジェクト内に置いて読み込みましょう。
<script src="/js/bundle.js"></script>
<script src="https://cdn.jsdelivr.net/..."></script>
インラインSVG
SVGの色をCSS側で変えたい時はインラインで埋め込むと思います。その際にSVGのコードをそのままHTMLに埋め込むのではなく、SVGを<symbol>
に変換して別ファイルに保存し、それを<use>
で呼び出します。こうすることでSVGの記述量が少なくなるのでHTMLの可読性が高まります。
また、<svg>
のwidth
height
fill
の属性を削除したほうがCSSで扱いやすくなります。
<div class="icon">
<svg>
<use xlink:href="img/arrow.svg#arrow"></use>
</svg>
</div>
<!-- img/arrow.svg -->
<symbol id="arrow" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></symbol>
<div class="icon">
<svg viewBox="0 0 24 24" width="24" height="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" fill="red"/></svg>
</div>
2. CSS
モダンなCSSを取り入れるなら、まずはレイアウト手法のGrid Layoutに慣れることからスタートするといいでしょう。また、CSSは特にブラウザのサポート状況が複雑なので、Can I use...などでしっかりと確認してから実務に取り入れてください。
Grid Layout
記事一覧などの格子状のレイアウトはGrid Layoutで実装します。Flexboxに比べ、レスポンシブ時に要素の順番や大きさが変わるケースにも対応ができたり、calc()
を使った横幅や余白の複雑な計算も不要になります。
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 3列に並べる */
gap: 40px; /* 子要素の上下左右の間隔 */
}
.grid {
display: grid;
grid-template-areas: "thumb title" "thumb description"; /* 付けた名前を並べる */
grid-template-columns: 300px 1fr; /* 1列目は300px、2列目は余った幅全て */
}
/* gridの子要素 */
.grid_title {
grid-area: title; /* 名前を付ける */
}
.grid_description {
grid-area: description;
}
.grid_thumb {
grid-area: thumb;
}
Subgrid
Grid Layoutで並べた各アイテム内の要素の縦位置を揃えたい時にSubgridを使います。こちらの例では、説明文の高さがバラバラでも日付の縦位置が同じ位置になるように実装しています。
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 40px;
}
.card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 3;
gap: 20px;
}
iOS Safariのバージョン15.8以下がサポート外なので、プロジェクトの要件に満たしているかを必ず確認しましょう。
gap
Flexboxで横並びにした要素の余白を調整するならgap
プロパティを使います。margin
を使うと、calc
や◯◯-of-type
などの記述が発生するので複雑になってしまいます。
.flex {
display: flex;
gap: 20px;
}
.flex {
display: flex;
}
.child {
margin-left: 20px;
}
.child:first-of-type {
margin-left: 0;
}
:has / :is / :where
便利な擬似クラスがここ数年で追加されました。
擬似クラス | 解説 | 備考 |
---|---|---|
:has() |
引数に指定した子孫要素を持つ場合、自分自身にマッチ | iOS Safari バージョン15.3以下ではサポート外 |
:is() |
引数に指定した要素にマッチ | 詳細度は通常計算 |
:where() |
引数に指定した要素にマッチ | 詳細度が常に0になる |
:has()
を使うことで、CSSだけで子要素の有無に応じてスタイルを変えられます(これまではJSを使っていました)。
/* .card の中に a が含まれているなら背景を赤に、含まれていないなら青にする */
.card {
background-color: blue;
}
.card:has(a) {
background-color: red;
}
:is()
:where()
を使うことで、親要素や前方隣接要素の状態に応じた記述が楽になります。
.post:is(h2, h3, h4, h5, h6) {
font-weight: bold;
}
.post h2, .post h3, .post h4, .post h5, .post h6 {
font-weight: bold;
}
Sassでも便利な使い方があります。以下はラジオボタンの選択状態に応じて背景色を変える例で、:is()
を使うことでspan
のブロック内にスタイルをまとめています。
.radio {
span {
background-color: blue; // 未選択の時
&:is(input:checked + span) {
background-color: red; // 選択済みの時
}
}
}
.radio {
span {
background-color: blue; // 未選択の時
}
input:checked + span {
background-color: red; // 選択済みの時
}
}
object-fit
background-size
プロパティの挙動を使うために画像をbackground-image
プロパティで読み込むのは古い手法です。昨今では<img>
で読み込んだ画像に対してobject-fit
を使うことで、background-size
と全く同じ挙動を再現できます。
<img>
を使えば遅延読み込みなどの恩恵を受けられるので、画像はできるだけ<img>
で読み込むようにしましょう。
.img {
width: 100px;
height: 100px;
object-fit: cover;
}
.img {
width: 100px;
height: 100px;
background-image: url(...);
background-size: cover;
}
aspect-ratio
画像の比率を制御するにはaspect-ratio
プロパティを使います。padding-top
を%
で指定する昔ながらの手法もありますが、aspect-ratio
のほうが記述が簡潔で分かりやすいです。
.img {
width: 100px;
height: 100px;
aspect-ratio: 16/9; /* 縦横比を16:9に */
object-fit: cover; /* coverを指定しないと画像の縦横比が崩れる */
object-fit-position: center top; /* 必要に応じて画像の位置を調整 */
}
.parent {
position: relative;
padding-top: 56.25%; /* 56.25% = 16:9 (9/16*100%) */
}
.child {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
inset
親要素全体に自身のサイズを広げる場合、inset
プロパティを使うと記述が簡潔になります。inset
はtop
left
right
bottom
を一括指定するショートハンドプロパティです。
.element {
position: absolute;
inset: 0;
margin: auto;
}
.element {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}
margin-inline: auto
横幅を指定している要素の左右中央寄せはmargin-inline: auto
を使います。
/* margin-inlineを使った書き方 */
.element {
margin-inline: auto;
}
.element {
margin-left: auto;
margin-right: auto;
}
.element {
margin: 0 auto;
}
place-content: center
横幅を指定しない要素の左右中央寄せはplace-content: center
を使います。
.parent {
display: grid;
place-content: center;
}
.parent {
display: flex;
justify-content: center;
align-items: center;
}
width: fit-content
width: fit-content
を指定すると自身の横幅が子要素の横幅と同じ値になります。つまり、width
に固定値を指定しなくてもmarign-inline: auto
などで中央配置できるようになります。
/* ひとつの要素で中央配置が可能に! */
.target {
width: fit-content;
marign-inline: auto;
}
.parent {
text-align: center;
}
.target {
display: inline-block;
}
word-break
文字列がはみ出ないように折り返しをword-break: break-word
で制御している方は多いと思いますが、現在は非推奨です。
文字列の折り返しについてはICSさんの記事で丁寧に解説されているので是非ご覧ください。
body {
overflow-wrap: anywhere;
word-break: normal;
line-break: strict;
}
body {
word-break: break-word;
}
transform
transform
プロパティのtranslate
やrotate
は独立プロパティになったので、以下のように指定できます。
.element {
translate: 10px;
scale: 1.5;
rotate: 45deg;
}
複数の変形を行っている場合の記述も簡潔になります。
.icon {
translate: 10px;
rotate: 45deg;
}
a:hover .icon {
rotate: 90deg;
}
.icon {
transform: translate(10px) rotate(45deg);
}
a:hover .icon {
transform: translate(10px) rotate(90deg);
}
transition
transition
プロパティを使う時はアニメーションを適用させたいプロパティを必ず指定します。
プロパティを指定しないでtransition: all 0.3s
のようにすると全てのプロパティにアニメーションが適用されるので、ページ読み込み時やレスポンシブ時に変な挙動になることがあります。
.fadein {
transition: opacity 0.3s;
}
.fadein {
transition: 0.3s;
}
filter
filter
プロパティを使うことで、画像をぼかしたり暗くしたりすることができます。hover時に画像をぼかすような処理も、ぼかし用の画像に切り替えるのではなくCSSだけで完結するので、画像が運用時に変わってもぼかし用の画像作成が不要になります。
.photo {
filter: blur(10px);
}
mix-blend-mode
Figmaなどのデザインツールの機能にある描画モード(乗算、スクリーン、オーバーレイなど)をブラウザ上でも再現できるのがmix-blend-mode
プロパティです。filter
プロパティと同様に、元画像に手を加えずに加工ができるので運用が楽になります。
.photo {
mix-blend-mode: overlay;
}
clip-path
三角形などの図形を描画するにはclip-path
プロパティを使います。三角形を作るにはborder
を使った昔ながらの手法がありますがclip-path
のほうが直感的に扱えます。
.triangle {
clip-path: polygon(100% 50%, 0 0, 0 100%);
width: 100px;
height: 100px;
background-color: red;
}
.triangle {
width: 0;
height: 0;
border-style: solid;
border-width: 100px 0 100px 173.2px;
border-color: transparent transparent transparent red;
}
便利なジェネレーターもあります。
currentColor
currentColor
を値として指定すると、現在のcolor
プロパティの値が参照されます。
以下のようなボタンの実装例を用意しました。currentColor
を使うことで、hover時のsvgの色指定を省略できます。
<a class="button" href="">
<span>BUTTON</span>
<svg ... /> // 矢印アイコン
</a>
.button {
color: white;
/* ...略 */
}
.button:hover {
color: blue;
/* ...略 */
}
.button svg {
fill: currentColor; /* .buttonのcolorを参照しているので、通常時はwhite、hover時はblueになる */
}
.button {
color: white;
/* ...略 */
}
.button:hover {
color: blue;
/* ...略 */
}
.button svg {
fill: white;
}
.button:hover svg {
fill: blue;
}
clamp()
clamp関数はvw
などの動的な値に対して最大(最小)値を設定できます。
例えば、フォントサイズにvw
を指定すると大きく(小さく)なりすぎることがありますが、clamp関数を使うことで最大(最小)の文字サイズを指定できるようになります。ブレイクポイントでvw
の値を変えるより直感的に扱えます。
.text {
font-size: clamp(16px, 5vw, 20px); /* ベースサイズは5vw、最小16px、最大20px */
}
.text {
font-size: 5vw;
}
@media (max-width: 767px) {
.text {
font-size: 8vw;
}
}
便利なジェネレーターもあります。
svh
要素の高さを画面いっぱいにするには100vh
ではなく100svh
を指定します。vh
はiOSのアドレスバーの高さを含んでしまうので「画面の高さ+アドレスバーの高さ」になってしまいますが、svh
はアドレスバーの高さを含まない純粋な「画面の高さ」のみを取得できます。
.main-visual {
height: 100svh;
}
border-radius: 100vmax
完全な角丸のボタンを実装する時のborder-radius
には9999px
などの大きい数値を指定するのではなく100vmax
を指定することで、ボタンがどんな大きさになっても完全な角丸を保てるようになります。
.button {
border-radius: 100vmax;
}
.button {
border-radius: 9999px;
}
@media (min-width: 768px) & range記法
昨今のブラウザではメディア種別のscreen
を省略しても「画面」と認識してくれるので、メディアクエリのscreen and
は省略しても問題ありません。
@media (min-width: 768px) {
.element { ... }
}
@media screen and (min-width: 768px) {
.element { ... }
}
また、range記法という記述方法も2023年にリリースされました。
@media (width <= 768px) {
.element { ... }
}
iOS Safariのバージョン16.3以下がサポート外なので、使う場合はコンパイラを挟むことを推奨します。
any-hover: hover
スマホやタブレットなどタップで操作をする端末ではhover処理は無効にします。
タップデバイスを判定するにはメディア特性のany-hover: hover
を使います。昨今は小さいノートパソコンや大きいスマホなどがあるので、画面幅で判定するのはよろしくありません。
@media (any-hover: hover) {
.button:hover {
background-color: red;
}
}
@media (min-width: 768px) {
.button:hover {
background-color: red;
}
}
prefers-reduced-motion: reduce
メディア特性のprefers-reduced-motion
を使うことで、デバイス設定で「視差効果を減らす」が有効かどうかを判定できます。
ユーザーは過度なアニメーションを求めていない場合もあるので、ユーザー側でアニメーションのON/OFFを選択できるように実装してあげることが大切です。
以下は「視覚効果を減らす」が有効化されている時に、アニメーション時間を極限まで短くする例です。
@media (prefers-reduced-motion: reduce) {
*,
::before,
::after {
transition-duration: 1ms !important;
animation-duration: 1ms !important;
animation-iteration-count: 1 !important;
}
}
Visually Hidden
Visually Hiddenとは、視覚的には要素を非表示にしたいけど、スクリーンリーダーには読み上げてもらいたい時に使うCSSスニペットです。
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
ラジオボタンやチェックボックスのinput
要素を非表示にしてスタイリングする際は、display: none
ではなくVisually Hiddenを使います。display: none
でinput
要素自体を消してしまうとフォーカスが当たらないなどの弊害が生じます。
[type="radio"] {
/* visually-hiddenのスタイル */
}
[type="radio"] {
display: none;
}
親要素の左右にpaddingが指定されている状態で子要素の横幅を画面幅と同じにするレイアウト手法
親要素の左右にpadding
が指定されている状態で、子要素の幅を画面幅と同じにする場合はcalc
とvw
を使って実装します。
.wrapper {
padding-left: 40px;
padding-right: 40px;
}
.photo {
width: 100vw;
margin-inline: calc(50% - 50vw);
}
従来の書き方だと、以下のようにpadding
の値に応じて子要素のwidth
やmargin
の値も変わってしまいます。これだと、レスポンシブ時にpadding
の値が変わったらwidth
やmargin
も変える必要がありますが、calc
とvw
を使うことで再定義が不要になります。
.wrapper {
padding-left: 40px;
padding-right: 40px;
}
.photo {
width: calc(100vw + 80px); /* 80px = 左右のpaddingの合計値 */
margin-left: -40px; /* ネガティブマージンで要素を左に移動させる */
}
コンテンツ幅から片方だけ画面の端まではみ出しているレイアウト手法
このようなレイアウトもcalc
とvw
を使うことで効率よく実装できます。
.片方だけはみ出させる要素(左配置の場合) {
width: 50vw;
margin-left: calc((50vw - 50%) * -1);
}
.片方だけはみ出させる要素(右配置の場合) {
width: 50vw;
margin-right: -50vw;
}
/* 反対側の要素には`width: 50%`を、これらの親要素には`display: flex`を指定します */
詳しくはCodepenをご覧ください。
メインコンテンツが少ない状態でもフッターを画面最下部に固定させるレイアウト手法
コンテンツ量が少なくてもフッターを画面最下部に固定するレイアウト手法です。
body {
min-height: 100dvh;
}
footer {
position: sticky;
top: 100%;
}
3. JavaScript
JavaScriptも画像と同様にパフォーマンスに影響を与えやすい項目なので、ファイルの読み込み方やスクロール時の処理の実装方法などをまずは覚えることをおすすめします。
Defer
<script>
にdefer
属性を付けると非同期でJSファイルがダウンロードされます。また、ダウンロード開始をできるだけ早くしたいので</body>
の手前ではなく<head>
のできるだけ上のほうで読み込ませます。
<head>
<script src="script.js" defer>
...
</head>
...
<script src="script.js">
</body>
また、type="module"
属性を指定することで、そのファイル内で定義した変数はグローバル変数として扱われなくなります。そのため、他のファイルで同じ変数名を使用していても、名前空間が分離されているのでお互いに影響を与えることがなくなります。
<script src="script.js" defer type="module">
DOMContentLoaded
JSファイルを<head>
の中でdefer
を付けて読み込む場合、ページ読み込み時の処理にはDOMContentLoaded
イベントを使うことで、DOMツリー構築完了時の実行が保証されます。これにより、要素の取得エラーなどが発生しなくなります。
load
イベントの場合は画像などのリソース読み込み完了後に実行されるので、実行タイミングが遅くなってしまいます。即時実行関数はDOMContentLoaded
とほぼ同じタイミングで実行はされますが、もしJSファイルの読み込み位置が変わったら、その位置によって実行タイミングが変わるので実装時に余計な気を遣う必要が出てきます。
window.addEventListener('DOMContentLoaded', () => {
// ここにページ読み込み時の処理を書く
});
window.addEventListener('load', () => {
// ここにページ読み込み時の処理を書く
});
(() => {
// ここにページ読み込み時の処理を書く
})();
Debounce
スクロールイベントやリサイズイベントは実行される頻度が極端に高いので、ブラウザに負荷がかかり画面がカクカクする原因になります。なので、Debounceという手法で実行頻度を減らしてあげます。
function debounce(func, timeout) {
let timer;
timeout = timeout !== undefined ? timeout : 300; // funcが呼び出されるまでの遅延時間
return () => {
const context = this;
const args = arguments;
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(context, args);
}, timeout);
};
}
以下はリサイズ時にヘッダーを取得する例です。
const getHeader = () => document.querySelector('header');
const debouncedFunction = debounce(getHeader)
window.addEventListener('resize', debouncedFunction, false);
const getHeader = () => document.querySelector('header');
window.addEventListener('resize', getHeader, false);
Intersection Observer
前項でも書いた通り、スクロールイベントは負荷が高いのであまり使いたくありません。Intersection Observerを使うことで、ブラウザに負荷をかけずにスクロールに応じた処理を実装できます。
以下はスクロールアニメーションのサンプルで、data-scroll-anima
属性を持つ要素が画面の上下20%の位置までスクロールされたら属性値がtrue
になります。
// 監視対象要素
const animaElements = document.querySelectorAll("[data-scroll-anima]");
// 交差時に実行される関数
const doWhenIntersect = entries => {
const entriesArray = Array.prototype.slice.call(entries, 0);
entriesArray.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.dataset.scrollAnima = 'true';
}
});
}
// IntersectionObserverのオプション
const options = {
root: null,
rootMargin: '-20% 0px -20% 0px', // 要素が画面の上下20%を超えたら監視する
threshold: 0
};
// 対象要素の数だけobserverで監視
const observer = new IntersectionObserver(doWhenIntersect, options);
animaElements.forEach((box) => {
observer.observe(box);
});
[data-scroll-anima] {
opacity: 0;
transition: opacity .3s;
}
[data-scroll-anima="true"] {
opacity: 1;
}
matchMedia
ブレイクポイントに応じて処理を実行する場合、画面幅をリサイズイベントで監視すると前項で書いた通りブラウザに負荷がかかるので、代わりにmatchMediaを使って今のブレイクポイントを判定します。
また、CSS変数にブレイクポイントを指定しておくことで、CSS側でブレイクポイントの値が変わってもJS側での修正は不要になります。
:root {
--breackpoint-md: 768px;
}
// ブレイクポイントの値をCSS変数から取得して、matchMediaにセット
const rootStyles = getComputedStyle(document.documentElement);
const breackpointMd = rootStyles.getPropertyValue('--breackpoint-md');
const mediaQueryList = window.matchMedia(`(max-width: ${breackpointMd})`);
// ブレイクポイントに応じて実行する処理
const mediaQueryFunction = (event) => {
if (event.matches) {
console.log('768px以下です');
} else {
console.log('769px以上です');
}
};
// ブレイクポイントが変わった時のイベントを登録
mediaQueryList.addEventListener('change', mediaQueryFunction);
// ページ読み込み時のイベントを登録
window.addEventListener('DOMContentLoaded', () => mediaQueryFunction(mediaQueryList));
Sassでブレイクポイントの変数を定義している場合、以下のようにCSS変数を登録をすれば上記と同じことができます。
$breackpoint-md: 768px;
:root {
--breackpoint-md: #{$breackpoint-md};
}
375px未満のレスポンシブ対応
幅320pxのような小さい端末のレスポンシブ対応はCSSで頑張るのではなく、Viewportで表示倍率を縮小します。
昨今のデザインは375pxで作られることが多く、そもそも320px程度まで考慮されていない場合が多いのでCSSで調整するには限界があります。なので、表示倍率を縮小することで実装工数が大幅に削減でき、大量のメディアクエリの記述も発生しなくなります。
const adjustViewport = () => {
const triggerWidth = 375;
const viewport = document.querySelector('meta[name="viewport"]');
const value = window.outerWidth < triggerWidth
? `width=${triggerWidth}, target-densitydpi=device-dpi`
: 'width=device-width, initial-scale=1';
viewport.setAttribute('content', value);
}
const debouncedFunction = debounce(adjustViewport) // debounce関数は、Debounceの項で解説した関数です
window.addEventListener('resize', debouncedFunction, false);
ES6以降の記法
JavaScriptはES6(ES2015)以降、便利な機能や構文が数多く追加されました。ここからはES6以降に追加されたWeb制作寄りの内容を少し紹介していきます。
文字列の結合
テンプレートリテラルを使うことで、変数と文字列の結合が楽になります。
const message = `私は${name}です。`;
const message = '私は' + name + 'です。';
配列操作
配列に関するメソッドはかなり追加されました。新しい配列を生成するmap
、特定の配列を探すfind
、配列の有無を確認するsome
など、これまではfor
文で行っていた処理をこれらのメソッドを使うことで記述量が圧倒的に短くなります。
// この中からidが2のデータを検索する
const users = [
{ id: 1, name: '山田' },
{ id: 2, name: '田中' },
{ id: 3, name: '中村' }
];
const targetUser = users.find(user => user.id === 2);
let targetUser;
for (let i = 0; i < users.length; i++) {
if (users[i].id === 2) {
targetUser = users[i];
break;
}
}
スプレッド構文
スプレッド構文を使うことで、配列やオブジェクトの結合や展開が楽になります。
let arr1 = [1, 2, 3];
let arr2 = [4, 5];
// 配列の結合
let combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5]
// 配列のコピー
let arrCopy = [...arr1]; // [1, 2, 3]
// 配列の結合
let combined = arr1.concat(arr2); // [1, 2, 3, 4, 5]
// 配列のコピー
let arrCopy = arr1.slice(); // [1, 2, 3]
Async / await
特定の処理の後に他の処理を実行する場合は Async / await を使います。setTimeout
で遅延させると、必ずしも遅延させた秒数で手前の処理が終わるとは限らないので絶対に辞めましょう。
// データを取得(取得に時間がかかる)
async function fetchData() {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
}
async function run() {
try {
const data = await fetchData(); // awaitを使うことでfetchData()が完了するまで次の行の処理を待つ
console.log('取得したデータ:', data);
} catch (error) {
console.error('エラーが発生しました:', error.message);
}
}
run();
function fetchData() {
// 同様の処理
}
async function run() {
const data = fetchData(); // fetchData()の完了を待たずに次の行を実行してしまう
// setTimeoutで処理を遅延させているが、fetchData()の完了が2000ミリ秒以内に終わる保証はないため、dataが空の状態でconsole.logが実行される可能性がある
setTimeout(() => {
console.log('取得したデータ:', data);
}, 2000);
}
run();
Fetch / Axios
APIなど外部からデータを取得する時はFetch
かAxios
を使います。昔はXMLHttpRequest
やjQueryのAjax
を使っていましたが、Fetch
やAxios
のほうが例外処理やデータの扱いに優れています。
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data');
xhr.onload = function() {
if (xhr.status === 200) {
console.log(xhr.responseText);
} else {
console.error('APIエラー');
}
};
xhr.onerror = function() {
console.error('ネットワークエラー');
};
xhr.send();
petamorikenさんからコメントをいただいたので追記です。
非同期処理には中断する処理が必要不可欠です。AbortController
を使い、タイムアウトしたら中断するような処理も同時に実装するようにしましょう。
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒後にAbortSignalを送信
fetch('https://api.example.com/data', {
signal: controller.signal
})
.then(response => {
clearTimeout(timeoutId); // タイムアウトをキャンセル
return response.json();
})
.then(data => {
console.log('Success:', data);
})
.catch(err => {
if (err.name === 'AbortError') {
console.log('Request timed out');
} else {
console.error('Error:', err);
}
});
最後に
かなりの量を紹介したので一度に全部を使いこなすのは難しいと思います。個人的にこれだけは...をいくつかピックアップしたので、まずはそれだけでも取り入れてみてください。
-
1. 画像
- Lazy loadingで遅延読み込みをして、画像の出し分けはPicture要素を使う
- 背景画像ではなくImg要素で読み込み、要素いっぱいに広げる時は
object-fit
、縦横比を制御するにはaspect-ratio
を使う
-
2. レイアウト
- 格子状のレイアウトはGrid Layoutを、Flexboxの間隔は
gap
を使う - 状況に応じて
calc()
とvw
を組み合わせてレイアウトを組む
- 格子状のレイアウトはGrid Layoutを、Flexboxの間隔は
-
3. JS最適化
- JSファイルは
Defer
で読み込み、処理はDOMContentLoaded
イベント内で行う - スクロールやリサイズのイベントは高負荷なので、Debounceで実行回数を間引く
- スクロール時の処理は
Intersection Observer
を使う
- JSファイルは
参考
こちらは普段私が情報をキャッチアップしている方々のサイトです。とても勉強になるので是非訪れてみてください。
Discussion
網羅的なまとめをありがとうございます。JavaScriptについて一部気になったためコメントさせてください。
Defer / DOMContentLoaded
これは
async
属性と混同しているように思います。defer
属性を付けたスクリプトはHTML文書解析後DOMContentLoaded
イベントの直前に実行されます。また特にモジュール機能を使わなかったとしても
type="module"
とすることで、誤ってグローバル変数を作ってしまうのを防ぐことができますね。おすすめします(外部ファイルとして読み込んだ場合はdefer
属性と同じタイミングで実行されます)。Fetch / Axios
補足になるのですが、Fetchのような非同期処理には中断する処理が不可欠だと思います。今ではWeb標準の
AbortController
/AbortSignal
を使うことができます。また最近では
EventTarget.prototype.addEventListener
のオプションにAbortSignal
を渡すことで一度に複数のハンドラーを削除することができます(Safari 15から使えます)。コメントありがとうございます!後日記事の内容を修正させていただきます。
完全にミスっていますね...。修正いたします。
type="module"
はたまに見かけるな〜くらいで全然気にしていなく、この機会にしっかりと理解できたのでよかったです。ありがとうございます。AbortController
も初めて知りました。タイムアウト用などに中断処理は確かに必須ですね。ファーストビューに含まれる画像にはつけないことをお勧めします。
LCPやFCPなどのスコアが悪化するだけでなく、実際の体感としても表示速度が遅くなったように感じる場合があります。
重要な画像はprefetchし、そうでない画像は遅延読み込みした方が良いです。
例示のコードだと初回(と前回の関数実行から300ms以上経過している状態)の実行時にも300msの遅延が発生します。
イベントで何かしらを描画する場合はアプリケーションが遅く感じる原因になります。
Good の例はイベントの登録、Bad の例は即時関数なので比較として妥当でないように思いました。
イベントの登録なら load イベントと比較したり、単なる関数なら記述する場所で比較するのはいかがでしょうか。
ご指摘ありがとうございます。
Lazy loadingとDebounceはどこまで細かく書くか悩ましいので検討させていただきます。
(この記事では網羅性を重視したかったので、細かく書きすぎるのもどうかなという葛藤がありまして...)
DOMContentLoadedは、私がdeferを付けたときの実行タイミングを完全に勘違いしていたので、表現方法などを考え直してみます。