12周年記念パーティ開催! 2024/5/10(金) 19:00

FlexboxとCSS Grid、実装ケーススタディ 前編 メッセージログのUIの大枠を組み立てる

メッセージログのUIをHTMLとCSSで組み立ててみます。みなさんもUIのデザインと仕様から自分ならどう実装するか、一度考えてみてください。

発行

著者 坂巻 翔大郎 フロントエンド・エンジニア
FlexboxとCSS Grid、実装ケーススタディ シリーズの記事一覧

はじめに

本シリーズでは、とあるUIを題材にして、どういったHTMLとCSSを書くと良いかを考えます。最初の題材「メッセージログ」のUIです。

昨今の複数人が参加してやり取りするようなウェブアプリでは、アプリ上でメッセージのやり取りができることがほとんどです。本記事では、そのやり取りの履歴が残るUIのことを、「メッセージログのUI」と呼びます。そのUIには、どのようなHTMLとCSSが適しているのかを解説していきます。

本記事で解説するものは、あくまで筆者の体験や仕様に基づいて考えているHTMLとCSSですから、SlackやChatworkといったメッセージログUIを持つ既存のアプリとは、HTML・CSSが大きく異なる場合があります。

なお、このケーススタディではFlexbox、CSS Gridの基本的な知識を前提に書かれています。コードを追えば、やっていること自体はわかると考えますが、知らないプロパティがあった場合などは以下のシリーズも適宜、参考にしながら読み進めてみてください。

作りたいUIのデザイン

まずは簡素なものですが、メッセージログのUIのデザインを用意しました。

このUIの仕様を定義しておきます。

  • メッセージ投稿者を示す画像・投稿者の名前・本文を1組にしたものを「メッセージ」と呼ぶ
  • 高さは固定で、全体の高さは524px
  • 「ログ」と書かれたメッセージログのヘッダ部分の高さは可変で、「ログ」の部分は「〜〜ログ」などに変わったり多言語化により長くなり複数行になる場合もある
  • メッセージが画像右の赤い枠からあふれる場合は、縦のスクロールバーを表示し、スクロールすることで全件読めること
  • 新しいメッセージは一番下に追加される
  • HTMLの要素の並び順と、見た目上の並び順が異なっても良い

外側から作り込む

まずは、外側の部分から作り込んでいきます*。ヘッダとメッセージらが入る部分です。

* 外側の部分から作り込んでいきます

内側のメッセージの部分から作っても構いませんが、普段の筆者は外側から作ることが多いです。

まずは外側から組み立てる

<div class="MessageLog">
    <h2 class="MessageLog_Head">ログ</h2>
    <div class="MessageLog_Body">
         <!-- ここにメッセージらが並ぶ... -->
    </div>
</div>

HTMLとしては特筆することはないですが、h2としている部分は文脈によって見出しレベルを変更します。h3が適切なときもありますし、h1が適切なこともあるでしょう。

次にCSSです。

Flexboxを使用して枠組みを作る

.MessageLog {
  display: flex;
  flex-direction: column;
  width: 300px;
  height: 524px;
  border-radius: 8px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
  overflow: hidden;
}
.MessageLog_Head {
  flex: 0 0 auto;
  margin: 0;
  padding: 10px;
  font-size: 18px;
  line-height: 1.2;
  background-color: #eee;
}
.MessageLog_Body {
  flex: 1 1 auto;
  overflow: auto;
  background-color: tomato; /* 一時的に確認のため色をつけておく */
}

.MessageLogをフレックスコンテナにし、全体の高さを524pxに固定しています。ヘッダとメッセージが入る部分は縦に並ぶのでflex-direction: column;としています。

このケースではFlexboxを使った

UIの仕様にあるとおり、ヘッダは高さが可変ですので、高さは指定せず、ヘッダのテキストが長くなり折り返す場合はそのぶんだけ高さが大きくなります。ヘッダ部分はflex: 0 0 auto;ですから余りの空間の有無に関わらず伸びたり縮んだりはしません。

ヘッダ部分(抜粋)

.MessageLog_Head {
  flex: 0 0 auto;
  /* 省略 */
}

メッセージが入る部分はflex: 1 1 auto;ですから、余りの空間があればあるだけ伸びますし、なければ縮みます。こうすることでメッセージが入る部分は、「全体の高さ」から「ヘッダの分の高さ」を除いた高さになります。

メッセージ部分(抜粋)

.MessageLog_Body {
  flex: 1 1 auto;
  /* 省略 */
}

デモでは、メッセージが入る部分に確認用の色を付けており、色のついた部分がヘッダの高さを除いた高さいっぱいになっている様子が確認できます。

採用したFlexboxの実装とFlexbox以外の方法の検討

ここで少し寄り道して、Flexbox以外でも大まかな枠組みを組み立てられないかも併せて検討してみましょう。実務でも要件に合わせて柔軟に考えられるよう、常にいくつかの方法と、それらの方法のできること、できないことを思い浮かべておくことは大切です。

Flexboxを使わないでもできる?(採用しなかった方法)

たとえばFlexboxを使わないでやろうとすると、次のようなCSSが考えられるかもしれません。

Flexboxを使わないで高さいっぱいにする

.MessageLog {
  width: 300px;
  height: 524px;
  /* 省略 */
}
.MessageLog_Head {
  /* 省略 */
}
.MessageLog_Body {
  height: 100%;
  /* 省略 */
}

ですが、上記のようなときの.MessageLog_Bodyheight: 100%は、「高さいっぱい」にならず、包含ブロック(この場合は.MessageLog)の高さいっぱいとなるので、実質height: 524pxとしているのと同じです。

つまり、.MessageLogの中には、可変のヘッダの高さNpxNは任意の数字)と524pxの高さのメッセージが入る部分があるため、524px以上になってしまうということになります。「全体の高さ - 高さが可変なヘッダの高さ = メッセージが入る部分の高さ」というようなことをしたい場合は、FlexboxやCSS Gridを使うことをおすすめします。

position: stickyでもできる?(採用しなかった方法)

同じような見た目になり、ありそうなCSSを考えてみると、次のようなものも考えられます。

stickyで位置を固定する

.MessageLog {
  width: 300px;
  height: 524px;
  overflow: auto;
}
.MessageLog_Head {
  position: sticky;
  top: 0;
  /* 省略 */
}
.MessageLog_Body {
  /* height: 100%; 不要 */
  /* overflow: auto; 不要 */
  /* 省略 */
}

.MessageLogoverflow: auto;にして、ヘッダ部分をposition: sticky;にする方法です。

position: sticky;が指定された要素は、overflowプロパティ値のhidden auto scroll overlayといったスクロールの仕組みを持つ直近の祖先要素(scrolling ancestor)に固定されるため、ヘッダ部分は常に.MessageLogの上部に位置します。ヘッダの高さが変わっても問題ありません。

position: sticky;を使用した方法でも良さそうですが、スクロールバーの位置が.MessageLog全体になっています。これでは仕様にある通りのスクロールできる範囲になっていませんので採用できません。ただ、スクロールバーの出し方がこちらのほうが良かったり、実装の都合がある場合はposition: stickyを使う方法でも良いかもしれません。

メッセージ部分のHTMLとCSS

外側の部分はできたので、そこに配置するメッセージのHTMLとCSSを考えます。

まずはHTMLです。

メッセージ部分を組み立てる

<p class="Message">
    <img class="Message_Img" src="./img/avatar.png" alt="">
    <b class="Message_Name">名前</b>
    <span class="Message_Text">1.メッセージ本文です。メッセージ本文です。</span>
</p>

<p class="Message">
    <img class="Message_Img" src="./img/avatar.png" alt="">
    <b class="Message_Name">名前</b>
    <span class="Message_Text">2.メッセージ本文です。メッセージ本文です。</span>
</p>

<!-- 繰り返し -->

メッセージ部分は、p要素で作成し、名前はb要素で囲みます。これはHTML Living Standardの4.14.3 会話を参考にしています。会話をマークアップするための特別な要素(たとえば<chat />のような要素)は定義されていませんが、p要素などを使用して会話をマークアップすることが推奨されています。

代わりに、著者はp要素や句読点を使って会話をマークアップすることを推奨する。スタイリング目的のために話者を印をつける必要がある著者は、spanまたはbを使用するよう推奨される。(出典:HTML Living Standard — Last Updated 30 March 2022 4.14.3 会話

Instead, authors are encouraged to mark up conversations using p elements and punctuation. Authors who need to mark the speaker for styling purposes are encouraged to use span or b.(出典:HTML Living Standard — Last Updated 31 March 2022 4.14.3 Conversations

次にCSSです。

CSS Gridでメッセージ部分を作る

.Message {
  display: grid;
  grid-template: /* ① */
    "img name" auto
    "img text" auto
    /auto minmax(0, 1fr)
  ;
  gap: 4px;
  margin: 0;
}
.Message_Img {
  grid-area: img; /* ② */
  align-self: start; /* ③ */
  width: 30px;
  height: 30px;
}
.Message_Name {
  grid-area: name; /* ② */
  font-weight: bold;
  font-size: 12px;
  line-height: 1;
}
.Message_Text {
  grid-area: text; /* ② */
  font-size: 16px;
  line-height: 1.2;
  overflow-wrap: break-word; /* ④ */
}

p要素である全体を.Messageをグリッドコンテナとし、①の部分でグリッドアイテムを配置するための定義をします。図にすると次のようになります。各アイテム間の余白をgapプロパティを使用して4px;設けています。

次に、②の部分では、画像・名前・メッセージ本文をgrid-areaプロパティを使用して、グリッドコンテナで定義した箇所に配置します。

grid-areaプロパティ

grid-areaについては、「これからのグリッドレイアウト」シリーズの「第2回 グリッドアイテムの配置」で解説しています。

画像に関しては③の部分で、垂直位置を始端にします。これによって名前や本文や長くなって縦に伸びたとしても、画像は常に上付きになります。要件によっては中央位置する場合もあり、その場合はcenterとします。

メッセージ本文は、URLや長い英単語などが含まれる可能性があるため、適切に折り返されるようとoverflow-wrapプロパティを指定します。overflow-wrap: break-word;とすると、可能な限り禁則処理を残しつつ、言語問わず収まるように単語途中でも改行します。

改行をコントロールするoverflow-wrapプロパティについては、別の記事で解説をしたいと思っています。

ここまでのまとめ

今回はメッセージログUIの外側の部分と内側のメッセージ部分をそれぞれ組み立てました。

また、レイアウトにはFlexboxとCSS Gridを使いましたが、他の方法も検討しました。実務でも、さまざまに考えたうちのひとつを採用するわけですが、要件が変われば、実装方法も変わってくるでしょう。

あるデザインを提示されたときに、どれだけ実装方法のバリエーションに思いを巡らせることができるか、その想像力も大切です。

次回はこれらを組み合わせて、メッセージログのUI全体を仕上げていきます。