チャット · チャットUI

既読と日付区切りのある和製トーク画面

保存した一覧を見る 0 →

このブラウザー内に保存します。別の端末との同期はなく、サイトデータを消すと保存も消えます。

印刷時は見本を静止画にし、プロンプトと解説を出力します。PDF保存はブラウザーの印刷画面から選べます。

Vibehaus · デザイン資料
https://vibe.itlibra.com/design/messenger-talk-chat/
完全無料・著作権フリー(CC0)· 料金¥0 · 商用利用・改変自由

プレビュー

画面に合わせる

指定幅で実際に組み替えて表示します。表示領域より広い場合は、この枠の中で横へスクロールできます。

vibe.itlibra.com/preview/messenger-talk-chat/

AIへの指示文

日本のスマホで一般的なメッセンジャーアプリ風のトーク画面を作ってください。グループトークです。

要件:
- 配色: 背景は青みの灰 #8fb0d8(淡い円形のにじみを2つ重ねる)、ヘッダーは濃紺 #283548、自分の吹き出しは緑 #85dd6a、相手の吹き出しは白。
- 画面幅は最大420px。広い画面では中央に端末大の枠を1つ置き、角丸22pxと影を付ける。幅440px未満では枠をやめて全画面にする。
- ヘッダーに戻るボタン、グループ名、メンバー数、検索・通話・メニュー。
- 自分の発言は右、相手の発言は左。相手側は丸いアイコンと名前を最初の1通にだけ表示し、連投の2通目以降はアイコン幅だけ空けて省略する。
- 吹き出しのしっぽ(小さな三角)はグループの最初の1通にだけ付ける。
- 時刻は吹き出しの外、画面の中央寄りの下端に小さく置く。自分の発言には時刻の上に「既読 3」のように既読人数を出す。
- 日付の区切りは中央の半透明の札(例:「9月24日(水)」「今日」)。参加・退出などのお知らせも中央に小さく出す。
- 写真とスタンプのメッセージを1件ずつ含める。スタンプは吹き出しを使わず大きな絵だけで見せる。
- 下部に+・写真・入力欄・スタンプ・送信。入力欄は複数行に伸びる。
- スマホでは Enter は改行、送信はボタンのみ。物理キーボードのある環境だけ Enter 送信にし、変換確定の Enter は無視する。
- 送信したメッセージは少し遅れて「既読 1」から「既読 3」へ変わるようにする。
- フォント: system-ui と Hiragino Sans / Yu Gothic。

バリエーション

1対1のトーク版

グループ名を相手の名前に、既読人数を「既読」だけにする。
相手の名前表示はすべて省き、アイコンは最初の1通だけ残す。

ダーク版

背景を #1d2330、ヘッダーを #12161f、相手の吹き出しを #2c3444、文字を白に。
自分の吹き出しは #3fbf5f に落として、暗い地で眩しくならないようにする。

このプレビューのコード

HTML・CSS・JavaScriptの1ファイルです。外部依存・広告・解析コードはありません。入力文はこのページ内にだけ追加し、既読をタイマーで再現します。外部送信・保存・通話・写真添付はなく、再読み込みで初期状態に戻ります。

CC0 · 改変・商用利用自由 · revision 51897f86b2e1

コードを表示
<!doctype html>
<!-- Vibehaus — CC0。外部依存なし。投稿・既読はページ内のデモで、送信・保存は行わない。 -->
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="robots" content="noindex" />
    <title>既読と日付区切りのある和製トーク画面 — Vibehaus プレビュー</title>
    <style>
      :root {
        --wall: #8fb0d8;
        --wall-2: #9dbadf;
        --head: #283548;
        --head-ink: #ffffff;
        --mine: #85dd6a;
        --mine-ink: #102010;
        --theirs: #ffffff;
        --ink: #1b1e24;
        --meta: #3f4c5e;
        --bar: #f6f7f9;
        --line: #d9dde3;
        --accent: #1fb14a;
        --font: system-ui, -apple-system, "Hiragino Sans", "Hiragino Kaku Gothic ProN",
          "Yu Gothic Medium", "Yu Gothic", "Meiryo", sans-serif;
      }
      * { box-sizing: border-box; }
      html, body { height: 100%; }
      body {
        margin: 0;
        display: grid;
        place-items: center;
        font-family: var(--font);
        font-size: 15px;
        line-height: 1.7;
        letter-spacing: 0;
        color: var(--ink);
        background: #e8ecf1;
      }
      button, textarea { font: inherit; color: inherit; }
      button { cursor: pointer; }

      /* 端末の画面幅。広い画面では中央に1本だけ置く */
      .talk {
        display: flex;
        flex-direction: column;
        width: min(420px, 100%);
        height: min(820px, 100dvh);
        overflow: hidden;
        background: var(--wall);
        border-radius: 22px;
        box-shadow: 0 30px 60px -30px rgba(20, 30, 50, 0.45);
      }

      .head {
        display: flex;
        align-items: center;
        gap: 0.6rem;
        padding: 0.8rem 0.9rem;
        color: var(--head-ink);
        background: var(--head);
      }
      .head h1 { margin: 0; font-size: 16px; font-weight: 700; line-height: 1.4; }
      .head small { display: block; font-size: 12px; font-weight: 400; opacity: 0.75; }
      .head .back, .head .tool {
        width: 32px;
        height: 32px;
        padding: 0;
        font-size: 18px;
        line-height: 1;
        color: inherit;
        background: none;
        border: 0;
        border-radius: 50%;
      }
      .head .tools { display: flex; gap: 0.2rem; margin-left: auto; }

      .log {
        flex: 1;
        overflow-y: auto;
        padding: 0.8rem 0.75rem 1.2rem;
        scrollbar-width: thin;
        scrollbar-color: rgba(30, 45, 70, 0.35) transparent;
        background:
          radial-gradient(circle at 20% 10%, var(--wall-2), transparent 40%),
          radial-gradient(circle at 80% 70%, var(--wall-2), transparent 45%),
          var(--wall);
      }

      /* 日付の区切り。中央の小さな札 */
      .day {
        display: table;
        margin: 0.6rem auto 0.9rem;
        padding: 0.1rem 0.8rem;
        font-size: 12px;
        color: #ffffff;
        background: rgba(30, 45, 70, 0.35);
        border-radius: 999px;
      }

      .row { display: flex; align-items: flex-end; gap: 0.35rem; margin: 0 0 0.45rem; }
      .row.mine { justify-content: flex-end; }
      /* 連投の2通目以降は名前とアイコンを省き、アイコン幅だけ空ける */
      .row.theirs.cont { padding-left: 40px; }
      .row.theirs.cont .avatar, .row.theirs.cont .name { display: none; }

      .avatar {
        flex: none;
        align-self: flex-start;
        width: 34px;
        height: 34px;
        display: grid;
        place-items: center;
        font-size: 13px;
        font-weight: 700;
        color: #ffffff;
        background: #e07a5f;
        border-radius: 50%;
      }
      .stack { display: flex; flex-direction: column; max-width: 72%; }
      .name { margin: 0 0 0.15rem 0.2rem; font-size: 12px; color: var(--meta); }
      .line { display: flex; align-items: flex-end; gap: 0.35rem; }
      .row.mine .line { flex-direction: row-reverse; }

      .bubble {
        position: relative;
        padding: 0.5rem 0.8rem;
        font-size: 15px;
        line-height: 1.6;
        white-space: pre-wrap;
        overflow-wrap: anywhere;
        border-radius: 18px;
      }
      .theirs .bubble { background: var(--theirs); }
      .mine .bubble { color: var(--mine-ink); background: var(--mine); }
      /* 吹き出しのしっぽは、グループの最初の1通にだけ付ける */
      .row.theirs:not(.cont) .bubble::before,
      .row.mine.first .bubble::before {
        content: "";
        position: absolute;
        top: 8px;
        width: 12px;
        height: 12px;
        background: inherit;
      }
      .row.theirs:not(.cont) .bubble::before { left: -5px; clip-path: polygon(100% 0, 0 0, 100% 100%); }
      .row.mine.first .bubble::before { right: -5px; clip-path: polygon(0 0, 100% 0, 0 100%); }

      /* 時刻と既読は吹き出しの外、内側(画面中央寄り)の下に小さく置く */
      .meta {
        flex: none;
        display: flex;
        flex-direction: column;
        font-size: 11px;
        line-height: 1.35;
        color: var(--meta);
        white-space: nowrap;
      }
      .mine .meta { align-items: flex-end; }
      .read { font-weight: 600; }

      .sticker {
        display: grid;
        place-items: center;
        width: 120px;
        height: 120px;
        font-size: 64px;
        line-height: 1;
        background: rgba(255, 255, 255, 0.35);
        border-radius: 24px;
      }
      .photo {
        width: 200px;
        aspect-ratio: 4 / 3;
        overflow: hidden;
        background:
          linear-gradient(160deg, rgba(255, 255, 255, 0) 55%, rgba(0, 0, 0, 0.12)),
          radial-gradient(circle at 70% 30%, #ffd89b 0 12%, transparent 13%),
          linear-gradient(#9fd1f4 0 55%, #7cbc6a 55% 100%);
        border-radius: 14px;
      }
      .notice {
        max-width: 80%;
        margin: 0.8rem auto;
        padding: 0.35rem 0.8rem;
        font-size: 12px;
        text-align: center;
        color: #ffffff;
        background: rgba(30, 45, 70, 0.3);
        border-radius: 12px;
      }

      .bar {
        display: flex;
        align-items: flex-end;
        gap: 0.35rem;
        padding: 0.5rem 0.6rem calc(0.5rem + env(safe-area-inset-bottom));
        background: var(--bar);
        border-top: 1px solid var(--line);
      }
      .bar .icon {
        flex: none;
        width: 36px;
        height: 36px;
        padding: 0;
        font-size: 20px;
        line-height: 1;
        color: #5b6575;
        background: none;
        border: 0;
        border-radius: 50%;
      }
      .field {
        flex: 1;
        display: flex;
        align-items: flex-end;
        min-height: 36px;
        padding: 0.3rem 0.4rem 0.3rem 0.8rem;
        background: #ffffff;
        border: 1px solid var(--line);
        border-radius: 18px;
      }
      textarea {
        flex: 1;
        max-height: 110px;
        padding: 0.15rem 0;
        line-height: 1.6;
        border: 0;
        outline: 0;
        resize: none;
        background: none;
      }
      .send { color: var(--accent) !important; font-weight: 700; }
      .send:disabled { opacity: 0.35; cursor: default; }
      .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
      :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

      @media (max-width: 440px) {
        body { background: var(--wall); }
        .talk { height: 100dvh; border-radius: 0; box-shadow: none; }
      }
      button { min-width:44px; min-height:44px; }
      .head { flex-wrap:wrap; gap:4px; }
      .head .tools { margin-left:auto; }
      .bar .icon { width:44px; height:44px; flex:none; }
      .bar { flex-wrap:wrap; }
      .bar .field { min-width:0; flex:1; }
      .bar textarea { min-width:0; }
      .bubble { overflow-wrap:anywhere; }
      .demo-note { margin:0; padding:5px 12px; font-size:12px; line-height:1.8; color:var(--ink); background:var(--wall); }
    </style>
  </head>
  <body>
    <section class="talk" aria-label="トーク">
      <header class="head">
        <button class="back" type="button" aria-label="トーク一覧へ戻る">‹</button>
        <h1>週末の撮影チーム<small>メンバー 4人</small></h1>
        <div class="tools">
          <button class="tool" type="button" aria-label="検索">⌕</button>
          <button class="tool" type="button" aria-label="通話">☏</button>
          <button class="tool" type="button" aria-label="メニュー">≡</button>
        </div>
      </header>
      <p class="demo-note">投稿・既読はデモです。外部送信・保存はなく、通話・写真・メニューは表示見本です。</p>

      <div class="log" id="log" role="log" aria-live="polite" aria-label="メッセージ">
        <p class="day">9月24日(水)</p>

        <div class="row theirs">
          <div class="avatar" aria-hidden="true">み</div>
          <div class="stack">
            <p class="name">みさき</p>
            <div class="line"><div class="bubble">土曜の撮影、集合は10時で大丈夫そう?</div><span class="meta">21:14</span></div>
          </div>
        </div>
        <div class="row theirs cont">
          <div class="avatar" aria-hidden="true">み</div>
          <div class="stack">
            <p class="name">みさき</p>
            <div class="line"><div class="bubble">雨だったら屋内のスタジオに切り替えます</div><span class="meta">21:14</span></div>
          </div>
        </div>

        <div class="row mine first">
          <div class="stack">
            <div class="line"><div class="bubble">10時で大丈夫です!</div><span class="meta"><span class="read">既読 3</span>21:20</span></div>
          </div>
        </div>
        <div class="row mine">
          <div class="stack">
            <div class="line"><div class="bubble">機材は三脚と反射板を持っていきます</div><span class="meta"><span class="read">既読 3</span>21:20</span></div>
          </div>
        </div>

        <p class="day">今日</p>
        <p class="notice">けんた さんが参加しました</p>

        <div class="row theirs">
          <div class="avatar" aria-hidden="true" style="background:#5a8dd6">け</div>
          <div class="stack">
            <p class="name">けんた</p>
            <div class="line"><div class="photo" role="img" aria-label="ロケ地の下見写真"></div><span class="meta">08:02</span></div>
          </div>
        </div>
        <div class="row theirs cont">
          <div class="avatar" aria-hidden="true" style="background:#5a8dd6">け</div>
          <div class="stack">
            <p class="name">けんた</p>
            <div class="line"><div class="bubble">下見してきました。午前は逆光になりにくいです</div><span class="meta">08:03</span></div>
          </div>
        </div>
        <div class="row theirs cont">
          <div class="avatar" aria-hidden="true" style="background:#5a8dd6">け</div>
          <div class="stack">
            <p class="name">けんた</p>
            <div class="line"><div class="sticker" role="img" aria-label="スタンプ:親指を立てる">👍</div><span class="meta">08:03</span></div>
          </div>
        </div>
      </div>

      <form class="bar" id="form">
        <button class="icon" type="button" aria-label="その他の送信">+</button>
        <button class="icon" type="button" aria-label="写真を送る">◎</button>
        <div class="field">
          <label class="sr" for="input">メッセージ</label>
          <textarea id="input" rows="1" placeholder="メッセージを入力"></textarea>
          <button class="icon" type="button" aria-label="スタンプ">☺</button>
        </div>
        <button class="icon send" id="send" type="submit" aria-label="送信" disabled>➤</button>
      </form>
    </section>

    <script>
      const log = document.getElementById("log");
      const form = document.getElementById("form");
      const input = document.getElementById("input");
      const send = document.getElementById("send");
      log.scrollTop = log.scrollHeight;

      const now = () => {
        const d = new Date();
        return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
      };

      function post(text) {
        // 直前が自分の発言なら、しっぽを付けずに続ける
        const last = log.lastElementChild;
        const first = !(last && last.classList.contains("mine"));
        const row = document.createElement("div");
        row.className = "row mine" + (first ? " first" : "");
        row.innerHTML = '<div class="stack"><div class="line"><div class="bubble"></div><span class="meta"><span class="read"></span></span></div></div>';
        row.querySelector(".bubble").textContent = text;
        row.querySelector(".meta").append(now());
        log.append(row);
        log.scrollTop = log.scrollHeight;
        // 既読は相手が開いたときに付く。ここでは少し遅らせて付けて見せる
        const read = row.querySelector(".read");
        setTimeout(() => (read.textContent = "既読 1"), 1200);
        setTimeout(() => (read.textContent = "既読 3"), 2600);
      }

      function grow() {
        input.style.height = "auto";
        input.style.height = input.scrollHeight + "px";
        send.disabled = input.value.trim() === "";
      }
      input.addEventListener("input", grow);

      function submitMessage(e) {
        e.preventDefault();
        if (!input.value.trim()) return;
        post(input.value.trim());
        input.value = "";
        grow();
      }
      form.addEventListener('submit', submitMessage);
      send.addEventListener('click', submitMessage);

      // スマホのメッセンジャーに合わせ、Enter は改行、送信はボタンだけにする。
      // 広い画面(物理キーボード)では Enter 送信にするが、変換確定の Enter は無視する
      input.addEventListener("keydown", (e) => {
        if (e.key !== "Enter" || e.shiftKey) return;
        if (e.isComposing || e.keyCode === 229) return;
        if (!matchMedia("(pointer: fine)").matches) return;
        e.preventDefault();
        submitMessage(e);
      });
    </script>
  </body>
</html>

2026年10月14日、Chromiumで320・390・768・1280pxの描画とキーボードのフォーカス表示を確認しました。他のブラウザは未確認です。利用先のブラウザと実データでも確認してください。

概要

日本でチャットの画面と言えば、多くの人が思い浮かべるのは、青みがかった地に緑と白の吹き出しが並ぶ、あのトーク画面です。2011年に登場したメッセンジャーアプリが国内の利用者の大半に行き渡った結果、その組み方は日本語の会話UIの共通語になりました。

見慣れた文法を借りる理由

新しい連絡機能を作るとき、独自の見た目にすると利用者は吹き出しの左右や時刻の位置から読み方を覚え直すことになります。すでに身体が覚えている文法に合わせれば、その学習がいりません。

崩れる形時刻を吹き出しの中に入れる

欧米のアプリでは一般的ですが、日本の利用者には見慣れない配置です。本文と時刻が同じ面に並ぶので、短い発言ほど窮屈に見えます。

効く形時刻と既読は吹き出しの外

画面の中央寄り、吹き出しの下端に小さく置きます。本文の面を広く保ったまま、いつ送って誰が読んだかが一目で分かります。

連投をまとめる細部

同じ人が続けて送ったときは、アイコンと名前を最初の1通だけに付け、2通目以降はアイコンの幅だけ空けて揃えます。吹き出しのしっぽも最初の1通だけです。これだけで、誰の発言がどこまで続いているかが段落のように読めるようになります。

しっぽ・アイコン・名前はグループの先頭にだけ付ける。全部の発言に付けると、画面が同じ部品の繰り返しで埋まります。

仕上げの判断軸

既読は便利な反面、返信を急かす圧にもなります。予約や配送の連絡のように返事が前提の場面では強い味方ですが、雑談の場では出さない選択肢を用意してください。

420px画面の最大幅。広い画面では端末大の枠を中央に1つ置く
72%吹き出しの最大幅。残りを時刻と既読の置き場にする
1通目だけしっぽ・アイコン・名前を付ける範囲

入力欄の Enter の扱いも日本語特有です。スマホでは改行しながら長文を打つ人が多いので、Enter は改行、送信はボタンだけにします。物理キーボードのある環境で Enter 送信にする場合は、変換を確定する Enter で送られないようにしてください。

チャットの構成要素

チャットの構成要素 相手 ── 左・淡い面 自分 ── 右・濃い面 入力欄は下に固定 左右と色の2つで区別する ── 色だけだと区別が付かない人がいる 新しい発言は読み上げにも伝える(aria-live)。時刻と既読は小さく、邪魔しない位置に
チャットは誰の発言かが一瞬で分かることが最優先です。左右の位置と面の濃さを両方使えば、色に頼らずに区別できます。

使いどころ

  • 日本の利用者向けのコミュニティ・グループ機能
  • 予約や配送の連絡を会話形式で見せる画面
  • アプリのプロモーションで使う操作画面のモック
  • 社内連絡ツールの試作

このデザインの分類

カテゴリ
チャット
スタイル
ソフト / プレイフル / クール

背景と狙いどころ

よくある質問

既読を出すと利用者の負担にならない?
なります。既読は返信を急かす圧として働くので、業務連絡や予約確認のように返事が前提の場面に向きます。雑談中心のコミュニティなら、既読を出さない設定を用意してください。
時刻を吹き出しの中に入れてはだめ?
欧米のアプリは吹き出しの中の右下に入れることが多いですが、日本で最も普及した形は外側です。日本の利用者向けなら外側のほうが違和感がありません。
スマホで Enter 送信にしないのはなぜ?
日本語のスマホ入力では、改行しながら長文を打つ使い方が一般的だからです。送信ボタンを押すまで送られない形のほうが、誤送信の事故が起きません。

この画面を実装する

動くサンプルとコピペできるコードを、技術別にまとめています。