Actions

Work Header

WhatsApp group chat skin

Chapter Text

Introduction

This workskin is based on two prexisting skins for formatting WhatsApp conversations, with a few tweaks and additions aimed at more easily replicating the format of a group chat with minimal effort on the HTML side. The basis of this skin comes mostly from etc_e_tal’s Social Media Work Skin Template: Whatsapp (which I’m going to refer to a few times in this introduction, so I’ll call it SMWSTW). SMWSTW in turn draws from Azdaema’s WhatsApp Skin; the present skin includes a number of features present there but not in SMWSTW:

  • Big emoji support;
  • Support for unsent text in the bottom bar;
  • Automatic ellipsis in the group name.

The following features from SMWSTW are also included:

  • Display of the sender’s name above a message, with classes for a range of colours;
  • A scroll bar;
  • Miscellaneous graphical updates;
  • Dark mode support.

In addition, I’ve made a few of my own additions/changes:

  • A list of members under the group name;
  • Classes for quoted text, links/@-replies, and link previews;
  • Automatic hiding of the sender’s name if it accompanies an outgoing message;
  • Alterations to the date indications (“Today” etc.) to more closely resemble the appearance of WhatsApp in 2021;
  • Automatic inclusion of the tick marks indicating a sent message has been read by all recipients, with options to override this;
  • Automatic placement of tails on the first message in a sequence, without the need to mark this message out as different from the others;
  • General cleanup and refactoring of the CSS classes to a. reduce the length of the CSS, and b. (more importantly) organise the HTML so that it’s as logical as possible and also requires a minimum of information to be specified in the work itself.

I have retained support for the reader class from SMWSTW but won’t be discussing that here: in short, if you wrap anything in an element with class reader, it will be hidden when the skin is active, but present otherwise, which can be of use to those with accessibility needs. SMWSTW discusses some potential metatextual additions that can be used to provide support with the help of this feature. Unfortunately, like in SMWSTW I haven’t found a way of hiding message timestamps when the skin is turned off, which could be detrimental to accessibility. I have however made these into block-level elements, which should help somewhat by keeping them separate from the main message content.

The skin has been tested on a PC running Linux and an Android phone; it looks slightly more authentic on the latter (probably in part because I don’t have the right fonts installed on my PC). If you’re viewing this tutorial on mobile, though, you will have to do some horizontal scrolling in chapter 5 to view the dark mode options, although this shouldn’t interfere with reading the code and explanations.

Navigation

The remaining chapters of this work cover the following:

You will probably have to be fairly familiar with the fundamentals of HTML to follow this – in particular, it assumes a knowledge of the usual nested structure, as well as basic terminology such as “element” and “class”. I can recommend nothing more highly than W3Schools for learning more about HTML and CSS.

Chapter Text

#workskin .reader {
  display: none;
}

#workskin .whatsapp {
  width: 340px;
  float: center;
  margin: 0 auto;
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  line-height: 1;
}

#workskin .whatsapp table {
  border-collapse: collapse;
  border-spacing: 0;
}

#workskin .whatsapp div,
#workskin .whatsapp span,
#workskin .whatsapp p,
#workskin .whatsapp img,
#workskin .whatsapp table,
#workskin .whatsapp tbody,
#workskin .whatsapp thead,
#workskin .whatsapp tr,
#workskin .whatsapp th,
#workskin .whatsapp td {
  margin: 0;
  padding: 0;
  border: 0;
  font-size: 100%;
  font: inherit;
  vertical-align: baseline;
}

#workskin .whatsapp p {
  display: inline-block;
  line-height: 1.2;
}

#workskin .whatsapp p:empty {
  display: none;
}

#workskin .whatsapp p,
#workskin .whatsapp .img {
  margin: 0;
}

#workskin .whatsapp .link {
  color: #34b4eb;
  display: inline;
}

#workskin .whatsapp .topbar {
  padding: 3px;
  height: 60px;
  width: 340px;
  color: #dee5eb;
}

#workskin .whatsapp .topbar td {
  vertical-align: middle;
  overflow: hidden;
}

#workskin .whatsapp.light .topbar {
  background-color: #075e55;
}

#workskin .whatsapp.dark .topbar {
  background-color: #232d36;
}

#workskin .whatsapp .icons {
  width: 0;
}

#workskin .whatsapp .icons::after {
  background-image: url("https://i.ibb.co/FmXV1d2/icons.png");
  background-size: 30px 15px;
  display: inline-block;
  max-width: 30px;
  width: 30px;
  height: 15px;
  margin-right: 15px;
  content: "";
}

#workskin .whatsapp .members,
#workskin .whatsapp .groupname {
  margin-right: auto;
  min-width: 180px;
  white-space: nowrap;
  max-width: 180px;
  display: flex;
}

#workskin .whatsapp .members {
  font-size: 0.75rem;
}

#workskin .whatsapp .groupname {
  font-weight: bold;
  font-size: 1.5rem;
  padding-top: 10px;
}

#workskin .whatsapp .topbar p {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 200px;
}

#workskin .whatsapp .pp img {
  min-width: 50px;
  width: 50px;
  border-radius: 50%;
  height: 50px;
  overflow: hidden;
  margin: 5px 0;
}

#workskin .whatsapp .backarrow::after {
  content: "←";
  font-weight: bold;
  font-size: 1.5rem;
  position: relative;
}

#workskin .whatsapp .messages {
  background-repeat: repeat-y;
  background-size: 100%;
  padding: 10px;
  padding-top: 0px;
  overflow: auto;
  max-height: 530px;
  max-width: 340px;
  text-align: center;
}

#workskin .whatsapp.light .messages {
  background-image: url("https://i.ibb.co/YjT1qGH/1620176370792.jpg");
}

#workskin .whatsapp.dark .messages {
  background-image: url("https://i.ibb.co/7ksHgZs/1620176474989.jpg");
}

#workskin .whatsapp ::-webkit-scrollbar {
  width: 10px;
}

#workskin .whatsapp ::-webkit-scrollbar-track {
  background: none;
}

#workskin .whatsapp ::-webkit-scrollbar-thumb {
  background: rgba(0,0,0,0.5);
  border-radius: 10px;
}

#workskin .whatsapp ::-webkit-scrollbar-thumb:hover {
  background: rgba(0,0,0,0.7);
  border-radius: 10px;
}

#workskin .whatsapp .bottombar {
  background-repeat: repeat-y;
  background-size: 100%;
  width: auto;
  height: 50px;
  padding-left: 15px;
  padding-bottom: 0px;
}

#workskin .whatsapp .typearea {
  position: relative;
  display: inline-block;
  padding: 2px 2px 2px 0;
  border-right: 2px solid #007aff;
  overflow: hidden;
  max-width: 170px;
  white-space: nowrap;
  margin-left: 2em;
  margin-top: 1em;
}

#workskin .whatsapp.dark .typearea {
  color: #dee5eb;
}

#workskin .whatsapp .typearea .from {
  display: none;
}

#workskin .whatsapp.light .bottombar {
  background-image: url("https://i.ibb.co/pyCv9Bj/1620176397802.jpg");
}

#workskin .whatsapp.dark .bottombar {
  background-image: url("https://i.ibb.co/6Dxsfrr/barra-escuracd.png");
}

#workskin .whatsapp .in,
#workskin .whatsapp .out {
  text-align: left;
}

#workskin .whatsapp .text {
  margin: 0.1em 1em;
  max-width: 75%;
  clear: both;
  position: relative;
  border-radius: 0.5em;
}

#workskin .whatsapp.light .text {
  color: #000000;
}

#workskin .whatsapp.dark .text {
  color: #dee5eb;
}

#workskin .whatsapp.light .out .text {
  float: right;
  background: #E1FFC7;
}

#workskin .whatsapp.dark .out .text {
  float: right;
  background: #054640;
}

#workskin .whatsapp.light .in .text {
  float: left;
  background: #f6f6f6;
}

#workskin .whatsapp.dark .in .text {
  float: left;
  background: #232d36;
}

#workskin .whatsapp .out > div:first-of-type {
  margin-top: 0.3em;
  border-radius: 0.5em 0 0.5em 0.5em;
}

#workskin .whatsapp .in > div:first-of-type {
  margin-top: 0.3em;
  border-radius: 0 0.5em 0.5em 0.5em;
}

#workskin .whatsapp.light .out > div:first-of-type::before {
  content: "";
  position: absolute;
  right: -0.4em;
  top: 0;
  width: 0.5em;
  height: 1em;
  border-right: 0.5em solid #E1FFC7;
  border-bottom-right-radius: 0.5em 1em;
}

#workskin .whatsapp.light .in > div:first-of-type::before {
  content: "";
  position: absolute;
  left: -0.4em;
  top: 0;
  width: 0.5em;
  height: 1em;
  border-left: 0.5em solid #f6f6f6;
  border-bottom-left-radius: 0.5em 1em;
}

#workskin .whatsapp .text p {
  margin: 0.25em 7px 0.25em .45em;
}

#workskin .whatsapp.dark .out > div:first-of-type::before {
  content: "";
  position: absolute;
  right: -0.4em;
  top: 0;
  width: 0.5em;
  height: 1em;
  border-right: 0.5em solid #054640;
  border-bottom-right-radius: 0.5em 1em;
}

#workskin .whatsapp.dark .in > div:first-of-type::before {
  content: "";
  position: absolute;
  left: -0.4em;
  top: 0;
  width: 0.5em;
  height: 1em;
  border-left: 0.5em solid #232d36;
  border-bottom-left-radius: 0.5em 1em;
}

#workskin .whatsapp .img {
  border-radius: 3px;
  width: 100%;
  display: block;
}

#workskin .whatsapp .img img {
  width: 100%;
}

#workskin .whatsapp .time {
  font-size: 0.75rem;
  color: #999;
  display: inline-block;
  margin: 0;
  float: right;
  background-color: transparent;
  line-height: 1.7em;
}

#workskin .whatsapp .img .time {
  color: #ffffff;
  position: absolute;
  bottom: 5px;
  right: 6px;
}

#workskin .whatsapp .out .time::after {
  background-size: 15px 10px;
  display: inline-block;
  width: 15px;
  height: 10px;
  content: "";
  margin: 0 0 0 2px;
}

#workskin .whatsapp .out .time::after {
  background-image: url(https://i.ibb.co/SRHp95x/bluecheck.png);
}

#workskin .whatsapp .out .unread::after {
  background-image: url(https://i.ibb.co/q7thTHp/gray2.png);
}

#workskin .whatsapp .out .img .unread::after {
  background-image: url(https://i.ibb.co/mXFwkhD/white2.png);
}

#workskin .whatsapp .out .unsent::after {
  background-image: url(https://i.ibb.co/6F74Wr1/gray1.png);
}

#workskin .whatsapp .out .img .unsent::after {
  background-image: url(https://i.ibb.co/vqr7T8X/white1.png);
}

#workskin .whatsapp .info {
  font-size: 0.75rem;
  width: 100%;
  margin: 10px auto;
  text-align: center;
  display: inline-block;
  line-height: 1.3;
}

#workskin .whatsapp .info p {
  max-width: 75%;
  border-radius: 5px;
  padding: 5px;
  display: inline-block;
}

#workskin .whatsapp.light .info p {
  background: #e3e3e3;
}

#workskin .whatsapp.dark .info p {
  background: #232d36;
  color: #999;
}

#workskin .whatsapp .from {
  font-weight: bold;
  line-height: .5em;
  display: block;
  padding-top: 8px;
}

#workskin .whatsapp .text > .from {
  font-size: 0.85rem;
  padding-bottom: 4px;
}

#workskin .whatsapp .out .text > .from {
  display: none;
}

#workskin .whatsapp .quote,
#workskin .whatsapp .preview {
  font-size: 0.75rem;
  border-radius: 0.5em;
  margin: 4px;
}

#workskin .whatsapp .preview {
  overflow: hidden;
  max-height: 5em;
}

#workskin .whatsapp.light .in .quote,
#workskin .whatsapp.light .in .preview {
  background-color: #ececec;
  color: #757575;
}

#workskin .whatsapp.light .out .quote,
#workskin .whatsapp.light .out .preview {
  background-color: #daf7c1;
  color: #757575;
}

#workskin .whatsapp.dark .in .quote,
#workskin .whatsapp.dark .in .preview {
  background-color: #171e24;
  color: #c9c9c9;
}

#workskin .whatsapp.dark .out .quote,
#workskin .whatsapp.dark .out .preview {
  background-color: #0e4037;
  color: #c9c9c9;
}

#workskin .whatsapp.light .preview-title {
  font-weight: bold;
  color: black;
}

#workskin .whatsapp.dark .preview-title {
  font-weight: bold;
  color: #dee5eb;
}

#workskin .whatsapp .preview p {
  float: left;
  height: 100%;
  : 100%
  padding: 0;
  margin: 0;
  line-height: 1.5;
}

#workskin .whatsapp .preview img {
  margin-right: 4px;
  float: left;
  height: 100%;
  width: 60px;
  max-width: 30%;
}

#workskin .whatsapp .onemoji p,
#workskin .whatsapp .twomoji p,
#workskin .whatsapp .threemoji p {
  display: block;
}

#workskin .whatsapp .onemoji p:first-of-type {
  font-size: 2.5rem;
}

#workskin .whatsapp .twomoji p:first-of-type {
  font-size: 2rem;
}

#workskin .whatsapp .threemoji p:first-of-type {
  font-size: 1.5rem;
}

#workskin .whatsapp .quote.red1 {
  border-left: 3px solid #fe0200;
}

#workskin .whatsapp .quote.red2 {
  border-left: 3px solid #d9262a;
}

#workskin .whatsapp .quote.pink {
  border-left: 3px solid #ff3e73;
}

#workskin .whatsapp .quote.blue1 {
  border-left: 3px solid #0166ff;
}

#workskin .whatsapp .quote.blue2 {
  border-left: 3px solid #297dfa;
}

#workskin .whatsapp .quote.blue3 {
  border-left: 3px solid #5887cb;
}

#workskin .whatsapp .quote.blue4 {
  border-left: 3px solid #34deff;
}

#workskin .whatsapp .quote.brown {
  border-left: 3px solid #af593e;
}

#workskin .whatsapp .quote.orange {
  border-left: 3px solid #ff861e;
}

#workskin .whatsapp .quote.yellow {
  border-left: 3px solid #ffe53a;
}

#workskin .whatsapp .quote.purple {
  border-left: 3px solid #8359ae;
}

#workskin .whatsapp .quote.green1 {
  border-left: 3px solid #01a368;
}

#workskin .whatsapp .quote.green2 {
  border-left: 3px solid #c5e17a;
}

#workskin .whatsapp .quote.green3 {
  border-left: 3px solid #22af22;
}

#workskin .whatsapp .quote.green4 {
  border-left: 3px solid #096309;
}

#workskin .whatsapp .quote.green5 {
  border-left: 3px solid #054105;
}

#workskin .whatsapp .quote.red1 .from,
#workskin .whatsapp .from.red1 {
  color: #fe0200;
}

#workskin .whatsapp .quote.red2 .from,
#workskin .whatsapp .from.red2 {
  color: #d9262a;
}

#workskin .whatsapp .quote.pink .from,
#workskin .whatsapp .from.pink {
  color: #ff3e73;
}

#workskin .whatsapp .quote.blue1 .from,
#workskin .whatsapp .from.blue1 {
  color: #0166ff;
}

#workskin .whatsapp .quote.blue2 .from,
#workskin .whatsapp .from.blue2 {
  color: #297dfa;
}

#workskin .whatsapp .quote.blue3 .from,
#workskin .whatsapp .from.blue3 {
  color: #5887cb;
}

#workskin .whatsapp .quote.blue4 .from,
#workskin .whatsapp .from.blue4 {
  color: #34deff;
}

#workskin .whatsapp .quote.brown .from,
#workskin .whatsapp .from.brown {
  color: #af593e;
}

#workskin .whatsapp .quote.orange .from,
#workskin .whatsapp .from.orange {
  color: #ff861e;
}

#workskin .whatsapp .quote.yellow .from,
#workskin .whatsapp .from.yellow {
  color: #ffe53a;
}

#workskin .whatsapp .quote.purple .from,
#workskin .whatsapp .from.purple {
  color: #8359a3;
}

#workskin .whatsapp .quote.green1 .from,
#workskin .whatsapp .from.green1 {
  color: #01a368;
}

#workskin .whatsapp .quote.green2 .from,
#workskin .whatsapp .from.green2 {
  color: #c5e17a;
}

#workskin .whatsapp .quote.green3 .from,
#workskin .whatsapp .from.green3 {
  color: #22af22;
}

#workskin .whatsapp .quote.green4 .from,
#workskin .whatsapp .from.green4 {
  color: #096309;
}

#workskin .whatsapp .quote.green5 .from,
#workskin .whatsapp .from.green5 {
  color: #054105;
}

Chapter Text

Here’s a demonstration of the features of this skin in light mode. The raw HTML code is underneath; AO3 tends to add a few sets of <p></p> here and there once this actually gets pasted into the editor, but the CSS ensures that these have no effect. Chapter 5 gives a breakdown of the various elements showcased here.

group icon

🦎🦎🦎

You, Asshole, St Braska 🙏

Today

St Braska 🙏

Took Yuna to the zoo today 😊

17:26

Jecht

Yevon glyph Spira Wildlife Park
Witness the glory of Yevon’s creation!

this one? https://spira-wp.sp

17:28

St Braska 🙏

That’s the one

17:31

She loved the lizards!

17:31

Young girl holding lizard

17:34

Jecht

dawwwwww

17:34

💖

17:35

Asshole

Cute 🦎

19:07

Jecht

Asdfhjk auron did u jsut

19:08

omg did u even know theres an eomji keyboard

19:08

You changed the subject from “boyzz 💫” to “🦎🦎🦎”

St Braska 🙏

Lol!

19:15

Asshole

You

omg did u even know theres an eomji keyboard

I know how phones work, Jecht.

19:56

Jecht

no.u

19:56

Asshole

Are you drunk?

19:58

@Jecht where are you? Is anyone with you?

20:12

Jecht

chill aurofn im finnee


<div class="whatsapp light">       
  <table class="topbar">
    <tbody>
      <tr>
       <td rowspan="2" class="backarrow"></td>
       <td rowspan="2" class="pp">
         <img src="https://i.ibb.co/8mXbptg/pp.png" alt="group icon"/>
       </td>
       <td class="groupname">
         <p>🦎🦎🦎</p>
       </td>
       <td rowspan="2" class="icons">
       </td>
      </tr>
      <tr>
       <td class="members">
         <p>You, Asshole, St Braska 🙏</p>
       </td>
      </tr>
    </tbody>
  </table>
  <div class="messages">
    <div class="info">
      <p>Today</p>
    </div>
    <div class="in">
      <div class="text">
	<p class="from blue2">St Braska 🙏</p>
        <p>Took Yuna to the zoo today 😊</p>
        <p class="time">17:26</p>
      </div>
    </div>
    <div class="out">
      <div class="text">
        <p class="from orange">Jecht</p>
	  <div class="preview">
            <p>
              <img src="https://i.ibb.co/y0JbTDT/yevon.jpg" alt="Yevon glyph"/>
              <span class="preview-title">Spira Wildlife Park</span><br/>Witness the glory of Yevon’s creation!
            </p>
          </div>
        <p>this one? <span class="link">https://spira-wp.sp</span></p>
        <p class="time">17:28</p>
      </div>
    </div>
    <div class="in">
      <div class="text">
        <p class="from blue2">St Braska 🙏</p>
        <p>That’s the one</p>
	<p class="time">17:31</p>
      </div>
      <div class="text">
        <p>She loved the lizards!</p>
        <p class="time">17:31</p>
      </div>
      <div class="text">
        <div class="img">
	  <p>
	    <img src="https://i.ibb.co/FD1w2DC/yuna.jpg" alt="Young girl holding lizard"/>
	  </p>
          <p class="time">17:34</p>
        </div>
      </div>
    </div>
    <div class="out">
      <div class="text">
        <p class="from orange">Jecht</p>
        <p>dawwwwww</p>
        <p class="time">17:34</p>
      </div>
      <div class="text">
        <div class="onemoji">
          <p>💖</p>
          <p class="time">17:35</p>
	</div>
      </div>
    </div>
    <div class="in">
      <div class="text">
        <p class="from red2">Asshole</p>
        <p>Cute 🦎</p>
        <p class="time">19:07</p>
      </div>
    </div>
    <div class="out">
      <div class="text">
        <p class="from orange">Jecht</p>
        <p>Asdfhjk auron did u jsut</p>
	<p class="time">19:08</p>
      </div>
      <div class="text">
        <p>omg did u even know theres an eomji keyboard</p>
        <p class="time">19:08</p>
      </div>
    </div>
    <div class="info">
      <p>You changed the subject from “boyzz 💫” to “🦎🦎🦎”</p>
    </div>
    <div class="in">
      <div class="text">
        <p class="from blue2">St Braska 🙏</p>
	<p>Lol!</p>
        <p class="time">19:15</p>
      </div>
    </div>
    <div class="in">
      <div class="text">
        <p class="from red2">Asshole</p>
        <div class="quote orange">
          <p class="from">You</p>
          <p>omg did u even know theres an eomji keyboard</p>
        </div>
        <p>I know how phones work, Jecht.</p>
        <p class="time">19:56</p>
      </div>
    </div>
    <div class="out">
      <div class="text">
        <p class="from orange">Jecht</p>
        <p>
          <span class="link">no.u</span>
        </p>
        <p class="time unread">19:56</p>
      </div>
    </div>
    <div class="in">
      <div class="text">
        <p class="from red2">Asshole</p>
        <p>Are you drunk?</p>
        <p class="time">19:58</p>
      </div>
      <div class="text">
        <p>
          <span class="link">@Jecht</span> where are you? Is anyone with you?
        </p>
        <p class="time">20:12</p>
      </div>
    </div>
  </div>
  <div class="bottombar">
    <div class="typearea">
      <p class="from">Jecht</p>
      <p>chill aurofn im finnee</p>
    </div>
  </div>
</div>

Chapter Text

I won’t repeat the raw HTML in this chapter as there’s only one change to make to convert everything from light mode to dark mode: replace the class light with dark on the outermost <div> element. That one change will switch the appearance from light to dark mode across the board with no additional alterations required.

group icon

🦎🦎🦎

You, Asshole, St Braska 🙏

Today

St Braska 🙏

Took Yuna to the zoo today 😊

17:26

Jecht

Yevon glyph Spira Wildlife Park
Witness the glory of Yevon’s creation!

this one? https://spira-wp.sp

17:28

St Braska 🙏

That’s the one

17:31

She loved the lizards!

17:31

Young girl holding lizard

17:34

Jecht

dawwwwww

17:34

💖

17:35

Asshole

Cute 🦎

19:07

Jecht

Asdfhjk auron did u jsut

19:08

omg did u even know theres an eomji keyboard

19:08

You changed the subject from “boyzz 💫” to “🦎🦎🦎”

St Braska 🙏

Lol!

19:15

Asshole

You

omg did u even know theres an eomji keyboard

I know how phones work, Jecht.

19:56

Jecht

no.u

19:56

Asshole

Are you drunk?

19:58

@Jecht where are you? Is anyone with you?

20:12

Jecht

chill aurofn im finnee

Chapter Text

Contents

This chapter will go through the process of building all the components shown in the example demonstrated in chapters 3 and 4. The constituent parts are as follows:

Basic structure

First, here are the elements that make up the overall structure and inside which everything else needs to be nested. We’ll be making a lot of use of <div> elements here: our outermost component is a <div> with the class whatsapp plus one of the classes light or dark. I’ll be showing both light and dark themes side by side in this chapter (if you’re on mobile, you’ll probably need to scroll to the right to see the dark theme examples).

Inside the outer <div>, there are three elements that represent the header (class topbar), the message area (class messages), and the bottom bar for typing input (class bottombar). The topbar element is a <table>; the others are <div> elements.

<div class="whatsapp light">      
  <table class="topbar"></table>
  <div class="messages"></div>
  <div class="bottombar"></div>
</div>
<div class="whatsapp dark">      
  <table class="topbar"></table>
  <div class="messages"></div>
  <div class="bottombar"></div>
</div>

So far, this gives us something that isn’t especially forthcoming:

Most obviously, the bottom bar is hidden, but this will reveal itself once we start adding messages!

The top bar

We’ll work through these three components in sequence, though, starting with the top bar. Let’s replace <table class="topbar"></table> with this structure:

<table class="topbar">
  <tbody>
    <tr>
     <td rowspan="2" class="backarrow"></td>
     <td rowspan="2" class="pp">
       <img src="URL of profile image goes here" alt="group icon"/>
     </td>
     <td class="groupname">
       <p>name of chat goes here</p>
     </td>
     <td rowspan="2" class="icons">
     </td>
    </tr>
    <tr>
     <td class="members">
       <p>list of group members goes here</p>
     </td>
    </tr>
  </tbody>
</table>

Here’s an example:

group icon

🦎🦎🦎

You, Asshole, St Braska 🙏

group icon

🦎🦎🦎

You, Asshole, St Braska 🙏

A couple of things to note before moving on: firstly, the name of the group and the list of members will automatically be truncated with an ellipsis if they spill over the boundary of the table cell. So when these are too long, we’ll see something like this:

group icon

An extremely long group name

An enormous number of people, certainly more friends than I or any sane person should have

group icon

An extremely long group name

An enormous number of people, certainly more friends than I or any sane person should have

Secondly, for the sake of realism, group members should be listed in the following order:

  1. “You” (i.e. the person whose phone it is);
  2. Contacts stored in the phone, in alphabetical order;
  3. Numbers of group members not stored as contacts in the phone, in numerical order.

This can also be replaced with a message such as Name is typing … when appropriate.

The message area

Now for the fun part! We’ll move into the <div> with class messages for all the code that has to do with the messages sent and received.

Received messages

Let’s start with messages that the viewer receives, and then move on to sent messages in a moment. So, for each uninterrupted sequence of messages sent by the same person, the first thing we’ll need is another <div> with the class in. Inside that, each individual message will be its own <div>, and each of these will have the class text. Inside that, you’ll have a <p> element, in which the content of the message is stored.

So here’s a very basic example, with just one sent message:

<div class="in">
  <div class="text">
    <p>Took Yuna to the zoo today 😊</p>
  </div>
</div>

And here’s how that looks in the context of the overall structure that we’ve already seen:

group icon

boyzz 💫

You, Asshole, St Braska 🙏

Took Yuna to the zoo today 😊

group icon

boyzz 💫

You, Asshole, St Braska 🙏

Took Yuna to the zoo today 😊

There are a couple of things missing from this: an indication of who has sent the message, and a timestamp. We’ll add these as additional <p> elements within the text element, with the classes from and time respectively. For our from element, we also need to indicate a colour to represent the sender by adding the name of the colour as an additional class. Based on SMWSTW, our colour options are the following: red1, red2, pink, blue1, blue2, blue3, blue4, brown, orange, yellow, purple, green1, green2, green3, green4, and green5. Here’s an example:

<div class="in">
  <div class="text">
    <p class="from blue2">St Braska 🙏</p>
    <p>Took Yuna to the zoo today 😊</p>
    <p class="time">17:26</p>
  </div>
</div>
group icon

boyzz 💫

You, Asshole, St Braska 🙏

St Braska 🙏

Took Yuna to the zoo today 😊

17:26

group icon

boyzz 💫

You, Asshole, St Braska 🙏

St Braska 🙏

Took Yuna to the zoo today 😊

17:26

Let’s make this slightly more complex by adding a second message to the sequence. To do this, we add another <div> with the class text right after the first one. This time, there’s no need to include the first <p>, the one with the from class, because the sender’s name has already been specified in the first message. We do still include a timestamp, though. So the sequence will look something like this:

<div class="in">
  <div class="text">
    <p class="from blue2">St Braska 🙏</p>
    <p>Took Yuna to the zoo today 😊</p>
    <p class="time">17:26</p>
  </div>
  <div class="text">
    <p>She loved the lizards!</p>
    <p class="time">17:28</p>
  </div>
</div>
group icon

boyzz 💫

You, Asshole, St Braska 🙏

St Braska 🙏

Took Yuna to the zoo today 😊

17:26

She loved the lizards!

17:28

group icon

boyzz 💫

You, Asshole, St Braska 🙏

St Braska 🙏

Took Yuna to the zoo today 😊

17:26

She loved the lizards!

17:28

In sequences like this, only the first message will have the tail.

Sent messages

The syntax for sent messages is exactly the same as for received messages. Just change the class in to out for the <div> representing the overall message group, and the appearance will changes accordingly.

<div class="out">
  <div class="text">
    <p class="from blue2">Braska</p>
    <p>Took Yuna to the zoo today 😊</p>
    <p class="time">17:26</p>
  </div>
  <div class="text">
    <p>She loved the lizards!</p>
    <p class="time">17:31</p>
  </div>
</div>
group icon

boyzz 💫

You, Auron ✨, THE GREAT JECHT

Braska

Took Yuna to the zoo today 😊

17:26

She loved the lizards!

17:28

group icon

boyzz 💫

You, Auron ✨, THE GREAT JECHT

Braska

Took Yuna to the zoo today 😊

17:26

She loved the lizards!

17:28

There are a couple of things to take note of here: firstly, the <p> element telling us who has sent the message is hidden, as this evidently doesn’t show up on outgoing messages. I’d recommend keeping it there in the HTML nonetheless, for accessibility reasons and to make the code easier to modify.

Secondly, in sent messages the timestamp is automatically followed by the blue double tick symbol, meaning the message has been read by all group members. Of course, there may be cases where this hasn’t happened – we’ll see how to account for this later.

Here’s a longer exchange of messages as a further example:

<div class="in">
  <div class="text">
    <p class="from blue2">St Braska 🙏</p>
    <p>Took Yuna to the zoo today 😊</p>
    <p class="time">17:26</p>
  </div>
  <div class="text">
    <p>She loved the lizards!</p>
    <p class="time">17:28</p>
  </div>
</div>
<div class="out">
  <div class="text">
    <p class="from orange">Jecht</p>
    <p>dawwwwww</p>
    <p class="time">17:34</p>
  </div>
</div>
<div class="in">
  <div class="text">
    <p class="from red2">Asshole</p>
    <p>Cute 🦎</p>
    <p class="time">19:07</p>
  </div>
</div>
<div class="out">
  <div class="text">
    <p class="from orange">Jecht</p>
    <p>Asdfhjk auron did u jsut</p>
    <p class="time">19:08</p>
  </div>
  <div class="text">
    <p>omg did u even know theres an eomji keyboard</p>
    <p class="time">19:08</p>
  </div>
</div>
<div class="in">
  <div class="text">
    <p class="from blue2">St Braska 🙏</p>
    <p>Lol!</p>
    <p class="time">19:15</p>
  </div>
</div>
<div class="in">
  <div class="text">
    <p class="from red2">Asshole</p>
    <p>I know how phones work, Jecht.</p>
    <p class="time">19:56</p>
  </div>
</div>
group icon

boyzz 💫

You, Asshole, St Braska 🙏

St Braska 🙏

Took Yuna to the zoo today 😊

17:26

She loved the lizards!

17:28

Jecht

dawwwwww

17:34

Asshole

Cute 🦎

19:07

Jecht

Asdfhjk auron did u jsut

19:08

omg did u even know theres an eomji keyboard

19:08

St Braska 🙏

Lol!

19:15

Asshole

I know how phones work, Jecht.

19:56

group icon

boyzz 💫

You, Asshole, St Braska 🙏

St Braska 🙏

Took Yuna to the zoo today 😊

17:26

She loved the lizards!

17:28

Jecht

dawwwwww

17:34

Asshole

Cute 🦎

19:07

Jecht

Asdfhjk auron did u jsut

19:08

omg did u even know theres an eomji keyboard

19:08

St Braska 🙏

Lol!

19:15

Asshole

I know how phones work, Jecht.

19:56

Status messages

There’s one more type of message to add in addition to sent and received messages, which consists of the status messages displayed by WhatsApp to specify what day it is, or to announce a change to the group such as an edit to the group name or the addition/removal of a member. Again, we use a <div> element for these, this time with the class info, and again include a <p> inside with the content. Here are a couple of examples:

<div class="info">
  <p>Today</p>
</div>
<div class="info">
  <p>You changed the subject from “boyzz 💫” to “🦎🦎🦎”</p>
</div>
group icon

🦎🦎🦎

You, Asshole, St Braska 🙏

Today

You changed the subject from “boyzz 💫” to “🦎🦎🦎”

group icon

🦎🦎🦎

You, Asshole, St Braska 🙏

Today

You changed the subject from “boyzz 💫” to “🦎🦎🦎”

Images

To include a message that consists solely of an image, a <div> element with class img should be used inside the <div> with class text. This should then contain a <p> element with the image inside, followed by the usual <p> element with the timestamp (note that this final <p> is inside the additional <div>, while the initial <p> stating who sent the message is not). Here’s an example:

<div class="text">
  <p class="from blue2">St Braska 🙏</p>
  <div class="img">
    <p>
      <img src="https://i.ibb.co/FD1w2DC/yuna.jpg" alt="Young girl holding lizard"/>
    </p>
    <p class="time">17:34</p>
  <div>
</div>
group icon

boyzz 💫

You, Asshole, St Braska 🙏

Today

St Braska 🙏

Young girl holding lizard

17:34

group icon

boyzz 💫

You, Asshole, St Braska 🙏

Today

St Braska 🙏

Young girl holding lizard

17:34

However, if the message contains text as well as an image, both this text and the timestamp should be outside the <div> element that contains the image. This ensures that the timestamp applies to the whole message, not just the image itself.

<div class="text">
  <p class="from blue2">St Braska 🙏</p>
  <div class="img">
    <p>
      <img src="https://i.ibb.co/FD1w2DC/yuna.jpg" alt="Young girl holding lizard"/>
    </p>
  </div>
  <p>With her new friend 😂</p>
  <p class="time">17:34</p>
</div>
group icon

boyzz 💫

You, Asshole, St Braska 🙏

Today

St Braska 🙏

Young girl holding lizard

With her new friend 😂

17:34

group icon

boyzz 💫

You, Asshole, St Braska 🙏

Today

St Braska 🙏

Young girl holding lizard

With her new friend 😂

17:34

Replies

Replies that quote an earlier message can be included if we use a <div> element with the class quote. Another class should be added to this element to indicate the colour. Inside this, we can embed a <p> element with the class from to indicate who sent the message (no need to indicate the colour for a second time – it’ll inherit from the outer <div>), and then, below this, the content of the quoted message in a <p>.

<div class="text">
  <p class="from red2">Asshole</p>
  <div class="quote orange">
    <p class="from">You</p>
    <p>omg did u even know theres an eomji keyboard</p>
  </div>
  <p>I know how phones work, Jecht.</p>
  <p class="time">19:56</p>
</div>
group icon

boyzz 💫

You, Asshole, St Braska 🙏

Today

Jecht

Asdfhjk auron did u jsut

19:08

omg did u even know theres an eomji keyboard

19:08

Asshole

You

omg did u even know theres an eomji keyboard

I know how phones work, Jecht.

19:56

group icon

boyzz 💫

You, Asshole, St Braska 🙏

Today

Jecht

Asdfhjk auron did u jsut

19:08

omg did u even know theres an eomji keyboard

19:08

Asshole

You

omg did u even know theres an eomji keyboard

I know how phones work, Jecht.

19:56

As usual, if we switch our in and out classes, the message positions and colours will update accordingly. Here’s an example with a message being quoted by the sender:

group icon

boyzz 💫

You, Braska, Jecht

Today

Jecht

Asdfhjk auron did u jsut

19:08

omg did u even know theres an eomji keyboard

19:08

Auron

Jecht

omg did u even know theres an eomji keyboard

I know how phones work, Jecht.

19:56

group icon

boyzz 💫

You, Braska, Jecht

Today

Jecht

Asdfhjk auron did u jsut

19:08

omg did u even know theres an eomji keyboard

19:08

Auron

Jecht

omg did u even know theres an eomji keyboard

I know how phones work, Jecht.

19:56

Links

Links in WhatsApp (including @-mentions) show up in blue text. To enable this here, we can wrap them in a <span> with the class link.

<div class="text">
  <p class="from red2">Asshole</p>
  <p>
    <span class="link">@Jecht</span> where are you? Is anyone with you?
  </p>
  <p class="time">20:12</p>
</div>
group icon

boyzz 💫

You, Asshole, St Braska 🙏

Today

Asshole

@Jecht where are you? Is anyone with you?

20:12

group icon

boyzz 💫

You, Asshole, St Braska 🙏

Today

Asshole

@Jecht where are you? Is anyone with you?

20:12

Link previews

Valid links to websites normally show up on WhatsApp with an image preview and brief summary. We can do this using the same sort of structure as our quote class: a <div> element with class preview. This time, we just include one <p> element inside, which contains firstly the image preview, then the title for the summary inside a <span> of class preview-title, followed by a line break (i.e. a <br/> element) and then the rest of the summary.

<div class="text">
  <p class="from orange">Jecht</p>
  <div class="preview">
    <p>
      <img src="https://i.ibb.co/y0JbTDT/yevon.jpg" alt="Yevon glyph"/>
      <span class="preview-title">Spira Wildlife Park</span><br/>Witness the glory of Yevon’s creation!
    </p>
  </div>
  <p>this one? <span class="link">https://spira-wp.sp</span></p>
  <p class="time">17:28</p>
</div>
group icon

boyzz 💫

You, Asshole, St Braska 🙏

Today

Jecht

Yevon glyph Spira Wildlife Park
Witness the glory of Yevon’s creation!

this one? https://spira-wp.sp

17:28

group icon

boyzz 💫

You, Asshole, St Braska 🙏

Today

Jecht

Yevon glyph Spira Wildlife Park
Witness the glory of Yevon’s creation!

this one? https://spira-wp.sp

17:28

As before, if we switch our out and in classes, the colours will update to match.

group icon

boyzz 💫

You, Auron ✨, THE GREAT JECHT

Today

THE GREAT JECHT

Yevon glyph Spira Wildlife Park
Witness the glory of Yevon’s creation!

this one? https://spira-wp.sp

17:28

group icon

boyzz 💫

You, Auron ✨, THE GREAT JECHT

Today

THE GREAT JECHT

Yevon glyph Spira Wildlife Park
Witness the glory of Yevon’s creation!

this one? https://spira-wp.sp

17:28

Unread messages

As mentioned above, sent messages automatically include a double blue tick after the timestamp to indicate that they’ve been read by everyone. When this doesn’t apply we can add the class unread to the <p> element containing the timestamp. For messages that have been sent but not received by all recipients, use the class unsent instead (this is slightly inaccurately named, but please forgive me the brief fandom reference).

<div class="out">
  <div class="text">
    <p class="from red2">Auron</p>
    <p>Are you drunk?</p>
    <p class="time">19:58</p>
  </div>
  <div class="text">
    <p>
      <span class="link">@Jecht</span> where are you? Is anyone with you?
    </p>
    <p class="time unread">20:12</p>
  </div>
  <div class="text">
    <p>We’ve talked about this.</p>
    <p class="time unsent">20:13</p>
  </div>
</div>
group icon

boyzz 💫

Jecht is typing …

Today

Auron

Are you drunk?

19:58

@Jecht where are you? Is anyone with you?

20:12

We’ve talked about this.

20:13

group icon

boyzz 💫

Jecht is typing …

Today

Auron

Are you drunk?

19:58

@Jecht where are you? Is anyone with you?

20:12

We’ve talked about this.

20:13

These look slightly different if the message consists solely of an image (and remember that the way the elements are nested is slightly different in this case):

group icon

boyzz 💫

You, Auron ✨, THE GREAT JECHT

Today

Braska

Pineapple

17:34

Lol just realised I sent the wrong picture, old age haha

17:47

Auron and Jecht

17:49

No not that one

17:49

Young girl holding lizard

17:50

group icon

boyzz 💫

You, Auron ✨, THE GREAT JECHT

Today

Braska

Pineapple

17:34

Lol just realised I sent the wrong picture, old age haha

17:47

Auron and Jecht

17:49

No not that one

17:49

Young girl holding lizard

17:50

Oversized emoji

WhatsApp increases the size of emoji if they appear in groups of three or fewer with no other text in the same message. To achieve this here, we can wrap the contents of the message (the two <p> elements with the emoji and the timestamp, but again, not the <p> with the name of the sender) in another <div> and give this the class onemoji, twomoji or threemoji as appropriate. Here are examples of all three:

<div class="out">
  <div class="text">
    <p class="from orange">Jecht</p>
    <div class="onemoji">
      <p>💖</p>
      <p class="time">17:35</p>
    </div>
  </div>
  <div class="text">
    <div class="twomoji">
      <p>💖💖</p>
      <p class="time">17:35</p>
    </div>
  </div>
  <div class="text">
    <div class="threemoji">
      <p>💖💖💖</p>
      <p class="time">17:35</p>
    </div>
  </div>
</div>
group icon

boyzz 💫

You, Asshole, St Braska 🙏

Today

Jecht

💖

17:35

💖💖

17:35

💖💖💖

17:35

Asshole

We get the message.

17:57

group icon

boyzz 💫

You, Asshole, St Braska 🙏

Today

Jecht

💖

17:35

💖💖

17:35

💖💖💖

17:35

Asshole

We get the message.

17:57

The bottom bar

We can add text in the bottom bar to show a message that is being typed by the focal character but isn’t sent yet. To do this, we add another <div> inside the <div> with class bottombar; the new <div> should have the class typearea. You can then insert a <p> inside this with the content of the unsent message (and if you include an indication of the sender here using the from class, this will be hidden as it is for sent messages). The content will be truncated automatically.

<div class="bottombar">
  <div class="typearea">
    <p class="from">Jecht</p>
    <p>chill aurofn im finnee</p>
  </div>
</div>
group icon

boyzz 💫

You, Asshole, St Braska 🙏

Today

Asshole

Are you drunk?

19:58

@Jecht where are you? Is anyone with you?

20:12

Jecht

chill aurofn im finnee

group icon

boyzz 💫

You, Asshole, St Braska 🙏

Today

Asshole

Are you drunk?

19:58

@Jecht where are you? Is anyone with you?

20:12

Jecht

chill aurofn im finnee

Chapter Text

Here are some issues that I’d like to work on in future versions:

  • The header could be more realistic;
  • After much fiddling, timestamp positioning still isn’t quite right – ideally, the baseline of the timestamp should line up with the text of the message in a way that works across platforms (instead of the current compromise, which is slightly off on both PC and mobile); also, the timestamps on images don’t line up with the others;
  • Text spacing/padding is also slightly off, although seems better when the correct fonts are installed;
  • Ideally, it would be nice for onemoji and related classes to be detected automatically, although even if this can be done with CSS I suspect it would require a feature that AO3 doesn’t support.

And here are some features that I initially implemented but had to remove, and which could perhaps be reinstated in the distant future:

  • Optional CSS-driven image blurring (relies on the blur function, not currently supported by AO3);
  • Storage of timestamps in the title attribute and placement in an after pseudoelement, making these invisible when the workskin is turned off (relies on content: attr(title), not currently supported by AO3);
  • Use of CSS grid for the layout of the header (also not supported by AO3, although I replaced this with a table and it looks pretty much the same).

Feel free to build on this work or to suggest improvements in the comments. I am a mere dabbler in CSS and would love to further my experience 😊