Nuxt.js v3, Storybook v7の環境にWeb Fontsを追加する(Google Fonts, Adobe Fonts)
Nuxt.js v3の環境でWeb Fontsの導入を行なったため、導入方法について紹介します。 また、Storybookについても同様に追加したため、その方法についても紹介します。
前提
今回は以下のFontを追加していこうと思います。
Google Fonts
Adobe Fonts
また、使用するバージョンについても以下になります。
- Nuxt.js: 3.8.1
- Storybook: 7.5.3
Google Fontsの追加
1. Google Fontsの情報を取得
Google Fontsの該当サイトで以下のような設定を行います。
- 使用するフォントを選択
<link>
か@import
の選択ができるので、今回は<link>
を選択します。(@import
の方でも問題ありません。)- 「CSS rules to specify families」の部分をコピーしておきます。
2. Nuxt.jsに追加
注意: ここで紹介する方法よりも追記の方が適切なため、そちらをご覧ください。
直接読み込む方法を見てみる
nuxt.config.ts
のapp.head
のエリアに<link>
から取得してきた内容を記載することで使用することができます。
・nuxt.config.ts
export default defineNuxtConfig({ app: { head: { link: [ { rel: 'preconnect', href: 'https://fonts.googleapis.com', }, { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '', }, { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Manrope:wght@400;700&display=swap', }, ], }, }, })
追記
上記の方法でも問題なくできますが、@nuxtjs/google-fonts
のモジュールを使用する方がフォントをプリロードできたり・フォント周りをローカルのプロジェクトにダウンロードしてくれたりと便利なので、@nuxtjs/google-fonts
のモジュールを使用する方をお勧めします。
基本、以下の通りに行えば問題ありません。
パッケージのインストール
$ yarn add -D @nuxtjs/google-fonts
以下のコードを記載
・nuxt.config.ts
export default defineNuxtConfig({ modules: ['@nuxtjs/google-fonts'], googleFonts: { families: { Manrope: [400, 700], }, }, })
・該当のscssファイルなど
$fontFamily: 'Manrope', sans-serif; body { font-family: $fontFamily; }
3. Storybookに追加
storybookでGoogle Fontsの読み込みを行いたい場合、以下の設定を行うことで読み込まれるようになります。
・.storybook/preview-head.html
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;700&display=swap" rel="stylesheet">
・Storybook用のSCSSファイル
/* Nuxt.jsと共有のcssを使用している場合は記載を行わなくてもよいです。 */ $fontFamily: 'Manrope', sans-serif; body { font-family: $fontFamily; }
Adobe Fontsの追加
Adobe Fontsの詳細はこちらをご覧ください。
1. Adobe Fontsの情報を取得
webプロジェクトに追加
をクリック- 追加するプロジェクトを選択した後に、「決定」をクリック
- 出力される
script
やstyle
をそれぞれコピーしておきます。
2. Nuxt.jsに追加
参考記事
nuxt.config.ts
に以下を記載。
・nuxt.config.ts
export default defineNuxtConfig({ app: { head: { script: [ { src: 'https://use.typekit.net/{プロジェクト ID}.js', }, ], }, }, }
全体で読み込みを行なっているファイルに対して、以下を記載。
onMounted(() => { Typekit.load({ async: true }) })
・該当のscssファイルなど
$fontFamily: "kinto-sans", sans-serif; body { font-family: $fontFamily; }
3. Storybookに追加
storybookについてもGoogle Fontesの時とほぼ同様に以下のファイルにscriptを追記すればフォントが適応されます。
・.storybook/preview-head.html
<script src="https://use.typekit.net/{プロジェクト ID}.js"></script> <script async> Typekit.load({ async: true }) </script>
・該当のscssファイルなど
/* Nuxt.jsと共有のcssを使用している場合は記載を行わなくてもよいです。 */ $fontFamily: "kinto-sans", sans-serif; body { font-family: $fontFamily; }
Bunを使用していろいろなFWを作成してみる(Next.js, Nuxt.js, Astro)
Bunのv1がアップデートされたので、各種FWでどのように使うことができるのかいろいろと調べてみた内容になります。
今回使用したバージョン
- bun: 1.0.11
- Next.js: 14.0.2
- Nuxt.js: 3.8.1
- Astro: 3.5.3
Bunとは
Bun is an all-in-one toolkit for JavaScript and TypeScript apps. It ships as a single executable called bun. At its core is the Bun runtime, a fast JavaScript runtime designed as a drop-in replacement for Node.js. It's written in Zig and powered by JavaScriptCore under the hood, dramatically reducing startup times and memory usage.
DeepLでの翻訳ですが、日本語訳はこちらになります。
BunはJavaScriptとTypeScriptアプリのためのオールインワンのツールキットです。bunという単一の実行ファイルとして出荷される。 核となるのはBunランタイムで、Node.jsのドロップイン置き換えとして設計された高速なJavaScriptランタイムだ。Zigで記述され、JavaScriptCoreで動作するため、起動時間とメモリ使用量が劇的に削減される。
各種FWで検証する前に
Bunのインストール
以下のコマンドを実行して動いていれば完了です。
$ bun --version 1.0.11
Homebrewの場合
$ brew tap oven-sh/bun # for macOS and Linux
$ brew install bun
npmの場合
$ npm install -g bun # the last `npm` command you'll ever need
各種FWで検証をしてみる
今回検証をする内容以外にもいろいろなものがあるため、ぜひみてみてください。
1. Next.jsを作成してみる
以下のように設定しました
❯ bun create next-app ✔ What is your project named? … next-bun ✔ Would you like to use TypeScript? … No / Yes ✔ Would you like to use ESLint? … No / Yes ✔ Would you like to use Tailwind CSS? … No / Yes ✔ Would you like to use `src/` directory? … No / Yes ✔ Would you like to use App Router? (recommended) … No / Yes ✔ Would you like to customize the default import alias (@/*)? … No / Yes ✔ What import alias would you like configured? … ~/src/* Creating a new Next.js app in /code/bun-sample/next-bun. Using bun. Initializing project with template: app Installing dependencies: - react - react-dom - next Installing devDependencies: - typescript - @types/node - @types/react - @types/react-dom - eslint - eslint-config-next bun install v1.0.11 (f7f6233e) + @types/node@20.9.0 + @types/react@18.2.37 + @types/react-dom@18.2.15 + eslint@8.53.0 + eslint-config-next@14.0.2 + typescript@5.2.2 + next@14.0.2 + react@18.2.0 + react-dom@18.2.0 276 packages installed [256.00ms] Initialized a git repository. Success! Created next-bun at /code/bun-sample/next-bun
bun dev
でページを開くことができます。
❯ bun dev $ next dev ▲ Next.js 14.0.2 - Local: http://localhost:3000 (node:80104) ExperimentalWarning: The Fetch API is an experimental feature. This feature could change at any time (Use `node --trace-warnings ...` to show where the warning was created) ✓ Ready in 2.9s ○ Compiling / ... ✓ Compiled / in 2s (517 modules) ○ Compiling /favicon.ico ... ✓ Compiled /favicon.ico in 1070ms (513 modules)
http://localhost:3000
でページを開くことができます。
2. Nuxt.jsを作成してみる
以下のように設定しました。
❯ bunx nuxi init nuxt-bun # nuxt-bunはアプリ名 # 最初にどのパッケージを使用するかが出てくるため、bunを選択 ❯ Which package manager would you like to use? ○ npm ○ pnpm ○ yarn ● bun ✔ Which package manager would you like to use? bun ◐ Installing dependencies... bun install v1.0.11 (f7f6233e) + @nuxt/devtools@1.0.2 + nuxt@3.8.1 + vue@3.3.8 + vue-router@4.2.5 ✔ Types generated in .nuxt 671 packages installed [1.88s] ✔ Installation completed. ✔ Initialize git repository? Yes ℹ Initializing git repository... hint: Using 'master' as the name for the initial branch. This default branch name hint: is subject to change. To configure the initial branch name to use in all hint: of your new repositories, which will suppress this warning, call: hint: hint: git config --global init.defaultBranch <name> hint: hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and hint: 'development'. The just-created branch can be renamed via this command: hint: hint: git branch -m <name> Initialized empty Git repository in /code/bun-sample/nuxt-bun/.git/ ✨ Nuxt project has been created with the v3 template. Next steps: › cd nuxt-bun › Start development server with bun run dev
bun dev
でページを開くことができます。
❯ bun dev $ nuxt dev Nuxt 3.8.1 with Nitro 2.7.2 ➜ Local: http://localhost:3000/ ➜ Network: use --host to expose ➜ DevTools: press Shift + Option + D in the browser (v1.0.2) ℹ Vite server warmed up in 800ms ℹ Vite client warmed up in 996ms [nitro] ✔ Nitro built in 313 ms (node:81016) ExperimentalWarning: The Fetch API is an experimental feature. This feature could change at any time (Use `node --trace-warnings ...` to show where the warning was created)
http://localhost:3000
でページを開くことができます。
3. Astroを作成してみる
以下のように設定しました。
❯ bun create astro (node:81217) ExperimentalWarning: The Fetch API is an experimental feature. This feature could change at any time (Use `node --trace-warnings ...` to show where the warning was created) astro Launch sequence initiated. dir Where should we create your new project? ./astro-bun tmpl How would you like to start your new project? Include sample files ██████ Template copying... deps Install dependencies? Yes ██████ Installing dependencies with bun... ts Do you plan to write TypeScript? Yes use How strict should TypeScript be? Strict ██████ TypeScript customizing... git Initialize a new git repository? Yes ██████ Git initializing... next Liftoff confirmed. Explore your project! Enter your project directory using cd ./astro-bun Run bun run dev to start the dev server. CTRL+C to stop. Add frameworks like react or tailwind using astro add. Stuck? Join us at https://astro.build/chat ╭─────╮ Houston: │ ◠ ◡ ◠ Good luck out there, astronaut! 🚀 ╰─────╯
bun dev
でページを開くことができます。
❯ bun dev $ astro dev ◆ Astro collects completely anonymous usage data. This optional program helps shape our roadmap. Run `npm run astro telemetry disable` to opt-out. Details: https://astro.build/telemetry 🚀 astro v3.5.3 started in 66ms ┃ Local http://localhost:4321/ ┃ Network use --host to expose
http://localhost:4321
でページを開くことができます。
scssでfont-sizeの横幅に応じて可変するmixinを作ってみる
以下の記事やサービスでfont-sizeを入力するとそれに応じて出力はされますが、scssで使用する場合にはmixinで使用したいため、font-size調整用のmixinを作成した内容です。
min-max-calculator.9elements.com
結果
@use 'sass:math'; // 共通で使用しているscss変数 $fontSizeDefault: 16; // body要素で表示しているfont-size $sizeDefaultPc: 1440; // 最大ウィンドウサイズの指定 $sizeSm: 390; // 最小ウィンドウサイズの指定 // 小数点を丸めるために使用するmixin @function roundToDecimal($number, $decimalPlaces) { $multiplier: math.pow(10, $decimalPlaces); @return math.div(math.round($number * $multiplier), $multiplier); } // font-sizeの可変対応を行う用のmixin // $minFontPxSize: 最小ウィンドウサイズの時のフォントサイズ // $maxFontPxSize: 最大ウィンドウサイズの時のフォントサイズ // @see https://github.com/9elements/min-max-calculator/blob/main/src/components/Calculator/Calculator.svelte#L58 @mixin font-size($minFontPxSize, $maxFontPxSize) { $minFontRemSize: math.div($minFontPxSize, $fontSizeDefault); $maxFontRemSize: math.div($maxFontPxSize, $fontSizeDefault); $variablePart: math.div($maxFontPxSize - $minFontPxSize, $sizeDefaultPc - $sizeSm); $variableSize: roundToDecimal( math.div($maxFontPxSize - $sizeDefaultPc * $variablePart, $fontSizeDefault), 3 ); font-size: clamp( #{$minFontRemSize}rem, #{$variableSize}rem + #{roundToDecimal($variablePart, 4) * 100}vw, #{$maxFontRemSize}rem ); } // 使い方 .hoge { @include font-size(16, 24); // font-size: clamp(1rem, 0.814rem + 0.76vw, 1.5rem); }
参考にしたコード
基本以下のコードを参考にscss用に変換したものになります。
scssでの計算の処理について今回使用したもの
割り算を行う
@use 'sass:math'; @debug math.div(1, 2); // 0.5 @debug math.div(100px, 5px); // 20 @debug math.div(100px, 5); // 20px
べき乗の計算を行う(10のべき乗を行い桁数を追加する)
@use 'sass:math'; @debug math.pow(10, 2); // 100 @debug math.pow(10, 3); // 1000
丸め込みを行う
@use 'sass:math'; @debug math.round(4); // 4 @debug math.round(4.2); // 4 @debug math.round(4.9); // 5
cssを使って画像を立体的に見せる方法
X(旧Twitter)で以下のものが流れてきて実装方法など気になったので、それを実装してみた内容になります。
A realistic 3D Card hover effect using only CSS 🤩
— T. Afif @ CSS Challenges (@ChallengesCss) 2023年5月9日
Demo: https://t.co/YFuQiTGH6r via @CodePen
✅ Minimal HTML
✅ Responsive
✅ Featuring the best video game ever
Inspired by @gggayane 👇#CSS https://t.co/2sZEwS09Rm pic.twitter.com/x2ALDjH4XA
実装を行ったもの
See the Pen hero-card by miwa_shuntaro (@miwashutaro0611) on CodePen.
<!-- @see https://codepen.io/t_afif/pen/mdzxJaa --> <figure class="hero"> <!-- NOTE: change image path --> <!-- https://assets.codepen.io/1480814/necro.png --> <img class="hero-image" src="https://placehold.jp/437x1000.png" alt="character image"> <figcaption class="hero-figcaption">character1</figcaption> </figure> <figure class="hero"> <img class="hero-image" src="https://placehold.jp/437x1000.png" alt="character image"> <figcaption class="hero-figcaption">character2</figcaption> </figure>
今回CSSについては「CSS nesting」を使用しているため、FireFoxでは動かない可能性があります。 (Mac & Chromeでは動作することを確認済みです。)
/* @see https://codepen.io/t_afif/pen/mdzxJaa */ /* reset css */ *, *::before, *::after { margin: 0; padding: 0; } body { width: 100%; min-height: 100vh; display: grid; grid-auto-flow: column; grid-auto-columns: min(230px,35vmin); place-content: end center; gap: 50px; } /* content css */ :root { --time: .5s } .hero { width: 100%; aspect-ratio: 1; margin: 0 0 60px; padding: 5px 20px 0; display: grid; grid-template-rows: 100%; cursor: pointer; position: relative; filter: drop-shadow(0 0 20px rgb(0 0 0/50%)); &::before { content: ""; position: absolute; z-index: -1; inset: 0; background: top/cover; transform-origin: bottom; filter: brightness(.9); transition: var(--time); background-image: url('https://placehold.jp/97e605/ffffff/680x980.png') } &:hover::before { filter: brightness(.3); transform: perspective(500px) rotateX(60deg); } } .hero-image { grid-area: 1/1; width: 100%; height: 100%; object-fit: cover; object-position: top; filter: contrast(.8) brightness(.7); place-self: end center; transition: var(--time); .hero:hover & { width: 130%; height: 255%; filter: contrast(1); } } .hero-figcaption { grid-area: 1/1; width: calc(100% + 40px); color: #fff; font-size: min(32px,5vmin); text-align: center; place-self: end center; transform: perspective(500px) translateY(100%) rotateX(-90deg); backface-visibility: hidden; transform-origin: top; background: #000; transition: var(--time); .hero:hover & { transform: perspective(500px)translateY(100%) rotateX(-30deg); } }
制作手順
今回使用している画像としては以下になります。
1. HTMLの作成
キャラクター用の画像
キャラクターの背景にある画像
<figure class="hero"> <!-- NOTE: change image path --> <img class="hero-image" src="https://placehold.jp/437x1000.png" alt="character image"> <figcaption class="hero-figcaption">character1</figcaption> </figure>
こちらは通常の画像で使うHTMLと同じになります。
画像については、ホバー後の全体のものを使用したいため、縦長の画像を使用しています。
今回使用している画像サイズについては437x1000
のものになります。
2. 背景画像部分の表示
- 画像の縦横比を1:1に変更
- 影を使用して立体感の演出
- 画像を少しだけ暗くする
画像の縦横比を1:1に変更
要素に対して、aspect-ratio
を設置することで横幅を設定するだけで高さも比率に合わせて調整してくれます。
影を使用して立体感の演出
画像の部分に影を適応させていきたいので、drop-shadow
を使用しています。
(今回検証で制作したものについては透過されていない画像なので、box-shadow
でも問題はないと思います。)
box-shadowとdrop-shadowの違いについては以下の記事が参考になります。
画像を少しだけ暗くする
色味についても、少し落とした感じにしたいため、brightness
を使用しています。
今回のcssの内容について
.hero { width: 100%; aspect-ratio: 1; margin: 0 0 60px; padding: 5px 20px 0; display: grid; grid-template-rows: 100%; cursor: pointer; position: relative; filter: drop-shadow(0 0 20px rgb(0 0 0/50%)); &::before { content: ""; position: absolute; z-index: -1; inset: 0; background: top/cover; transform-origin: bottom; filter: brightness(.9); transition: var(--time); background-image: url('https://placehold.jp/97e605/ffffff/680x980.png') } }
3. img
タグで表示した画像の表示エリアの調整
- gridのエリアに縦1・横1の割合で配置を行う
img
タグで配置した画像のサイズ・配置箇所の調整- コントラスト・明るさの調整
gridのエリアに縦1・横1の割合で配置を行う
親要素で指定したgrid
の要素に対してどの場所に配置を行うか指定を行う際に使用します。
今回の場合、親要素のgrid
全体に対して表示を行うため、1/1
のように記載しています。
img
タグで配置した画像のサイズ・配置箇所の調整
img
タグに対して、以下を使用することで
object-fit: コンテンツと画像の比率が合わない場合、全体を見えるように・横幅いっぱいなどの指定を行うことができます。 object-position: 画像の配置する場所を配置場所を指定することができます。
コントラストの調整
画像のコントラストを調整することができます。
今回のcssの内容について
.hero-image { grid-area: 1/1; width: 100%; height: 100%; object-fit: cover; object-position: top; filter: contrast(.8) brightness(.7); place-self: end center; transition: var(--time); }
4. caption
タグのスタイルの調整
- gridのエリアに縦1・横1の割合で配置を行う(イメージと同じ箇所に配置を行う)
- コンテンツの位置調整
- 奥行きの設定
- コンテンツの裏側を見せないようにする
gridのエリアに縦1・横1の割合で配置を行う(イメージと同じ箇所に配置を行う)
親要素で指定したgrid
の要素に対してどの場所に配置を行うか指定を行う際に使用します。
今回の場合、親要素のgrid
全体に対して表示を行うため、1/1
のように記載しています。
girdでの表示の場合、同じエリアに配置することでposition: absolute
と同じようなことを行うことができます。
詳細については以下の記事を参考にしてください。
コンテンツの位置調整
gridの中で要素をどの位置に表示を行うか指定することができます。
奥行きの設定
perspective
を追加すると3次元での表現を行うことができます。
コンテンツの裏側を見せないようにする
要素が立体的な時にbackface-visibility: hidden;
を指定することで裏側にある要素を隠すことができます。
今回のcssの内容について
.hero-figcaption { grid-area: 1/1; width: calc(100% + 40px); color: #fff; font-size: min(32px,5vmin); text-align: center; place-self: end center; transform: perspective(500px) translateY(100%) rotateX(-90deg); backface-visibility: hidden; transform-origin: top; background: #000; transition: var(--time); }
5. 2から4で作成したものに対して、ホバーのインタラクションをつける
設定した各種各種内容について
hero全体で行っていることについて
背景画像用として作成して擬似要素の奥行きを追加し、要素の回転を行う
heroの画像で行っていることについて
コントラストを通常に戻して、元のサイズの画像の表示を行う
heroのキャプションで行っていることについて
位置を移動させて、要素を回転させる
今回のcssの内容について
.hero { &:hover::before { filter: brightness(.3); transform: perspective(500px) rotateX(60deg); } } .hero-image { .hero:hover & { width: 130%; height: 255%; filter: contrast(1); } } .hero-figcaption { .hero:hover & { transform: perspective(500px) translateY(100%) rotateX(-30deg); } }
AstroのView Transitionsを試してみる
Astro v2.9からページ遷移のアニメーションをAstroでつけることができるようになったため、少し検証してみた内容になります。
ページ全体に遷移アニメーションを追加したい場合
コード
- astro.config.mjs
export default defineConfig({ // experimentalを追加 experimental: { viewTransitions: true, }, })
- ページ共通の場所
--- import { ViewTransitions } from 'astro:transitions' --- <!DOCTYPE html> <html lang="ja"> <head> <!-- headの中に「ViewTransitions」を追加 --> <ViewTransitions /> </head> <body> <!-- なにかのコンテンツ --> </body> </html>
結果
ページ遷移しても状態を保持したい場合
コード
以下のコンポーネントを新規作成して、ページ全体の箇所に追加しています。
<video controls="" autoplay="" transition:persist> <source src="https://ia804502.us.archive.org/33/items/GoldenGa1939_3/GoldenGa1939_3_512kb.mp4" type="video/mp4"> </video>
結果
ページ遷移時にスライドさせる
コード
<!-- アニメーションさせたいコンテンツにたいして、`transition:animate="slide"`を追加 --> <body transition:animate="slide"> <!-- なにかのコンテンツ --> </body>
結果
実務で使うツール・情報収集しているものについて紹介
実務で使っているツールや普段の情報収集で使っているものになります。
基本個人のメモ用でフロントエンドエンジニア向けです。
実務で使っているツール
SassMeister
オンライン上でsassからcssの変換ができるツール。
基本css-modulesを使用しているため、プロジェクトでのビルドとしては使用していませんが、以下のような&
を記載した際のcssでの挙動やその他どのようなsassで記載する際にどのようにcssが表示されるかの検証として使用しています。
placehold.jp
ダミー画像を生成してくれるツール。
https://placehold.jp/150x150.png
のように記載してくれるとサイズが表示された画像を表示してくれます。
caninclude
親要素、子要素に入れる要素を入力するとその要素がHTMLセマンティックとして問題ないか検証してくれるツール。
ちなみに以下のようなマークアップの場合、ちゃんと警告も出してくれます。
<p> <div></div> <p>
runjs
JavaScriptで記載した内容を検証してくれるツール。
Node.jsでの検証やブラウザのdevtoolを使うほどではないけど、変数の中身がどのようになっているのかを検証したい時に役に立ちます。
screensizes
iPhone, iPadの画面サイズの一覧を表示してくれるサイト。
普段のWeb制作時にiPhoneの画面サイズを一覧形式で見ることができるので、各種デバイスの違い等もわかりやすいです。また、スクリーンの倍率や発売された時の倍率も教えてくれるため、iPhoneの情報を調べるのにも役に立ちます。
各種整形ツール
各種出力されたデータを整形や変換をする際にはこちらのツールを使用しています。
情報収集しているもの
ギャラリーサイト
日常的に見るギャラリーサイトはこの4つを見ています。
Youtube, Podcast
タブUIをちゃんと理解する
タブのUIについて実装する機会はちょこちょこあるものの、ちゃんと理解しようと思い調べた内容になります。
参考にしたもの
今回実装したものについて
コードについて
<div class="tab"> <ul class="tab-list" role="tablist"> <li class="tab-item" role="presentation"> <button type="button" class="tab-button js-button-1 is-active" aria-controls="panel1" aria-selected="true" role="tab" id="tab1" > タブ1 </button> </li> <li class="tab-item" role="presentation"> <button type="button" class="tab-button js-button-2" aria-controls="panel2" aria-selected="false" role="tab" id="tab2" > タブ2 </button> </li> <li class="tab-item" role="presentation"> <button type="button" class="tab-button js-button-3" aria-controls="panel3" aria-selected="false" role="tab" id="tab3" > タブ3 </button> </li> </ul> <div class="tab-panel js-pannel" id="panel1" role="tabpanel" aria-labelledby="tab1"> <p>パネル1</p> <p><a href="#">リンク1</a></p> </div> <div class="tab-panel js-pannel" id="panel2" role="tabpanel" aria-labelledby="tab2" hidden> <p>パネル2</p> <p><a href="#">リンク2</a></p> </div> <div class="tab-panel js-pannel" id="panel3" role="tabpanel" aria-labelledby="tab3" hidden> <p>パネル3</p> <p><a href="#">リンク3</a></p> </div> </div>
.tab { width: min(500px, 100%); } .tab-list { display: flex; } .tab-item + .tab-item { margin-left: 2px; } .tab-button { padding: 8px 16px; background: #fff; border-radius: 4px 4px 0 0; font-weight: bold; border-bottom: 1px solid #ddd; } .tab-button.is-active { color: #fff; background: #87ceeb; } .tab-panel { min-height: 100px; padding: 24px; background: #fff; }
// 今回使用するtab部分の要素を取得 const elemTabButton1 = document.querySelector('.js-button-1') const elemTabButton2 = document.querySelector('.js-button-2') const elemTabButton3 = document.querySelector('.js-button-3') // aria-controlsに記載されている内容を元に変更するid名を取得 const getIdName = (buttonElem) => { const idName = buttonElem.getAttribute('aria-controls') return idName } // クリックした時のイベント処理 const clickEvent = (elem) => { // クリック前にアクティブな要素を見つけて、activeを削除する const elemActiveButton = document.querySelector('button.is-active') if(elemActiveButton) { const activeIdName = getIdName(elemActiveButton) const elemActivePanel = document.getElementById(activeIdName) elemActiveButton.classList.remove('is-active') elemActiveButton.setAttribute('aria-selected', 'false') elemActivePanel.hidden = true } // クリックした要素に対して、activeにする const clickIdName = getIdName(elem) const elemclickPanel = document.getElementById(clickIdName) elem.classList.add('is-active') elem.setAttribute('aria-selected', 'true') elemclickPanel.hidden = false } // 各要素をクリックした時に関数を実行 elemTabButton1.addEventListener('click', () => { clickEvent(elemTabButton1) }, false) elemTabButton2.addEventListener('click', () => { clickEvent(elemTabButton2) }, false) elemTabButton3.addEventListener('click', () => { clickEvent(elemTabButton3) }, false)
実装した手順について
1. マークアップの作成
1-1. タブの全体を作成
<div class="tab"> <!-- タブの一覧 --> <ul class="tab-list" role="tablist"> <li class="tab-item" role="presentation"> <button type="button" class="tab-button js-button-1 is-active" aria-controls="panel1" aria-selected="true" role="tab" id="tab1" > タブ1 </button> </li> </ul> <!-- タブのパネル --> <div class="tab-panel js-pannel" id="panel1" aria-labelledby="tab1" role="tabpanel">パネル</div> </div>
1-2. タブの選択部分を作成
<ul class="tab-list" role="tablist"> <li class="tab-item" role="presentation"> <button type="button" class="tab-button js-button-1 is-active" aria-controls="panel1" aria-selected="true" role="tab" id="tab1" > タブ1 </button> </li> <li class="tab-item" role="presentation"> <button type="button" class="tab-button js-button-2" aria-controls="panel2" aria-selected="false" role="tab" id="tab2" > タブ2 </button> </li> <li class="tab-item" role="presentation"> <button type="button" class="tab-button js-button-3" aria-controls="panel3" aria-selected="false" role="tab" id="tab3" > タブ3 </button> </li> </ul>
対応した内容について
role-tablistについて
tablist
をHTMLに宣言することで、tab
のroleのものを紐付ける場合に使用します。
タブの1つ目にフォーカスを当てている場合
タブの3つ目にフォーカスを当てている場合
role-presentationについて
今回の場合、マークアップの見やすさからul>li
のものを使用していますが、「リストアイテム」等の読み込みが不要なため、presentation
を宣言しています。
role-tabについて
tablist
とtab
のroleを組み合わせることで要素の関連を結びつけることができるようになったり、VoiceOver等で「タブ」として認識されるようになります。(イメージについてはtablist
で共有した画像を参照)
aria-controlsについて
タブとタブパネルを結びつけるために宣言しています。
aria-selectedについて
タブの要素に対して、選択中かそうでないかをスクリーンリーダーに使えるために宣言しています。
1-3. タブの表示部分の作成
<div class="tab-panel js-pannel" id="panel1" role="tabpanel" aria-labelledby="tab1"> <p>パネル1</p> <p><a href="#">リンク1</a></p> </div> <div class="tab-panel js-pannel" id="panel2" role="tabpanel" aria-labelledby="tab2" hidden> <p>パネル2</p> <p><a href="#">リンク2</a></p> </div> <div class="tab-panel js-pannel" id="panel3" role="tabpanel" aria-labelledby="tab3" hidden> <p>パネル3</p> <p><a href="#">リンク3</a></p> </div>
対応した内容について
tabpanelについて
タブで表示されるコンテンツ部分をVoiceOver等で「タブパネル」のコンテンツと知らせるために宣言しています。
aria-labelledbyについて
どのタブパネルの要素が表示されているのかをタブの要素と結びつけることで以下のようにスクリーンリーダーが反応してくれるため、宣言しています。 aria-labelでも同じように読み込んでくれますが、html要素とaria-labelの2つを編集する必要があるため、aria-labelledbyの方を使用しています。
- aria-labelledbyなしの場合:
リンク、リンク1 、タブ1、タブパネル - aria-labelledbyありの場合:
リンク、リンク1 - aria-labelで
<div class="tab-panel js-pannel" id="panel1" role="tabpanel" aria-label="タブ1">{他と同じもの}</div>
のように使用した場合:
リンク、リンク1 、タブ1、タブパネル
hidden属性について
tab
で選択されていない要素のコンテンツの場合、ブラウザ上に表示・スクリーンリーダーでも読み込みは行われてほしくないため、非表示のコンテンツに対してはhidden
を宣言しています。
2. スタイルの追加
こちらは基本的なタブUIを実装したのみで、特別なことをしていないため、手順等は省略します。
3. タブとして動作をするようにする
今回のタブの要素を取得・クリックイベントの登録
// 今回使用するtab部分の要素を取得 const elemTabButton1 = document.querySelector('.js-button-1') const elemTabButton2 = document.querySelector('.js-button-2') const elemTabButton3 = document.querySelector('.js-button-3') // 各要素をクリックした時に関数を実行 elemTabButton1.addEventListener('click', () => { // ここにクリックしたときの関数を記載(今回は「clickEvent」の関数名) }, false) elemTabButton2.addEventListener('click', () => { // ここにクリックしたときの関数を記載(今回は「clickEvent」の関数名) }, false) elemTabButton3.addEventListener('click', () => { // ここにクリックしたときの関数を記載(今回は「clickEvent」の関数名) }, false)
現在activeなものを初期化する
const elemActiveButton = document.querySelector('button.is-active') if(elemActiveButton) { const activeIdName = getIdName(elemActiveButton) const elemActivePanel = document.getElementById(activeIdName) elemActiveButton.classList.remove('is-active') elemActiveButton.setAttribute('aria-selected', 'false') elemActivePanel.hidden = true }
ここの箇所で行っている内容については以下になります。
- アクティブな要素(
button.is-active
)を見つけて、存在したら以下を実行 - 現在アクティブなボタンのclass名から
is-active
の削除 → スタイルの変更をするため - 現在アクティブなボタンの
aria-selected
をfalseにする → スクリーンリーダーで選択中の要素でないことを知らせるため - 現在アクティブなパネルを非表示にする
クリックしたものに対して、activeにする
// クリックした要素に対して、activeにする const clickIdName = getIdName(elem) const elemclickPanel = document.getElementById(clickIdName) elem.classList.add('is-active') elem.setAttribute('aria-selected', 'true') elemclickPanel.hidden = false
ここの箇所で行っている内容については以下になります。
- クリックしたボタンのclass名から
is-active
の追加 → スタイルの変更をするため - クリックしたボタンの
aria-selected
をtrueにする → スクリーンリーダーで選択中の要素であることを知らせるため - クリックした要素に紐づくパネルを表示にする