Đến nội dung chính

Màn hình trò chuyện

Dùng khi cuộc trò chuyện phải cho thấy ngay ai nói gì, đã đọc chưa và lúc nào.

Màn hình mẫu

Xem trước trực tiếp

Nguồn

<script>document.body.className="dc-bleed"</script>
<div class="dc-ch">
  <header class="dc-ch__top">
    <button class="dc-ch__ico" type="button"><span aria-hidden="true">←</span><span class="dc-sr">대화 목록으로</span></button>
    <span class="dc-ch__ava" aria-hidden="true">현</span>
    <div class="dc-ch__who">
      <h1 class="dc-ch__name">김현수 팀장</h1>
      <p class="dc-ch__sub">미소식품 인사팀 · 지금 활동 중</p>
    </div>
    <button class="dc-ch__ico" type="button"><span aria-hidden="true">☏</span><span class="dc-sr">전화 걸기</span></button>
    <button class="dc-ch__ico" type="button"><span aria-hidden="true">⋯</span><span class="dc-sr">대화 설정 더보기</span></button>
  </header>

  <main class="dc-ch__body" aria-label="주고받은 메시지">
    <p class="dc-ch__day">9월 12일 금요일</p>

    <div class="dc-ch__row dc-ch__row--in">
      <p class="dc-ch__bubble">다음 주 월요일부터 오전 9시 출근으로 바뀌었어요. 괜찮으세요?</p>
      <p class="dc-ch__meta">09:12</p>
    </div>

    <div class="dc-ch__row dc-ch__row--out">
      <p class="dc-ch__bubble dc-ch__bubble--out">네, 괜찮습니다. 지하철로 8시 45분쯤 도착할 것 같아요.</p>
      <p class="dc-ch__meta">09:14 · 읽음</p>
    </div>

    <div class="dc-ch__row dc-ch__row--in">
      <a class="dc-ch__file" href="#">
        <span class="dc-ch__file-ico" aria-hidden="true">PDF</span>
        <span class="dc-ch__file-txt">
          <span class="dc-ch__file-name">근로계약서_변경분.pdf</span>
          <span class="dc-ch__file-size">284KB · 서명이 필요합니다</span>
        </span>
      </a>
      <p class="dc-ch__meta">09:15</p>
    </div>

    <div class="dc-ch__row dc-ch__row--out">
      <p class="dc-ch__bubble dc-ch__bubble--out">확인했습니다. 시급이 10,320원으로 적혀 있는데 맞나요? 서명은 오늘 저녁에 할게요.</p>
      <p class="dc-ch__meta">09:20 · 읽음</p>
    </div>

    <p class="dc-ch__typing">
      <span class="dc-ch__typing-dot"></span>
      <span class="dc-ch__typing-dot"></span>
      <span class="dc-ch__typing-dot"></span>
      <span class="dc-sr">김현수 팀장이 입력 중입니다</span>
    </p>
  </main>

  <div class="dc-ch__quick" role="group" aria-label="빠른 답장">
    <button class="dc-ch__chip" type="button">네, 알겠습니다</button>
    <button class="dc-ch__chip" type="button">조금 늦을 것 같아요</button>
    <button class="dc-ch__chip" type="button">전화 주세요</button>
  </div>

  <form class="dc-ch__composer" action="#" method="post">
    <button class="dc-ch__plus" type="button"><span aria-hidden="true">+</span><span class="dc-sr">사진·파일 첨부</span></button>
    <label class="dc-sr" for="dc-ch-msg">보낼 메시지</label>
    <textarea class="dc-ch__input" id="dc-ch-msg" name="message" rows="1" placeholder="메시지 입력"></textarea>
    <button class="dc-ch__send" type="submit"><span aria-hidden="true">↑</span><span class="dc-sr">보내기</span></button>
  </form>
</div>
.dc-ch { display:flex; flex-direction:column; height:100vh; overflow:hidden; background:var(--dc-bg); color:var(--dc-text); font-family:var(--dc-font); }
.dc-ch__top { flex:0 0 auto; display:flex; align-items:center; gap:var(--dc-2); padding:calc(var(--dc-safe-top) + var(--dc-2)) var(--dc-1) var(--dc-2) var(--dc-1); background:var(--dc-surface); border-bottom:1px solid var(--dc-line); }
.dc-ch__ico { flex:0 0 auto; display:grid; place-items:center; width:44px; height:44px; border:0; border-radius:var(--dc-r-full); background:transparent; color:var(--dc-text); font-size:19px; line-height:1; }
.dc-ch__ico:active { background:var(--dc-sunken); }
.dc-ch__ava { flex:0 0 auto; display:grid; place-items:center; width:34px; height:34px; border-radius:var(--dc-r-full); background:var(--dc-brand-soft); color:var(--dc-brand-text); font-size:14px; font-weight:800; }
.dc-ch__who { flex:1 1 auto; min-width:0; display:flex; flex-direction:column; }
.dc-ch__name { font-size:15px; font-weight:700; line-height:1.3; color:var(--dc-text); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.dc-ch__sub { font-size:11px; color:var(--dc-ok); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.dc-ch__body { flex:1 1 auto; min-height:0; overflow-y:auto; overscroll-behavior:contain; display:flex; flex-direction:column; gap:var(--dc-3); padding:var(--dc-4); background:var(--dc-bg); }
.dc-ch__day { align-self:center; padding:var(--dc-1) var(--dc-3); border-radius:var(--dc-r-full); background:var(--dc-sunken); font-size:11px; font-weight:700; color:var(--dc-text-mute); }
.dc-ch__row { display:flex; flex-direction:column; gap:var(--dc-1); max-width:82%; }
.dc-ch__row--in { align-self:flex-start; align-items:flex-start; }
.dc-ch__row--out { align-self:flex-end; align-items:flex-end; }
.dc-ch__bubble { padding:var(--dc-3) var(--dc-4); border:1px solid var(--dc-line); border-radius:var(--dc-r-lg); border-bottom-left-radius:var(--dc-r-xs); background:var(--dc-surface); color:var(--dc-text); font-size:15px; line-height:1.5; box-shadow:var(--dc-sh-1); }
.dc-ch__bubble--out { border-color:transparent; border-radius:var(--dc-r-lg); border-bottom-right-radius:var(--dc-r-xs); background:var(--dc-brand); color:var(--dc-text-on); }
.dc-ch__meta { font-size:11px; color:var(--dc-text-mute); }
.dc-ch__file { display:flex; align-items:center; gap:var(--dc-3); padding:var(--dc-3); border:1px solid var(--dc-line); border-radius:var(--dc-r-md); background:var(--dc-surface); box-shadow:var(--dc-sh-1); text-decoration:none; }
.dc-ch__file:active { background:var(--dc-sunken); }
.dc-ch__file-ico { flex:0 0 auto; display:grid; place-items:center; width:40px; height:44px; border-radius:var(--dc-r-xs); background:var(--dc-danger-soft); color:var(--dc-danger); font-size:11px; font-weight:800; }
.dc-ch__file-txt { display:flex; flex-direction:column; gap:2px; min-width:0; }
.dc-ch__file-name { font-size:14px; font-weight:700; color:var(--dc-text); word-break:break-all; }
.dc-ch__file-size { font-size:12px; color:var(--dc-text-sub); }
.dc-ch__typing { align-self:flex-start; display:flex; align-items:center; gap:5px; padding:var(--dc-3) var(--dc-4); border:1px solid var(--dc-line); border-radius:var(--dc-r-lg); border-bottom-left-radius:var(--dc-r-xs); background:var(--dc-surface); }
.dc-ch__typing-dot { width:7px; height:7px; border-radius:var(--dc-r-full); background:var(--dc-text-mute); animation:dc-ch-bounce 1.2s infinite ease-in-out; }
.dc-ch__typing-dot:nth-child(2) { animation-delay:.15s; }
.dc-ch__typing-dot:nth-child(3) { animation-delay:.3s; }
@keyframes dc-ch-bounce { 0%, 70%, 100% { opacity:.35; transform:translateY(0); } 35% { opacity:1; transform:translateY(-3px); } }
.dc-ch__quick { flex:0 0 auto; display:flex; gap:var(--dc-2); padding:var(--dc-2) var(--dc-4); overflow-x:auto; background:var(--dc-bg); scrollbar-width:none; }
.dc-ch__quick::-webkit-scrollbar { display:none; }
.dc-ch__chip { flex:0 0 auto; min-height:36px; padding:0 var(--dc-4); border:1px solid var(--dc-line-field); border-radius:var(--dc-r-full); background:var(--dc-surface); color:var(--dc-text-sub); font-family:inherit; font-size:13px; font-weight:700; white-space:nowrap; }
.dc-ch__chip:active { background:var(--dc-sunken); }
.dc-ch__composer { flex:0 0 auto; display:flex; align-items:flex-end; gap:var(--dc-2); padding:var(--dc-2) var(--dc-3) calc(var(--dc-safe-bottom) + var(--dc-2)); background:var(--dc-surface); border-top:1px solid var(--dc-line); }
.dc-ch__composer--reply { flex-direction:column; align-items:stretch; gap:var(--dc-2); }
.dc-ch__quote { display:flex; align-items:center; gap:var(--dc-2); padding:var(--dc-2) var(--dc-2) var(--dc-2) var(--dc-3); border-left:3px solid var(--dc-brand); border-radius:var(--dc-r-xs); background:var(--dc-sunken); }
.dc-ch__quote-txt { flex:1 1 auto; min-width:0; display:flex; flex-direction:column; font-size:13px; color:var(--dc-text-sub); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.dc-ch__quote-who { font-size:11px; font-weight:800; color:var(--dc-brand-text); }
.dc-ch__quote-x { flex:0 0 auto; display:grid; place-items:center; width:44px; height:44px; border:0; border-radius:var(--dc-r-full); background:transparent; color:var(--dc-text-sub); font-size:15px; }
.dc-ch__quote-x:active { background:var(--dc-bg); }
.dc-ch__bar { display:flex; align-items:flex-end; gap:var(--dc-2); }
.dc-ch__plus { flex:0 0 auto; display:grid; place-items:center; width:44px; height:44px; border:0; border-radius:var(--dc-r-full); background:var(--dc-sunken); color:var(--dc-text); font-size:20px; line-height:1; }
.dc-ch__plus:active { background:var(--dc-line); }
.dc-ch__input { flex:1 1 auto; min-width:0; min-height:44px; max-height:120px; padding:var(--dc-3); border:1px solid var(--dc-line-field); border-radius:var(--dc-r-lg); background:var(--dc-bg); color:var(--dc-text); font-family:inherit; font-size:15px; line-height:1.4; resize:none; }
.dc-ch__send { flex:0 0 auto; display:grid; place-items:center; width:44px; height:44px; border:0; border-radius:var(--dc-r-full); background:var(--dc-sunken); color:var(--dc-text-mute); font-size:19px; font-weight:800; line-height:1; }
.dc-ch__send--on { background:var(--dc-brand); color:var(--dc-text-on); }
.dc-ch__send--on:active { background:var(--dc-brand-hi); }

Cách dùng

  • Tin của mình bên phải, màu nhấn; tin của người kia bên trái, nền mặt phẳng — phân biệt bằng cả vị trí, không chỉ màu
  • Giờ và trạng thái đã đọc để ngoài bong bóng, nếu để trong sẽ lẫn với nội dung
  • Ô nhập bắt đầu một dòng và cao dần theo nội dung; nút gửi vẫn ở chỗ cũ
  • Tệp và ảnh hiển thị dạng thẻ, ghi rõ dung lượng và việc cần làm tiếp

Điểm cần lưu ý

  • Ô nhập cũng cần label (ẩn bằng dc-sr); placeholder không phải nhãn
  • Đừng để ba dấu chấm động là dấu hiệu duy nhất của "đang nhập" — cho dc-sr đọc ra
  • Thanh nhập dưới phải có --dc-safe-bottom, nếu không thanh home che mất

Tiếp theo