jackmiwamiwa devblog

フロントエンドをメインに行ったことをまとめていくサイトになります。

Nuxt.js v3, Storybook v7の環境にWeb Fontsを追加する(Google Fonts, Adobe Fonts)

Nuxt.js v3の環境でWeb Fontsの導入を行なったため、導入方法について紹介します。 また、Storybookについても同様に追加したため、その方法についても紹介します。

前提

今回は以下のFontを追加していこうと思います。

Google Fonts

fonts.google.com

Adobe Fonts

fonts.adobe.com

また、使用するバージョンについても以下になります。

  • Nuxt.js: 3.8.1
  • Storybook: 7.5.3

Google Fontsの追加

1. Google Fontsの情報を取得

Google Fontsの該当サイトで以下のような設定を行います。

fonts.google.com

  1. 使用するフォントを選択
  2. <link>@importの選択ができるので、今回は<link>を選択します。(@importの方でも問題ありません。)
  3. CSS rules to specify families」の部分をコピーしておきます。

2. Nuxt.jsに追加

注意: ここで紹介する方法よりも追記の方が適切なため、そちらをご覧ください。

直接読み込む方法を見てみる

nuxt.config.tsapp.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のモジュールを使用する方をお勧めします。

github.com

google-fonts.nuxtjs.org

基本、以下の通りに行えば問題ありません。

google-fonts.nuxtjs.org

パッケージのインストール

$ 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.js.org

.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の詳細はこちらをご覧ください。

www.adobe.com

1. Adobe Fontsの情報を取得

  1. webプロジェクトに追加をクリック
  2. 追加するプロジェクトを選択した後に、「決定」をクリック
  3. 出力されるscriptstyleをそれぞれコピーしておきます。

fonts.adobe.com

2. Nuxt.jsに追加

参考記事

zenn.dev

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.sh

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

bun.sh

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で検証をしてみる

今回検証をする内容以外にもいろいろなものがあるため、ぜひみてみてください。

bun.sh

1. Next.jsを作成してみる

bun.sh

以下のように設定しました

❯ 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を作成してみる

bun.sh

以下のように設定しました。

❯ 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.sh

以下のように設定しました。

❯ 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を作成した内容です。

coliss.com

www.fluid-type-scale.com

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用に変換したものになります。

github.com

scssでの計算の処理について今回使用したもの

sass-lang.com

割り算を行う

sass-lang.com

@use 'sass:math';

@debug math.div(1, 2); // 0.5
@debug math.div(100px, 5px); // 20
@debug math.div(100px, 5); // 20px

べき乗の計算を行う(10のべき乗を行い桁数を追加する)

sass-lang.com

@use 'sass:math';

@debug math.pow(10, 2); // 100
@debug math.pow(10, 3); // 1000

丸め込みを行う

sass-lang.com

@use 'sass:math';

@debug math.round(4); // 4
@debug math.round(4.2); // 4
@debug math.round(4.9); // 5

cssを使って画像を立体的に見せる方法

X(旧Twitter)で以下のものが流れてきて実装方法など気になったので、それを実装してみた内容になります。

実装を行ったもの

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では動作することを確認済みです。)

developer.mozilla.org

caniuse.com

/* @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の作成

キャラクター用の画像

https://placehold.jp/437x1000.png

キャラクターの背景にある画像

https://placehold.jp/97e605/ffffff/680x980.png

<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を設置することで横幅を設定するだけで高さも比率に合わせて調整してくれます。

developer.mozilla.org

影を使用して立体感の演出

画像の部分に影を適応させていきたいので、drop-shadowを使用しています。 (今回検証で制作したものについては透過されていない画像なので、box-shadowでも問題はないと思います。)

developer.mozilla.org

box-shadowとdrop-shadowの違いについては以下の記事が参考になります。

ics.media

画像を少しだけ暗くする

色味についても、少し落とした感じにしたいため、brightnessを使用しています。

developer.mozilla.org

今回の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のように記載しています。

developer.mozilla.org

imgタグで配置した画像のサイズ・配置箇所の調整

imgタグに対して、以下を使用することで

object-fit: コンテンツと画像の比率が合わない場合、全体を見えるように・横幅いっぱいなどの指定を行うことができます。 object-position: 画像の配置する場所を配置場所を指定することができます。

developer.mozilla.org

developer.mozilla.org

コントラストの調整

画像のコントラストを調整することができます。

developer.mozilla.org

今回の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のように記載しています。

developer.mozilla.org

girdでの表示の場合、同じエリアに配置することでposition: absoluteと同じようなことを行うことができます。 詳細については以下の記事を参考にしてください。

coliss.com

コンテンツの位置調整

gridの中で要素をどの位置に表示を行うか指定することができます。

developer.mozilla.org

奥行きの設定

perspectiveを追加すると3次元での表現を行うことができます。

developer.mozilla.org

コンテンツの裏側を見せないようにする

要素が立体的な時にbackface-visibility: hidden;を指定することで裏側にある要素を隠すことができます。

developer.mozilla.org

今回の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でつけることができるようになったため、少し検証してみた内容になります。

docs.astro.build

ページ全体に遷移アニメーションを追加したい場合

コード

  • 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>

結果

サンプルページへ

ページ遷移しても状態を保持したい場合

コード

docs.astro.build

以下のコンポーネントを新規作成して、ページ全体の箇所に追加しています。

<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

www.sassmeister.com

オンライン上でsassからcssの変換ができるツール。

基本css-modulesを使用しているため、プロジェクトでのビルドとしては使用していませんが、以下のような&を記載した際のcssでの挙動やその他どのようなsassで記載する際にどのようにcssが表示されるかの検証として使用しています。

placehold.jp

placehold.jp

ダミー画像を生成してくれるツール。

https://placehold.jp/150x150.pngのように記載してくれるとサイズが表示された画像を表示してくれます。

https://placehold.jp/150x150.png

caninclude

caninclude.glitch.me

親要素、子要素に入れる要素を入力するとその要素がHTMLセマンティックとして問題ないか検証してくれるツール。

ちなみに以下のようなマークアップの場合、ちゃんと警告も出してくれます。

<p>
   <div></div>
<p>

runjs

runjs.app

JavaScriptで記載した内容を検証してくれるツール。

Node.jsでの検証やブラウザのdevtoolを使うほどではないけど、変数の中身がどのようになっているのかを検証したい時に役に立ちます。

screensizes

screensizes.app

iPhone, iPadの画面サイズの一覧を表示してくれるサイト。

普段のWeb制作時にiPhoneの画面サイズを一覧形式で見ることができるので、各種デバイスの違い等もわかりやすいです。また、スクリーンの倍率や発売された時の倍率も教えてくれるため、iPhoneの情報を調べるのにも役に立ちます。

各種整形ツール

lab.syncer.jp

lab.syncer.jp

lab.syncer.jp

tech-unlimited.com

各種出力されたデータを整形や変換をする際にはこちらのツールを使用しています。

情報収集しているもの

ギャラリーサイト

bm.s5-style.com

muuuuu.org

sankoudesign.com

www.aaa11y.com

日常的に見るギャラリーサイトはこの4つを見ています。

Youtube, Podcast

www.youtube.com

www.youtube.com

normalize.fm

rebuild.fm

open.spotify.com

uit-inside.linecorp.com

仕事の時のBGMとして定期的にこちらのYoutube, Podcastを聴いています。

タブUIをちゃんと理解する

タブのUIについて実装する機会はちょこちょこあるものの、ちゃんと理解しようと思い調べた内容になります。

参考にしたもの

qiita.com

kt-media.blog

ics.media

今回実装したものについて

コードについて

  <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について

developer.mozilla.org

tablistをHTMLに宣言することで、tabのroleのものを紐付ける場合に使用します。

タブの1つ目にフォーカスを当てている場合

タブの3つ目にフォーカスを当てている場合

role-presentationについて

developer.mozilla.org

今回の場合、マークアップの見やすさからul>liのものを使用していますが、「リストアイテム」等の読み込みが不要なため、presentationを宣言しています。

role-tabについて

developer.mozilla.org

tablisttabのroleを組み合わせることで要素の関連を結びつけることができるようになったり、VoiceOver等で「タブ」として認識されるようになります。(イメージについてはtablistで共有した画像を参照)

aria-controlsについて

developer.mozilla.org

タブとタブパネルを結びつけるために宣言しています。

aria-selectedについて

developer.mozilla.org

タブの要素に対して、選択中かそうでないかをスクリーンリーダーに使えるために宣言しています。

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について

developer.mozilla.org

タブで表示されるコンテンツ部分をVoiceOver等で「タブパネル」のコンテンツと知らせるために宣言しています。

aria-labelledbyについて

developer.mozilla.org

どのタブパネルの要素が表示されているのかをタブの要素と結びつけることで以下のようにスクリーンリーダーが反応してくれるため、宣言しています。 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属性について

developer.mozilla.org

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にする → スクリーンリーダーで選択中の要素であることを知らせるため
  • クリックした要素に紐づくパネルを表示にする