Screenshot of a WhatsApp chat about lighthouse power status.

AO3 text message skins: iMessage and WhatsApp, done properly

A texting scene is the most common social-media moment in fanfiction and the easiest one to get subtly wrong. Readers know what these apps look like. If the reaction sits in the wrong place, or every message carries a read receipt, or the delivery ticks never change, the scene reads as an approximation of a phone rather than a phone. This page contains complete, working iMessage and WhatsApp work skins for AO3, plus the small details that separate the two.

The quickest route: open the AO3 Skin Generator, choose iMessage or WhatsApp, build the conversation, and click Work skin. Everything below explains what that output contains and why it is shaped the way it is. For an overview of every feature, see the AO3 Skin Generator guide.

How a work skin works, in one paragraph

Two pieces have to be in place. The work skin holds the CSS — bubble shapes, colours, spacing, the tail on the last message in a run. The chapter HTML holds this particular conversation: these names, these lines, these timestamps. Save the CSS once per fic, then paste fresh HTML into each chapter that needs a conversation. The Twitter work skin guide covers this split in more detail; the installation steps are identical here.

Fictional iMessage group conversation rendered with an AO3 work skin, showing a quote-reply and two Tapbacks
The iMessage version: Tapbacks on the bubble, a read receipt under the last message.
The same fictional conversation rendered as a WhatsApp group chat with a reaction pill and delivery ticks
The same beat on WhatsApp: a reaction pill and blue double ticks.

Installing either skin

  1. Sign in to AO3 and open Dashboard → Skins.
  2. Open My Work Skins, then choose Create Work Skin.
  3. Give it a title you will recognise, such as yourname — texting.
  4. Paste the CSS from below into the CSS field and save.
  5. Edit your work. Under Associations, choose the skin under Select Work Skin.
  6. Open the chapter editor’s HTML mode and paste the conversation HTML.
  7. Preview the chapter with Creator’s Style on and off before posting.

Paste into AO3’s HTML mode, never the Rich Text editor. Rich Text will rewrite the markup, and the most common symptom is a conversation that renders as a single grey block.

iMessage: the complete CSS

This is the exact light-theme CSS the generator produced for the example above, and it passes the same AO3 allowlist and value checks the app applies before it lets you copy anything. Take the whole block — a partial stylesheet gives you half-styled bubbles.

AO3 iMessage work-skin CSS
#workskin .chat{width:100%;max-width:23.438em;min-width:20em;margin:0 auto;display:flex;flex-direction:column;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;padding:0;background:#fff;}
#workskin .chat p{margin:0;padding:0;}
#workskin .ios-header{position:relative;background:#f6f6f6;min-height:3.75em;display:flex;align-items:center;padding:0.5em 0.6em;border-bottom:1px solid #d1d1d6;color:#000;}
#workskin .ios-back,#workskin .ios-header-actions{color:#007aff;font-size:1.35em;line-height:1;flex-shrink:0;}
#workskin .ios-back{margin-right:0.35em;}
#workskin .ios-header-actions{margin-left:0.35em;}
#workskin .ios-header-name-wrapper{min-width:0;flex:1;text-align:center;}
#workskin .ios-header-subtitle{font-size:0.688em;line-height:1.2;opacity:0.7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
#workskin .ios-header-avatar,#workskin .ios-header-avatar-placeholder{width:2.375em;height:2.375em;border-radius:50%;overflow:hidden;flex-shrink:0;display:block;margin-right:0.5em;}
#workskin .ios-header-avatar img{width:100%;height:100%;}
#workskin .ios-header-avatar-placeholder{background:rgba(0,0,0,0.06);color:#000;text-align:center;line-height:2.375em;font-size:0.875em;font-weight:700;}
#workskin .ios-header-name{font-size:0.938em;font-weight:600;color:#000;line-height:1.25;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
#workskin .ios-status-bar{background:#f6f6f6;padding:0.429em 1.143em 0.286em 1.143em;display:flex;justify-content:space-between;align-items:center;font-size:0.875em;font-weight:600;color:#000;border-bottom:1px solid #e0e0e0;}
#workskin .ios-status-bar p{display:contents;}
#workskin .ios-status-bar .time{flex:1;text-align:center;}
#workskin .ios-status-bar .status-icons{display:flex;align-items:center;font-size:0.857em;}
#workskin .ios-status-bar .status-icons > *{margin-left:0.333em;}
#workskin .ios-status-bar .status-icons > *:first-child{margin-left:0;}
#workskin .chat-header{text-align:center;font-size:0.813em;color:#86868b;padding:0.615em 0.923em 0.462em 0.923em;margin-bottom:0.308em;font-weight:400;background:#fff;}
#workskin .chat-header .to-label{font-weight:400;color:#86868b;margin-right:0.308em;}
#workskin .chat-header .contact-name{font-weight:600;color:#000;display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
#workskin .chat-messages{padding:0.75em 0.5em;background:#fff;}
#workskin .time-break{text-align:center;font-size:0.688em;color:#86868b;margin:1.091em 0 0.727em 0;font-weight:500;}
#workskin .row{display:flex;margin:0 0 0 -0.375em;align-items:flex-end;flex-wrap:wrap;width:100%;}
#workskin .row > *{margin-left:0.375em;}
#workskin .row.single{margin:0.75em 0;}
#workskin .row.first{margin:0.75em 0 0.125em 0;}
#workskin .row.middle{margin:0.125em 0;}
#workskin .row.last{margin:0.125em 0 0.75em 0;}
#workskin .row.out{justify-content:flex-end;}
#workskin .row.in{justify-content:flex-start;}
#workskin img.avatar{width:1.75em;height:1.75em;border-radius:50%;overflow:hidden;flex-shrink:0;}
#workskin dl.msg{margin:0;display:flex;flex-direction:column;margin-top:-0.063em;}
#workskin dl.msg > *{margin-top:0.063em;}
#workskin .row.out dl.msg{align-items:flex-end;}
#workskin .row.in dl.msg{align-items:flex-start;}
#workskin dt.sender{font-size:0.688em;color:rgba(60,60,67,0.6);margin:0.545em 0 0.182em 3.273em;font-weight:500;max-width:60%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
#workskin dd{margin:0;}
#workskin dd.bubble{position:relative;display:inline-block;min-width:0;max-width:17.333em;padding:0.533em 0.8em;border-radius:1.2em;line-height:1.35;font-size:0.938em;white-space:normal;word-break:keep-all;overflow-wrap:anywhere;}
#workskin .ios-tone-red{color:#c8102e;}
#workskin blockquote.ios-reply-red{border-left-color:#c8102e;}
#workskin .ios-tone-orange{color:#a2500a;}
#workskin blockquote.ios-reply-orange{border-left-color:#a2500a;}
#workskin .ios-tone-yellow{color:#7a5d00;}
#workskin blockquote.ios-reply-yellow{border-left-color:#7a5d00;}
#workskin .ios-tone-green{color:#1b6b2c;}
#workskin blockquote.ios-reply-green{border-left-color:#1b6b2c;}
#workskin .ios-tone-mint{color:#0c6b63;}
#workskin blockquote.ios-reply-mint{border-left-color:#0c6b63;}
#workskin .ios-tone-teal{color:#00637a;}
#workskin blockquote.ios-reply-teal{border-left-color:#00637a;}
#workskin .ios-tone-cyan{color:#0a6b8f;}
#workskin blockquote.ios-reply-cyan{border-left-color:#0a6b8f;}
#workskin .ios-tone-blue{color:#0a58ca;}
#workskin blockquote.ios-reply-blue{border-left-color:#0a58ca;}
#workskin .ios-tone-indigo{color:#4b3fbb;}
#workskin blockquote.ios-reply-indigo{border-left-color:#4b3fbb;}
#workskin .ios-tone-purple{color:#7a3fb5;}
#workskin blockquote.ios-reply-purple{border-left-color:#7a3fb5;}
#workskin .ios-tone-pink{color:#b01e57;}
#workskin blockquote.ios-reply-pink{border-left-color:#b01e57;}
#workskin .ios-tone-brown{color:#7a5c46;}
#workskin blockquote.ios-reply-brown{border-left-color:#7a5c46;}
#workskin .message-text{display:inline;}
#workskin .chat.ios-scroll .chat-messages{height:34em;overflow-y:auto;overflow-x:hidden;}
#workskin .ios-group-sender{display:inline-block;font-size:0.733em;font-weight:600;line-height:1.2;margin-bottom:0.35em;vertical-align:middle;}
#workskin dd.bubble .group-avatar,#workskin dd.bubble .group-avatar-initials{display:inline-block;width:1.333em;height:1.333em;border-radius:50%;vertical-align:middle;margin-right:0.35em;margin-bottom:0.35em;flex-shrink:0;}
#workskin dd.bubble .group-avatar-initials{background:rgba(0,0,0,0.06);text-align:center;font-size:0.5em;width:1.6em;height:1.6em;line-height:1.6em;font-weight:700;white-space:nowrap;}
#workskin blockquote.ios-reply{display:block;margin:0 0 0.45em 0;padding:0.35em 0.5em;border-left:0.25em solid #8e8e93;border-radius:0.3em;background:rgba(0,0,0,0.06);font-style:normal;color:inherit;overflow:hidden;}
#workskin blockquote.ios-reply b,#workskin blockquote.ios-reply span{display:block;font-style:normal;color:inherit;}
#workskin blockquote.ios-reply b{font-size:0.75em;opacity:0.85;}
#workskin blockquote.ios-reply span{font-size:0.8em;}
#workskin .ios-reply-thumb{float:right;width:2.5em;height:2.5em;border-radius:0.25em;margin-left:0.4em;}
#workskin blockquote.ios-reply-missing{border-left-color:#c8102e;}
#workskin .ios-images{display:block;margin-top:0.4em;font-size:0;overflow:hidden;border-radius:0.7em;}
#workskin .ios-image{display:block;width:100%;height:auto;margin:0;}
#workskin .ios-images .image-layout-row,#workskin .ios-images .image-layout-columns{display:block;width:100%;font-size:0;overflow:hidden;}
#workskin .ios-images .image-layout-row{margin-bottom:1%;}
#workskin .ios-images .image-layout-last-row{margin-bottom:0;}
#workskin .ios-images .image-layout-cell,#workskin .ios-images .image-layout-column{display:inline-block;vertical-align:top;overflow:hidden;}
#workskin .ios-images .image-layout-first{margin-right:1%;}
#workskin .ios-images .image-split-33-67 .image-layout-first{width:32.5%;}#workskin .ios-images .image-split-33-67 .image-layout-second{width:66.5%;}
#workskin .ios-images .image-split-40-60 .image-layout-first{width:39.5%;}#workskin .ios-images .image-split-40-60 .image-layout-second{width:59.5%;}
#workskin .ios-images .image-split-50-50 .image-layout-first{width:49.5%;}#workskin .ios-images .image-split-50-50 .image-layout-second{width:49.5%;}
#workskin .ios-images .image-split-60-40 .image-layout-first{width:59.5%;}#workskin .ios-images .image-split-60-40 .image-layout-second{width:39.5%;}
#workskin .ios-images .image-split-67-33 .image-layout-first{width:66.5%;}#workskin .ios-images .image-split-67-33 .image-layout-second{width:32.5%;}
#workskin .ios-images .image-layout-feature,#workskin .ios-images .image-layout-stack-item,#workskin .ios-images .image-layout-side-item{display:block;width:100%;overflow:hidden;}
#workskin .ios-images .image-layout-feature,#workskin .ios-images .image-layout-stack-item{margin-bottom:1%;}
#workskin .ios-images .image-layout-side-item{margin-bottom:2%;}
#workskin .ios-images .image-layout-last-item{margin-bottom:0;}
#workskin a.ios-link-preview{display:block;margin-top:0.4em;border-radius:0.7em;overflow:hidden;background:rgba(0,0,0,0.06);color:inherit;text-decoration:none;padding-bottom:0.4em;}
#workskin a.ios-link-preview b,#workskin a.ios-link-preview span{display:block;margin:0.25em 0.55em 0 0.55em;}
#workskin .ios-link-image{display:block;width:100%;height:auto;margin:0;}
#workskin .ios-link-description{font-size:0.85em;opacity:0.85;}
#workskin .ios-link-site,#workskin .ios-link-url{font-size:0.75em;opacity:0.7;overflow-wrap:anywhere;}
#workskin .ios-audio-card,#workskin .ios-video-card{display:block;position:relative;margin-top:0.4em;padding:0.5em;border-radius:0.7em;background:rgba(0,0,0,0.06);overflow:hidden;}
#workskin .ios-audio-play{display:inline-block;width:1.8em;vertical-align:middle;}
#workskin .ios-waveform{display:inline-block;width:11em;height:2.2em;vertical-align:middle;white-space:nowrap;overflow:hidden;}
#workskin .ios-wave-bar{display:inline-block;width:0.13em;margin-right:0.11em;vertical-align:middle;background:#6e6e73;border-radius:0.1em;}
#workskin .ios-wave-1{height:0.4em;}#workskin .ios-wave-2{height:0.7em;}#workskin .ios-wave-3{height:1em;}#workskin .ios-wave-4{height:1.3em;}#workskin .ios-wave-5{height:1.6em;}#workskin .ios-wave-6{height:1.9em;}
#workskin .ios-audio-duration,#workskin .ios-audio-transcript,#workskin .ios-video-title,#workskin .ios-video-duration,#workskin .ios-media-description,#workskin .ios-captions,#workskin .ios-media-source{display:block;font-size:0.75em;margin-top:0.25em;}
#workskin .ios-video-title{font-weight:600;}
#workskin .ios-media-source{color:inherit;text-decoration:underline;overflow-wrap:anywhere;}
#workskin .ios-native-audio{display:block;width:100%;margin-top:0.45em;}
#workskin .ios-video-poster,#workskin .ios-native-video,#workskin .ios-youtube-player iframe{display:block;width:100%;height:auto;border:0;border-radius:0.5em;background:#000;}
#workskin .ios-youtube-player iframe{height:11.25em;max-width:100%;}
#workskin .ios-video-placeholder{display:block;box-sizing:border-box;min-height:8em;padding:3.5em 1em;text-align:center;background:#1c1c1e;color:#aeaeb2;border-radius:0.5em;}
#workskin .ios-video-play{position:absolute;left:45%;top:2.5em;font-size:2em;color:#fff;text-shadow:0 1px 3px #000;}
#workskin .ios-tapbacks{position:absolute;top:-0.75em;background:#e9e9eb;color:#000;border:0.125em solid #fff;border-radius:0.875em;padding:0.063em 0.4em;font-size:0.813em;line-height:1.35;box-shadow:0 1px 3px rgba(0,0,0,0.2);white-space:nowrap;z-index:2;}
#workskin dd.bubble.out .ios-tapbacks{left:-0.5em;}
#workskin dd.bubble.in .ios-tapbacks{right:-0.5em;}
#workskin dd.bubble.has-tapbacks{padding-top:0.95em;margin-top:1.2em;}
#workskin .ios-event{text-align:center;margin:0.9em 0;color:#86868b;}
#workskin .ios-event dl{margin:0;}
#workskin .ios-event dd{display:inline-block;font-size:0.688em;font-weight:600;padding:0.2em 0.5em;}
#workskin .ios-event-system dd{max-width:82%;font-weight:400;}
#workskin dd.bubble.image-bubble{padding:0.533em 0.8em;max-width:60%;overflow:visible;}
#workskin dd.bubble.multi-image-bubble{width:17.333em;max-width:82%;box-sizing:border-box;}
#workskin dd.bubble.image-bubble img.message-image{width:100%;height:auto;display:block;border-radius:0.8em;margin-top:0.4em;}
#workskin dd.bubble.image-bubble.out{border-bottom-right-radius:0.267em;}
#workskin dd.bubble.image-bubble.out img.message-image{border-bottom-right-radius:0.267em;}
#workskin dd.bubble.image-bubble.in{border-bottom-left-radius:0.267em;}
#workskin dd.bubble.image-bubble.in img.message-image{border-bottom-left-radius:0.267em;}
#workskin dd.bubble.out{background:rgba(0, 122, 255, 1);color:#fff;border-bottom-right-radius:0.267em;}
#workskin dd.bubble.out .bubble-tail{display:none;}
#workskin dd.bubble.out.has-tail .bubble-tail-out{display:block;position:absolute;right:-0.533em;bottom:-0.067em;color:rgba(0, 122, 255, 1);}
#workskin dd.bubble.in{background:rgba(233, 233, 235, 1);color:#000;border-bottom-left-radius:0.267em;}
#workskin dd.bubble.in .bubble-tail{display:none;}
#workskin dd.bubble.in.has-tail .bubble-tail-in{display:block;position:absolute;left:-0.533em;bottom:-0.067em;color:rgba(233, 233, 235, 1);}
#workskin dd.bubble.emoji-only{background:transparent;box-shadow:none;padding:0.133em 0.2em;border-radius:0;overflow:visible;}
#workskin dd.bubble.emoji-only .emoji-content{display:block;line-height:1.05;white-space:nowrap;}
#workskin dd.bubble.emoji1 .emoji-content{font-size:4em;}
#workskin dd.bubble.emoji2 .emoji-content{font-size:2.4em;}
#workskin dd.bubble.emoji-only.out .time{color:rgba(60,60,67,0.6);}
#workskin .chat.css-tails dd.bubble.out.has-tail::after{content:'';position:absolute;right:-0.4em;bottom:0;width:0.533em;height:1.067em;border-right:8px solid rgba(0, 122, 255, 1);border-bottom-right-radius:1.067em 0.533em;}
#workskin .chat.css-tails dd.bubble.in.has-tail::after{content:'';position:absolute;left:-0.4em;bottom:0;width:0.533em;height:1.067em;border-left:8px solid rgba(233, 233, 235, 1);border-bottom-left-radius:1.067em 0.533em;}
#workskin dd.bubble.out .time{display:block;font-size:0.733em;color:rgba(255,255,255,0.65);margin-top:0.545em;font-weight:400;}
#workskin dd.bubble.in .time{display:block;font-size:0.733em;color:rgba(0,0,0,0.55);margin-top:0.545em;font-weight:400;}
#workskin dd.bubble.image-bubble .time.image-time{position:absolute;bottom:0.727em;right:0.727em;margin:0;background:rgba(0,0,0,0.6);padding:0.182em 0.545em;border-radius:0.909em;font-size:0.733em;color:#fff;}
#workskin dd.bubble .reaction{position:absolute;top:-0.75em;background:#e9e9eb;color:#000;border:0.125em solid #fff;border-radius:0.875em;padding:0.063em 0.375em;font-size:0.875em;line-height:1.35;box-shadow:0 1px 3px rgba(0,0,0,0.2);z-index:2;}
#workskin dd.bubble.out .reaction{left:-0.5em;}
#workskin dd.bubble.in .reaction{right:-0.5em;}
#workskin dd.bubble.has-reaction{padding-top:0.95em;margin-top:1.2em;}
#workskin dd.status-indicator{font-size:0.625em;color:rgba(60,60,67,0.6);text-align:right;margin:0.2em 1em 0 0;font-weight:400;}
#workskin dd.attach{margin-top:0.125em;}
#workskin img.attach-img{max-width:13.75em;border-radius:0.75em;display:block;}
#workskin .row.typing{align-items:center;margin-left:-0.375em;}
#workskin .row.typing > *{margin-left:0.375em;}
#workskin .typing-bubble{background:rgba(233, 233, 235, 1);padding:0.625em 0.875em;border-radius:1.125em;display:inline-block;line-height:0;border-bottom-left-radius:0.25em;}
#workskin .typing-bubble .dot{display:inline-block;vertical-align:middle;width:0.5em;height:0.5em;margin-left:0.25em;background:rgba(60,60,67,0.6);border-radius:50%;opacity:0.4;}
#workskin .typing-bubble .dot:first-child{margin-left:0;}
#workskin .typing-bubble .dot:nth-child(1){opacity:0.85;}
#workskin .typing-bubble .dot:nth-child(2){opacity:0.65;}
#workskin .typing-label{font-size:0.688em;color:rgba(60,60,67,0.6);font-weight:400;}
#workskin .ios-footer{position:relative;background:#f6f6f6;height:2.938em;border-top:1px solid #e0e0e0;}
#workskin .ios-input-bar{background:#f6f6f6;padding:0.5em 0.75em;border-top:1px solid #e0e0e0;display:flex;align-items:center;}
#workskin .ios-input-bar p{display:contents;}
#workskin .ios-input-bar .ios-input-plus,#workskin .ios-input-bar .input-placeholder,#workskin .ios-input-bar .ios-input-mic{margin-left:0.5em;}
#workskin .ios-input-bar .ios-input-plus{margin-left:0;}
#workskin .ios-input-plus,#workskin .ios-input-mic{color:#007aff;font-size:1.2em;line-height:1;}
#workskin .ios-input-bar .input-placeholder{flex:1;background:#fff;border:1px solid #c7c7cc;border-radius:1.286em;padding:0.571em 0.857em;font-size:0.875em;color:#86868b;}
#workskin dd.bubble strong,#workskin dd.bubble b{font-weight:700;}
#workskin dd.bubble em,#workskin dd.bubble i{font-style:italic;}
#workskin dd.bubble s,#workskin dd.bubble strike,#workskin dd.bubble del{text-decoration:line-through;}
#workskin dd.bubble code{font-family:'SF Mono','Menlo','Monaco','Consolas',monospace;font-size:0.9em;background:rgba(0,0,0,0.05);padding:0.148em 0.37em;border-radius:0.296em;border:1px solid rgba(0,0,0,0.1);}
#workskin dd.bubble pre{margin:0.533em 0;padding:0;}
#workskin dd.bubble pre code{display:block;padding:0.593em 0.741em;white-space:pre-wrap;word-break:break-word;border-radius:0.444em;}
#workskin dd.bubble blockquote{margin:0.4em 0;padding:0.267em 0 0.267em 0.667em;border-left:3px solid rgba(0,0,0,0.3);background:rgba(0,0,0,0.03);font-style:italic;}
#workskin dd.bubble ul,#workskin dd.bubble ol{margin:0.4em 0;padding-left:1.333em;}
#workskin dd.bubble li{margin:0.133em 0;}
#workskin dd.bubble ul{list-style-type:disc;}
#workskin dd.bubble ol{list-style-type:decimal;}
#workskin .wm{margin-top:1.778em;font-size:0.563em;opacity:0.45;text-align:center;color:#86868b;}
#workskin .visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}

The matching chapter HTML

iMessage chapter HTML
<div class="chat ios theme-light ios-frame-header css-tails"><div class="ios-header"><div class="ios-back">‹</div><div class="ios-header-avatar-placeholder">LC</div><div class="ios-header-name-wrapper"><div class="ios-header-name">Lighthouse Crew</div><div class="ios-header-subtitle">Nadia, Wren</div></div><div class="ios-header-actions">ⓘ</div></div><div class="chat-messages"><div class="ios-event ios-event-date"><dl><dd>Today</dd></dl></div><div class="row in single"><dl class="msg"><dd class="bubble in has-tail"><span class="group-avatar-initials ios-tone-red">NA</span> <b class="ios-group-sender ios-tone-red">Nadia</b> <span class="message-text">The lamp room light is on.</span><span class="visually-hidden"> </span><span class="time">11:04</span></dd></dl></div><div class="row in single"><dl class="msg"><dd class="bubble in has-tail"><span class="group-avatar-initials ios-tone-indigo">WR</span> <b class="ios-group-sender ios-tone-indigo">Wren</b> <span class="message-text">That building has had no power since March.</span><span class="visually-hidden"> </span><span class="time">11:05</span></dd></dl></div><div class="row out single"><dl class="msg"><dt class="visually-hidden">You: </dt><dd class="bubble out has-tail has-reply has-tapbacks"><blockquote class="ios-reply ios-reply-indigo"><b>Replying to Wren</b> <span>That building has had no power since March.</span> </blockquote><span class="message-text">I know. I am looking at it right now.</span><span class="visually-hidden"> </span><span class="time">11:06</span><span class="visually-hidden"> </span><span class="ios-tapbacks"><span class="visually-hidden">Tapbacks: </span>‼️ ❤️ 2</span></dd><dd class="status-indicator">Read</dd></dl></div></div></div>

WhatsApp: the complete CSS

WhatsApp is a separate skin, not the iMessage one recoloured. The two renderers agree about almost nothing except the shape of a URL, and the CSS reflects that.

AO3 WhatsApp work-skin CSS
#workskin .chat{width:100%;max-width:25em;min-width:20em;margin:0 auto;display:flex;flex-direction:column;font-family:Arial, Helvetica, sans-serif;background:#ece5dd;padding:0;border-radius:1.25em;overflow:hidden;}
#workskin .chat p{margin:0;padding:0;}
#workskin .android-header{background:#075e54;min-height:3.75em;display:flex;align-items:center;color:#fff;padding:0.35em 0.65em;overflow:hidden;}
#workskin .wa-back{font-size:2em;line-height:1;margin-right:0.25em;}
#workskin .android-header-avatar,#workskin .android-header-avatar-placeholder{width:2.5em;height:2.5em;border-radius:50%;display:block;flex-shrink:0;margin-right:0.65em;border:0.125em solid rgba(255,255,255,0.2);}
#workskin .android-header-avatar-placeholder{background:#128c7e;color:#fff;text-align:center;line-height:2.5em;font-weight:600;}
#workskin .android-header-name-wrapper{min-width:0;flex:1;color:#fff;overflow:hidden;}
#workskin .android-header-name{font-size:1em;font-weight:600;line-height:1.35;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
#workskin .android-header-subtitle{font-size:0.75em;line-height:1.2;opacity:0.85;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
#workskin .wa-header-actions{font-size:1.3em;white-space:nowrap;margin-left:0.5em;}
#workskin .chat-messages{padding:0.75em 0.5em;background:#ece5dd;}
#workskin .chat.wa-scroll .chat-messages{height:30em;overflow-y:auto;overflow-x:hidden;}
#workskin .row{display:flex;width:100%;align-items:flex-end;}
#workskin .row.single{margin:0.7em 0;}
#workskin .row.first{margin:0.7em 0 0.12em 0;}
#workskin .row.middle{margin:0.12em 0;}
#workskin .row.last{margin:0.12em 0 0.7em 0;}
#workskin .row.out{justify-content:flex-end;}
#workskin .row.in{justify-content:flex-start;}
#workskin dl.msg{margin:0;display:flex;flex-direction:column;max-width:82%;}
#workskin .row.out dl.msg{align-items:flex-end;}
#workskin .row.in dl.msg{align-items:flex-start;}
#workskin dd{margin:0;}
#workskin dd.bubble{position:relative;display:inline-block;min-width:2.5em;max-width:20em;padding:0.5em 0.714em;border-radius:0.571em;line-height:1.4;font-size:0.875em;box-shadow:0 1px 2px rgba(0,0,0,0.1);white-space:normal;word-break:keep-all;overflow-wrap:anywhere;}
#workskin dd.bubble.multi-image-bubble{width:20em;max-width:100%;box-sizing:border-box;}
#workskin dd.bubble.out{background:rgba(220, 248, 198, 1);color:#000;}
#workskin dd.bubble.in{background:rgba(255, 255, 255, 1);color:#000;}
#workskin .row.out.last dd.bubble,#workskin .row.out.single dd.bubble{border-bottom-right-radius:0.143em;}
#workskin .row.in.last dd.bubble,#workskin .row.in.single dd.bubble{border-bottom-left-radius:0.143em;}
#workskin .group-sender{display:block;font-size:0.8em;font-weight:700;line-height:1.2;margin-bottom:0.35em;}
#workskin .group-sender .group-avatar,#workskin .group-sender .group-avatar-initials{display:inline-block;width:1.667em;height:1.667em;border-radius:50%;vertical-align:middle;margin-right:0.35em;flex-shrink:0;}
#workskin .group-sender .group-avatar-initials{background:rgba(0,0,0,0.06);color:inherit;text-align:center;line-height:1.667em;font-size:0.75em;font-weight:700;white-space:nowrap;}
#workskin .wa-tone-green{color:#137333;}
#workskin .wa-reply-green{border-left-color:#137333;}
#workskin .wa-tone-teal{color:#00796b;}
#workskin .wa-reply-teal{border-left-color:#00796b;}
#workskin .wa-tone-lime{color:#4f6f00;}
#workskin .wa-reply-lime{border-left-color:#4f6f00;}
#workskin .wa-tone-yellow{color:#7a5d00;}
#workskin .wa-reply-yellow{border-left-color:#7a5d00;}
#workskin .wa-tone-orange{color:#a34600;}
#workskin .wa-reply-orange{border-left-color:#a34600;}
#workskin .wa-tone-red{color:#b3261e;}
#workskin .wa-reply-red{border-left-color:#b3261e;}
#workskin .wa-tone-pink{color:#a3195b;}
#workskin .wa-reply-pink{border-left-color:#a3195b;}
#workskin .wa-tone-magenta{color:#8e1a8e;}
#workskin .wa-reply-magenta{border-left-color:#8e1a8e;}
#workskin .wa-tone-purple{color:#6f42c1;}
#workskin .wa-reply-purple{border-left-color:#6f42c1;}
#workskin .wa-tone-violet{color:#5b3fb4;}
#workskin .wa-reply-violet{border-left-color:#5b3fb4;}
#workskin .wa-tone-blue{color:#1769aa;}
#workskin .wa-reply-blue{border-left-color:#1769aa;}
#workskin .wa-tone-cyan{color:#006b78;}
#workskin .wa-reply-cyan{border-left-color:#006b78;}
#workskin .message-text{display:inline;}
#workskin .wa-reply{display:block;margin:0 0 0.45em 0;padding:0.4em 0.5em;border-left:0.25em solid #667781;border-radius:0.25em;background:rgba(0,0,0,0.06);font-style:normal;}
#workskin .wa-reply b,#workskin .wa-reply span{display:block;}
#workskin .wa-reply span{font-size:0.9em;max-height:3.8em;overflow:hidden;}
#workskin .wa-reply-missing{border-left-color:#b3261e;}
#workskin .wa-images{display:block;margin-top:0.4em;font-size:0;overflow:hidden;border-radius:0.45em;}
#workskin .wa-image{display:block;width:100%;height:auto;margin:0;}
#workskin .wa-images .image-layout-row,#workskin .wa-images .image-layout-columns{display:block;width:100%;font-size:0;overflow:hidden;}
#workskin .wa-images .image-layout-row{margin-bottom:1%;}
#workskin .wa-images .image-layout-last-row{margin-bottom:0;}
#workskin .wa-images .image-layout-cell,#workskin .wa-images .image-layout-column{display:inline-block;vertical-align:top;overflow:hidden;}
#workskin .wa-images .image-layout-first{margin-right:1%;}
#workskin .wa-images .image-split-33-67 .image-layout-first{width:32.5%;}#workskin .wa-images .image-split-33-67 .image-layout-second{width:66.5%;}
#workskin .wa-images .image-split-40-60 .image-layout-first{width:39.5%;}#workskin .wa-images .image-split-40-60 .image-layout-second{width:59.5%;}
#workskin .wa-images .image-split-50-50 .image-layout-first{width:49.5%;}#workskin .wa-images .image-split-50-50 .image-layout-second{width:49.5%;}
#workskin .wa-images .image-split-60-40 .image-layout-first{width:59.5%;}#workskin .wa-images .image-split-60-40 .image-layout-second{width:39.5%;}
#workskin .wa-images .image-split-67-33 .image-layout-first{width:66.5%;}#workskin .wa-images .image-split-67-33 .image-layout-second{width:32.5%;}
#workskin .wa-images .image-layout-feature,#workskin .wa-images .image-layout-stack-item,#workskin .wa-images .image-layout-side-item{display:block;width:100%;overflow:hidden;}
#workskin .wa-images .image-layout-feature,#workskin .wa-images .image-layout-stack-item{margin-bottom:1%;}
#workskin .wa-images .image-layout-side-item{margin-bottom:2%;}
#workskin .wa-images .image-layout-last-item{margin-bottom:0;}
#workskin .wa-link-preview{display:block;margin-top:0.4em;border-radius:0.4em;overflow:hidden;background:rgba(0,0,0,0.07);color:inherit;text-decoration:none;padding-bottom:0.4em;}
#workskin .wa-link-preview b,#workskin .wa-link-preview span{display:block;margin:0.25em 0.5em 0 0.5em;}
#workskin .wa-link-image{display:block;width:100%;height:auto;margin:0;}
#workskin .wa-link-description{font-size:0.9em;opacity:0.85;}
#workskin .wa-link-site,#workskin .wa-link-url{font-size:0.78em;opacity:0.7;overflow-wrap:anywhere;}
#workskin .wa-media{display:block;position:relative;margin-top:0.4em;padding:0.5em;border-radius:0.45em;background:rgba(0,0,0,0.06);overflow:hidden;}
#workskin .wa-play{display:inline-block;width:1.8em;vertical-align:middle;}
#workskin .wa-waveform{display:inline-block;width:12em;height:2.2em;vertical-align:middle;white-space:nowrap;overflow:hidden;}
#workskin .wa-wave-bar{display:inline-block;width:0.14em;margin-right:0.12em;vertical-align:middle;background:#667781;border-radius:0.1em;}
#workskin .wa-wave-1{height:0.4em;}#workskin .wa-wave-2{height:0.7em;}#workskin .wa-wave-3{height:1em;}#workskin .wa-wave-4{height:1.3em;}#workskin .wa-wave-5{height:1.6em;}#workskin .wa-wave-6{height:1.9em;}
#workskin .wa-duration,#workskin .wa-transcript,#workskin .wa-media-description,#workskin .wa-captions,#workskin .wa-media-source{display:block;font-size:0.78em;margin-top:0.25em;}
#workskin .wa-media-source{color:inherit;text-decoration:underline;overflow-wrap:anywhere;}
#workskin .wa-video-poster,#workskin .wa-native-video,#workskin .wa-youtube-player iframe{display:block;width:100%;height:auto;border:0;border-radius:0.35em;background:#000;}
#workskin .wa-youtube-player iframe{height:11.25em;max-width:100%;}
#workskin .wa-video-placeholder{display:block;box-sizing:border-box;min-height:8em;padding:3.5em 1em;text-align:center;background:#1f2c34;color:#aebac1;border-radius:0.35em;}
#workskin .wa-video-play{position:absolute;left:46%;top:2.5em;font-size:2em;color:#fff;text-shadow:0 1px 3px #000;}
#workskin .wa-native-audio{display:block;width:100%;margin-top:0.45em;}
#workskin dd.bubble .time{display:block;font-size:0.714em;color:rgba(0,0,0,0.45);margin-top:0.4em;text-align:right;padding-right:2.4em;}
#workskin .wa-ticks{position:absolute;right:0.55em;bottom:0.45em;font-size:0.75em;color:rgba(0,0,0,0.45);letter-spacing:-0.2em;}
#workskin .wa-ticks-read{color:#53bdeb;}
#workskin .wa-reactions{position:absolute;bottom:-1.65em;background:#fff;border:0.071em solid rgba(0,0,0,0.08);border-radius:0.857em;padding:0.1em 0.45em;font-size:0.857em;line-height:1.4;box-shadow:0 1px 2px rgba(0,0,0,0.15);white-space:nowrap;z-index:2;}
#workskin dd.bubble.out .wa-reactions{right:0.75em;}
#workskin dd.bubble.in .wa-reactions{left:0.75em;}
#workskin dd.bubble.has-reaction{margin-bottom:2em;}
#workskin .wa-event{text-align:center;margin:0.8em 0;color:#667781;}
#workskin .wa-event dl{margin:0;}
#workskin .wa-event dd{display:inline-block;background:#fff5c4;border-radius:0.5em;padding:0.4em 0.65em;font-size:0.75em;box-shadow:0 1px 2px rgba(0,0,0,0.1);}
#workskin .wa-event-system dd{max-width:82%;}
#workskin dd.bubble.emoji-only{background:transparent;box-shadow:none;padding:0.143em 0.214em;border-radius:0;overflow:visible;}
#workskin dd.bubble.emoji-only .emoji-content{display:block;line-height:1.05;white-space:nowrap;}
#workskin dd.bubble.emoji1 .emoji-content{font-size:4.286em;}
#workskin dd.bubble.emoji2 .emoji-content{font-size:2.571em;}
#workskin .row.typing{align-items:center;}
#workskin .typing-bubble{background:rgba(255, 255, 255, 1);padding:0.625em 0.875em;border-radius:0.5em;display:inline-block;line-height:0;box-shadow:0 1px 2px rgba(0,0,0,0.1);}
#workskin .typing-bubble .dot{display:inline-block;vertical-align:middle;width:0.5em;height:0.5em;margin-left:0.25em;background:rgba(0,0,0,0.4);border-radius:50%;opacity:0.4;}
#workskin .typing-bubble .dot:first-child{margin-left:0;}
#workskin .typing-bubble .dot:nth-child(1){opacity:0.85;}
#workskin .typing-bubble .dot:nth-child(2){opacity:0.65;}
#workskin .typing-label{font-size:0.688em;color:rgba(0,0,0,0.6);margin-left:0.4em;}
#workskin .android-footer{background:#f0f0f0;min-height:3.75em;border-top:0.063em solid #d1d7db;display:flex;align-items:center;padding:0 0.6em;color:#54656f;}
#workskin .wa-footer-plus,#workskin .wa-footer-mic{width:2em;text-align:center;font-size:1.35em;}
#workskin .wa-footer-input{flex:1;background:#fff;border-radius:1.25em;padding:0.55em 0.9em;color:#667781;font-size:0.9em;}
#workskin dd.bubble strong,#workskin dd.bubble b{font-weight:700;}
#workskin dd.bubble em,#workskin dd.bubble i{font-style:italic;}
#workskin dd.bubble s,#workskin dd.bubble strike,#workskin dd.bubble del{text-decoration:line-through;}
#workskin dd.bubble code{font-family:'SF Mono','Menlo','Monaco','Consolas',monospace;font-size:0.9em;background:rgba(0,0,0,0.05);padding:0.159em 0.397em;border-radius:0.317em;border:1px solid rgba(0,0,0,0.1);}
#workskin dd.bubble pre{margin:0.571em 0;padding:0;}
#workskin dd.bubble pre code{display:block;padding:0.635em 0.794em;white-space:pre-wrap;word-break:break-word;border-radius:0.476em;}
#workskin dd.bubble blockquote{margin:0.429em 0;padding:0.286em 0 0.286em 0.714em;border-left:3px solid rgba(0,0,0,0.3);background:rgba(0,0,0,0.03);font-style:italic;}
#workskin dd.bubble ul,#workskin dd.bubble ol{margin:0.429em 0;padding-left:1.429em;}
#workskin dd.bubble li{margin:0.143em 0;}
#workskin dd.bubble ul{list-style-type:disc;}
#workskin dd.bubble ol{list-style-type:decimal;}
#workskin .wm{margin-top:1.333em;font-size:0.563em;opacity:0.45;text-align:center;color:#667781;}
#workskin .visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}

The matching chapter HTML

WhatsApp chapter HTML
<div class="chat android theme-light wa-frame-header"><div class="android-header"><div class="wa-back">‹</div><div class="android-header-avatar-placeholder">LC</div><div class="android-header-name-wrapper"><div class="android-header-name">Lighthouse Crew</div><div class="android-header-subtitle">Nadia, Wren</div></div><div class="wa-header-actions">⌕ ⋮</div></div><div class="chat-messages"><div class="wa-event wa-event-date"><dl><dd>TODAY</dd></dl></div><div class="row in single"><dl class="msg"><dd class="bubble in has-tail"><b class="group-sender wa-tone-teal"><span class="group-avatar-initials">NA</span> Nadia</b> <span class="message-text">The lamp room light is on.</span><span class="visually-hidden"> </span><span class="time">11:04</span></dd></dl></div><div class="row in single"><dl class="msg"><dd class="bubble in has-tail"><b class="group-sender wa-tone-purple"><span class="group-avatar-initials">WR</span> Wren</b> <span class="message-text">That building has had no power since March.</span><span class="visually-hidden"> </span><span class="time">11:05</span></dd></dl></div><div class="row out single"><dl class="msg"><dt class="visually-hidden">You: </dt><dd class="bubble out has-tail has-reply has-reaction"><blockquote class="wa-reply wa-reply-purple"><b>Replying to Wren</b><span>That building has had no power since March.</span></blockquote><span class="message-text">I know. I am looking at it right now.</span><span class="visually-hidden"> </span><span class="time">11:06</span><span class="wa-ticks wa-ticks-read">✓✓</span><span class="wa-reactions"><span class="visually-hidden">Reactions: </span>😮 ×2</span></dd></dl></div></div></div>

The details that make it read as real

These are the differences most hand-written skins miss. They are also the reason one skin cannot serve both platforms by swapping a colour variable.

The same fictional group conversation side by side: iMessage on the left with Tapbacks on the bubble, WhatsApp on the right with a reaction pill below it
One conversation, both platforms. The reaction sits on the bubble on iMessage and below it on WhatsApp — the difference the table describes.
DetailiMessageWhatsApp
Where a reaction sitsPinned to the bubble’s top edge, overlapping it, on the side away from the speakerA pill below the bubble, tucked to the speaker’s own side
Reaction countsThe number follows the emoji: ❤️ 2The number is prefixed: 😮 ×2
Delivery stateA word — Delivered or Read — under the last message of an outgoing run, and only when read receipts are switched onTicks on every outgoing message: sent, ✓✓ delivered, ✓✓ in blue for read
Group sender namesInitials chip and name inline, leading the message text inside the bubbleName on its own line inside the bubble, above the text

Two things people expect to differ do not. Bubble tails attach to the last message of a run on both platforms, not the first. And quote-replies get near-identical treatment on both: a tinted panel inside the bubble with a coloured bar down its left edge, tinted to the original speaker. If you are adapting a skin from one platform to the other, those two are the parts you can leave alone.

The exports encode all of this already. You can see the divergence in the HTML above: iMessage emits <span class="ios-tapbacks"> plus a separate <dd class="status-indicator">Read</dd>, while WhatsApp emits <span class="wa-ticks wa-ticks-read">✓✓</span> and a <span class="wa-reactions"> pill.

Sender colours are classes, not colour values

In a group chat each speaker gets their own name colour. The obvious way to do this is an inline style attribute on each name — and AO3 strips inline styles, so the obvious way produces a group chat where everyone is the same colour on the archive while looking correct in your editor.

The exports use a fixed palette of classes instead: ios-tone-red, ios-tone-indigo and ten more on iMessage, wa-tone-teal, wa-tone-purple and ten more on WhatsApp. The colour lives in the stylesheet, where AO3 permits it. This is why the app offers a palette rather than a colour picker for group members: a free colour value cannot survive the trip.

A date divider is its own entry

Date dividers and system messages (“Nadia added Wren”) render as their own block, outside the <dl> that holds a message — never inside a bubble. That is deliberate: an event nested in a speech element gets read aloud as though a character had said it.

The practical consequence catches people out. An event is its own entry, not a label you attach to an existing message. Give a message both a date divider and a line of dialogue and the divider wins — the dialogue is dropped, silently, with no warning in the app or the archive. Add the divider as a separate entry immediately before the message it should precede.

Light and dark, and one skin per work

Both platforms export in light or dark. The important constraint is not the theme, it is that AO3 allows a work exactly one work skin. Selecting a second does not stack them; it replaces the first.

So if your fic contains iMessage in chapter 2 and WhatsApp in chapter 9, do not save two skins. Choose the All four platforms master skin, which covers iMessage, WhatsApp, X/Twitter and Google in every theme each platform supports, and select that one skin on the work.

The same iMessage and WhatsApp conversations rendered in dark mode side by side
Dark mode on both platforms. Only the theme changes; the markup is identical.

What happens when Creator’s Style is off

AO3 readers can switch a work’s styling off, and downloads and screen readers care far more about reading order than about bubble shapes. A texting skin is unusually exposed here, because a conversation stripped of its CSS can easily become an unattributed wall of alternating sentences.

These exports use a description list for the conversation: each message is a <dl class="msg"> in which the <dd> is what was said. Every message is attributed to a speaker exactly once, which is subtler than it sounds. In a group chat, incoming messages already show a visible name, so no hidden label is added. Everywhere else — your own messages, and any one-to-one conversation — the export inserts <dt class="visually-hidden">Nadia: </dt> instead. Emitting both would make a download name the same speaker twice on every line.

Reactions and delivery state carry hidden labels for the same reason. Unstyled, the Tapback row reads “Tapbacks: ‼️ ❤️ 2” rather than presenting two floating symbols with no explanation.

Preview both states before you post. With Creator’s Style disabled, check that every line still has a speaker, the conversation is in the right order, and nothing has become a bare symbol. A skin that looks perfect styled and turns to noise unstyled is not finished.

Building it without editing code

AO3 Skin Generator editor beside a live preview of a fictional iMessage conversation
The editor builds the conversation and produces the CSS and HTML above.

The AO3 Skin Generator app is the practical route. It composes the conversation, validates the CSS against AO3’s rules before letting you copy it, and hands you the two blocks in the right order. Replies, reactions, events, group colours, link previews and media are all authoring controls rather than things you hand-write.

If you edit CSS yourself and AO3 rejects it, the searchable complete AO3 CSS property reference will tell you which declaration it refused.

Troubleshooting

The conversation is one grey block

Either the skin is not selected on the work, or the HTML went in through Rich Text mode. Check Select Work Skin on the work first, since creating a skin does not attach it to anything. Then confirm the chapter HTML still opens with the outer chat ios or chat android container.

Everyone’s name is the same colour

The tone classes are missing from the CSS, or the HTML was written by hand with inline colours. Recopy the full stylesheet, and check the sender names carry a class such as ios-tone-red rather than a style attribute.

Bubbles have no tails, or every message has one

Tails are applied by run position, not per message. Each row carries single, first, middle or last, and each bubble carries has-tail or no-tail — the tail goes on the message that ends a run. Deleting a message by hand without updating its neighbours leaves those boundaries inconsistent, which is what produces a run of tailless bubbles or a tail on every one.

AO3 refuses to save the skin

The CSS on this page is generated and passes the project’s AO3 validator, so start by undoing your most recent edit. Then check the property against the AO3 CSS allowlist, and check its value: an allowed property still fails on calc(), a web font, or a rejected image address.

Limits, and what to check before posting

  • This tool is unofficial and is not affiliated with the OTW or Archive of Our Own.
  • Typing indicators and “20 minutes later” time breaks exist in the generated output but are not exposed as authoring controls in the current app. Use a date or system event for a divider you can edit.
  • A work can select one work skin, not several stacked skins. Use the master skin if your fic mixes platforms.
  • Group name colours come from a fixed palette, not a free colour picker, because AO3 removes inline styles.
  • Avatars and any images you attach are hosted elsewhere; AO3 stores no copy. Keep your own backups of images you control.
  • Preview with Creator’s Style both on and off. Unstyled, every line should still name its speaker.

Sources and further reading

Not sure a work skin is what you need? Compare AO3 work skins, site skins and public skins before installing anything.

Similar Posts