jockles.com

All the Fannish things I do.

Megaskin by Jockles

← Previous Chapter · Chapter Index · Next Chapter →

Chapter 5: iMessage CSS

Thanks to CodenameCarrot and La_Temperanza, whose How to Make iOS Text Messages on AO3 this module is built on. Copied and adapted from it: picture messages, the typing dots, and the hidden text for readers with the style off.

5.1 Chapter Contents

This chapter goes through the CSS for the iMessage chats in the last chapter. It's the simplest of the modules, so it's a good one to learn from: every other module is built the same way, just with more parts.

5.2 Getting the CSS

The iMessage CSS is here: jockles.com/fic/megaskin/css/imessage.css. It needs the core CSS too (jockles.com/fic/megaskin/css/core.css), so paste the core into your work skin first and this underneath it.

5.3 How the Module is Laid Out

Every module is in the same order, so once you know your way around this one, you know your way around all of them:

  1. The main rules, which are the light theme: the window, the bubbles, and every other part.
  2. Links: the colour of links and fake-links.
  3. Dark theme: the colours that change for theme-dark.
  4. Auto theme: the same dark colours again, for theme-auto on a device in dark mode.

Every rule in it has .imsg in it, so it can only ever touch iMessage chats.

5.4 The Window

#workskin .imsg {
  background: #ffffff;
  color: #000000;
  border: 1px solid #d8d8dc;
  border-radius: 14px;
}

#workskin .imsg .mob-head {
  background: #f6f6f8;
  color: #000000;
  border-bottom: 1px solid #d8d8dc;
  font-size: 0.85em;
}

#workskin .imsg .mob-head span {
  display: block;
  font-weight: normal;
  color: #8e8e93;
  font-size: 0.85em;
}

The core has already made the box phone-sized, so all this adds is the look: white, with a thin grey edge and rounded corners. The header is a slightly greyer white with a line under it. display: block on the span in the header is what puts "iMessage" on its own line under the name - a span normally sits on the same line as the text before it.

5.5 Bubbles

#workskin .imsg .msg {
  display: table;
  max-width: 75%;
  margin: 0.18em 0;
  padding: 0.45em 0.8em;
  border-radius: 18px;
}

#workskin .imsg .out {
  margin-left: auto;
  background: #0a84ff;
  color: #ffffff;
  border-bottom-right-radius: 5px;
}

#workskin .imsg .in {
  margin-right: auto;
  background: #e9e9eb;
  color: #000000;
  border-bottom-left-radius: 5px;
}
  • display: table makes a paragraph only as wide as its words. A normal paragraph stretches across the whole chat, which would make every bubble the same width.
  • max-width: 75% stops a long message going all the way across, so there's always a gap on the other side, like on a phone.
  • margin-left: auto pushes a sent bubble over to the right: auto takes up all of the spare room on that side. Received bubbles do the same on the right, which keeps them on the left.
  • border-radius: 18px rounds every corner, and the smaller 5px corner at the bottom makes the little point on the side the bubble came from.
Changing the bubble colours
  • Sent bubbles: #0a84ff is iMessage blue. It's also used for your own tapbacks, and it appears in the dark and auto themes too, so change all of them - your text editor's find and replace is the easiest way.
  • Received bubbles: #e9e9eb in light mode, and #262628 in the dark and auto themes.
  • If you make a bubble lighter, change its color (the text) to something dark, so it's still easy to read.

5.6 Green Bubbles

#workskin .imsg .out.sms { background: #30d158; color: #ffffff; }

.out.sms means a bubble that has both out and sms. Received messages are never green on a real iPhone, so the rule only works on sent ones - in sms does nothing.

5.7 Names, Status and System Lines

#workskin .imsg .who {
  display: block;
  font-size: 0.72em;
  color: #8e8e93;
  margin: 0.5em 0 0 0.9em;
}

#workskin .imsg .imsg-status {
  display: block;
  text-align: right;
  font-size: 0.7em;
  color: #8e8e93;
  margin: 0.1em 0.4em 0.4em 0;
}

#workskin .imsg .mob-sys { color: #8e8e93; opacity: 1; }

#8e8e93 is Apple's grey, and it's used for all the small print. The who line is nudged in from the left (0.9em) so the name lines up with the text inside the bubble, not the edge of it. The core fades system lines with opacity, and iMessage puts it back to 1 because its grey is already faded enough.

5.8 Edited Messages

#workskin .imsg .imsg-edited, #workskin .imsg .imsg-edited-in {
  display: block;
  text-align: right;
  font-size: 0.7em;
  color: #0a84ff;
  margin: 0.1em 0.4em 0.4em 0;
}

#workskin .imsg .imsg-edited-in { text-align: left; margin: 0.1em 0 0.4em 0.4em; }

It's the same small print as the status line, but in Apple's blue (#0a84ff), which shows up on both light and dark chats. imsg-edited-in is the left-hand version, under a message from the other person.

5.9 Tapbacks

Tapbacks are the fiddliest part, because they sit half outside the bubble. They work by taking the reaction out of the normal flow of the text and pinning it to the bubble's corner:

#workskin .imsg .msg.tapped {
  position: relative;
  margin-top: 1.7em;
}

#workskin .imsg .imsg-tap {
  position: absolute;
  top: -1.75em;
  right: -0.95em;
  min-width: 2.15em;
  height: 2.15em;
  padding: 0 0.35em;
  box-sizing: border-box;
  border: 2px solid #ffffff;
  border-radius: 1.075em;
  background: #e9e9eb;
  color: #8e8e93;
  font-size: 1em;
  line-height: 1.9em;
  text-align: center;
  white-space: nowrap;
}
  • position: relative on the bubble and position: absolute on the tapback go together. The tapback is placed using the bubble's edges, and top: -1.75em and right: -0.95em move it up and out past the top right corner.
  • Because it's been taken out of the flow, the tapback doesn't push anything out of the way, so tapped adds space above the bubble (margin-top: 1.7em) for it to sit in. That's the only thing tapped does.
  • The white border is the same colour as the chat, so it looks like a little gap between the tapback and the bubble behind it.
  • border-radius is half the height, which makes a circle, or a pill shape if the emoji is wide.

The two tail dots are made from nothing at all:

#workskin .imsg .imsg-tap::before,
#workskin .imsg .imsg-tap::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  background: #e9e9eb;
}

#workskin .imsg .imsg-tap::before {
  right: -0.2em;
  bottom: -0.2em;
  width: 0.5em;
  height: 0.5em;
  border: 2px solid #ffffff;
}

#workskin .imsg .imsg-tap::after {
  right: -0.55em;
  bottom: -0.5em;
  width: 0.25em;
  height: 0.25em;
}

::before and ::after add two extra boxes to every tapback, with content: "" meaning there's no text in them. Each one is rounded into a circle and placed off the bottom corner, a big one and then a small one, trailing away.

#workskin .imsg .out .imsg-tap { right: auto; left: -0.95em; }

#workskin .imsg .out .imsg-tap::before { right: auto; left: -0.2em; }

#workskin .imsg .out .imsg-tap::after { right: auto; left: -0.55em; }

#workskin .imsg .imsg-tap.me { background: #0a84ff; color: #ffffff; }

#workskin .imsg .imsg-tap.me::before,
#workskin .imsg .imsg-tap.me::after { background: #0a84ff; }

On your own messages, everything is flipped to the left: right: auto cancels the right-hand position, and left takes over. me turns the tapback and both its dots blue.

Stacked tapbacks in group chats
#workskin .imsg .imsg-tap { z-index: 4; }

#workskin .imsg .imsg-tap + .imsg-tap { z-index: 3; }

#workskin .imsg .imsg-tap + .imsg-tap + .imsg-tap { z-index: 2; }

#workskin .imsg .imsg-tap + .imsg-tap + .imsg-tap + .imsg-tap { z-index: 1; }

#workskin .imsg .imsg-tap + .imsg-tap { right: 0.5em; }

#workskin .imsg .imsg-tap + .imsg-tap + .imsg-tap { right: 1.95em; }

#workskin .imsg .imsg-tap + .imsg-tap + .imsg-tap + .imsg-tap { right: 3.4em; }

#workskin .imsg .imsg-tap + .imsg-tap::before,
#workskin .imsg .imsg-tap + .imsg-tap::after { display: none; }
  • + means "straight after". So .imsg-tap + .imsg-tap is the second tapback, and adding another + .imsg-tap each time gets the third and fourth.
  • Each one is moved a step further along the bubble (0.5em, 1.95em, 3.4em), so they overlap like a fan.
  • z-index decides which is in front. The first has the highest number, so it's on top.
  • Only the first keeps its tail dots.
  • Sent messages have the same three lines with left instead of right. A fifth tapback would sit on top of the fourth - add another line with 4.85em if you ever need one.
Keeping a sender's name next to their tapped message
#workskin .imsg .who + .msg.tapped { margin-top: 0.18em; }

#workskin .imsg .who:has(+ .msg.tapped) { margin-top: 1.5em; }

In a group chat, the space tapped adds would go between the name and the message, which looks wrong. The first line takes the space away from a tapped message that comes straight after a name. The second puts it above the name instead: :has(+ .msg.tapped) means "a name line that has a tapped message straight after it". If a reader's browser doesn't understand :has, the name just sits a bit closer to the message above - nothing breaks.

5.10 Pictures

#workskin .imsg .msg.pic { width: 55%; padding: 0.2em; overflow: hidden; }

#workskin .imsg .msg.pic img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 15px;
}

#workskin .imsg .msg.pic.text { padding: 2.5em 1em; font-style: italic; text-align: center; }

A picture bubble is a fixed 55% of the chat wide, whatever size the picture is, and the picture fills it. height: auto keeps the picture's shape, so nothing gets squashed. With text, the description gets lots of padding so it's about the size of a photo.

5.11 Typing Dots

#workskin .imsg .typing-indicator {
  display: table;
  margin: 0.3em 0;
  padding: 0.6em 0.8em;
  border-radius: 18px;
  border-bottom-left-radius: 5px;
  background: #e9e9eb;
}

#workskin .imsg .typing-indicator span {
  display: inline-block;
  width: 0.6em;
  height: 0.6em;
  margin-right: 0.25em;
  border-radius: 50%;
  background: #8e8e93;
  opacity: 0.5;
}

#workskin .imsg .typing-indicator span:nth-child(2) { opacity: 0.7; }

#workskin .imsg .typing-indicator span:nth-child(3) { opacity: 1; margin-right: 0; }

The bubble is built like a received message. Each empty span becomes a small grey circle, and :nth-child(2) and :nth-child(3) - the second and third dot - get darker, so it looks like the dots are mid-bounce.

5.12 Message Bar

#workskin .imsg .imsg-input {
  display: flex;
  align-items: center;
  margin: 0;
  padding: 0.45em 0.7em 0.65em 0.7em;
}

#workskin .imsg .imsg-plus {
  flex: none;
  width: 1.55em;
  height: 1.55em;
  margin-right: 0.35em;
  border-radius: 50%;
  box-sizing: border-box;
  border: 1px solid #8e8e93;
  background: #ffffff;
  color: #000000;
  font-size: 1.3em;
  font-weight: 300;
  line-height: 1.45em;
  text-align: center;
}

#workskin .imsg .imsg-field {
  flex: 1;
  position: relative;
  padding: 0.45em 2.4em 0.45em 0.9em;
  border: 1px solid #8e8e93;
  border-radius: 1.3em;
  background: #f0f0f2;
  color: #6c6c70;
}

#workskin .imsg .imsg-mic {
  position: absolute;
  right: 0.7em;
  top: 50%;
  transform: translateY(-50%);
  font-size: 0.95em;
}
  • display: flex lines the + button and the box up in a row, and align-items: center centres them against each other.
  • flex: none keeps the + button the same size, and flex: 1 lets the box take up all the room that's left.
  • The microphone is pinned inside the right end of the box, the same way as a tapback. top: 50% with translateY(-50%) is the usual way to centre something from top to bottom: move its top edge halfway down, then move it back up by half its own height.
  • The extra padding on the right of the box (2.4em) leaves room for the microphone, so the word never runs under it.

For a half-typed message, typed turns the words black (white in dark mode), and the send arrow takes the microphone's place:

#workskin .imsg .imsg-field.typed { color: #000000; }
#workskin .imsg .imsg-send {
  position: absolute;
  right: 0.3em;
  bottom: 0.3em;
  width: 1.7em;
  height: 1.7em;
  line-height: 1.7em;
  border-radius: 50%;
  background: #0a84ff;
  color: #ffffff;
  font-weight: bold;
  text-align: center;
}

The arrow is pinned to the bottom corner rather than the middle, so on a long message it sits by the last line, like the real one.

5.13 Links

#workskin .imsg .in .fake-link, #workskin .imsg .in a:not(.tl) { color: #0a84ff; }

#workskin .imsg .out .fake-link, #workskin .imsg .out a:not(.tl) { color: #ffffff; }

Links are blue in grey bubbles and white in blue ones, so they show up on both.

5.14 Dark and Auto Themes

The dark theme is the light rules again, with only the colours that change:

#workskin .imsg.theme-dark { background: #000000; color: #ffffff; border-color: #2c2c2e; }

#workskin .imsg.theme-dark .mob-head { background: #1c1c1e; color: #ffffff; border-bottom-color: #2c2c2e; }

#workskin .imsg.theme-dark .in,
#workskin .imsg.theme-dark .typing-indicator {
  background: #262628;
  color: #ffffff;
}

...and so on for the tapbacks and the message bar. Sent bubbles don't appear, because iMessage blue is the same in both.

The auto theme is exactly the same block, with theme-auto in place of theme-dark, inside this:

@media (prefers-color-scheme: dark) {
  #workskin .imsg.theme-auto { background: #000000; color: #ffffff; border-color: #2c2c2e; }
  ...
}

Everything inside @media (prefers-color-scheme: dark) { ... } only counts when the reader's device is in dark mode. Otherwise theme-auto doesn't match anything, and the chat uses the light rules.

Changing a dark colour

Because AO3 doesn't allow variables, a dark colour is written in both the dark block and the auto block. Change it in both, or theme-dark and theme-auto chats won't match. Find and replace in a text editor catches every copy at once.

5.15 Taking Parts Out

If you never use a part, you can delete its rules to make your skin shorter. Each part has a comment above it saying what it is. The ones that come out cleanly are:

  • Tapbacks: everything with tapped or imsg-tap in it, including the lines in the dark and auto blocks.
  • Green bubbles: the .out.sms line.
  • Pictures: the three .msg.pic rules.
  • Typing dots: the four typing-indicator rules, and .typing-indicator in the dark and auto blocks.
  • Message bar: everything with imsg-input, imsg-plus, imsg-field or imsg-mic in it.

Leave the window, the bubbles and the theme blocks, which everything else depends on.

← Previous Chapter · Chapter Index · Next Chapter →