AO3 HTML Formatting Guide: Which Tags Work, and Why Some Layouts Break
If you have ever posted a work on AO3, you may have run into something like this: a layout that looked fine locally changes after you paste it into AO3; a color does not show up; a chat-style scene gets squeezed on mobile; or AO3 Preview quietly adds a few <p> and <br> tags for you.
AO3 is not an editor where you can paste arbitrary webpage code and expect it to run as-is. It does allow writers to use some HTML and CSS for formatting, but that code goes through a parser and a sanitizer first. The parser tries to complete and tidy incomplete code. The sanitizer removes code that is not allowed or may be unsafe. AO3’s official HTML FAQ says this directly.1
This is not a development tutorial. It is about a practical problem many writers run into: why some formatting fails, and where your effort is best spent if you want a work to remain readable after downloading, on mobile, or under different reading settings. This article refers to AO3’s official FAQ and to a few implementation details visible in AO3’s public source code, but the main question is still: how should a writer make decisions?
Why does AO3 clean HTML?
The reason is simple: an AO3 work page is not a private webpage owned by the author. It is a public archive page that readers open directly.
If AO3 allowed arbitrary HTML, CSS, and JavaScript, each work could affect the whole page: tracking readers, inserting external scripts, covering the interface, or breaking the site layout. For an archive that preserves user-submitted works long term, that risk is too high.
So AO3 uses an allowlist model. It is not “everything is allowed unless it is dangerous.” It is closer to “only things on the allowed list are kept.” The official HTML FAQ lists allowed tags and attributes; the source file sanitizer_config.rb shows a similar configuration. Elements such as a, blockquote, br, div, em, img, ol, p, span, table, strong, and ul can be kept, but attributes and link protocols are still limited.12
In other words, many formatting failures are not because you “wrote it wrong.” They happen because AO3 was never going to let that piece of code into the final page.
Which HTML is safer?
From a writer’s point of view, the safest tools are still basic semantic tags.
For example:
<p>: paragraph<br>: line break<strong>/<b>: bold text<em>/<i>: italic text<blockquote>: quotation<hr>: divider<ul>,<ol>,<li>: lists<a href="...">: link<img src="...">: image<ruby>,<rt>,<rp>: ruby annotations<table>,<tr>,<td>,<th>: tables
These tags appear in AO3’s official FAQ and in the source allowlist. They may not make a page look especially fancy, but they are durable: they read well on mobile, are less likely to break when downloaded as EPUB, and still leave the content intact when a reader changes skins or hides the creator’s style.
For writers working with Chinese, Japanese, or other text that benefits from pronunciation notes, ruby is worth noticing. AO3 allows the <ruby>, <rt>, and <rp> group, which is cleaner than forcing every pronunciation note into parentheses in the main text.
Why do script, iframe, and style disappear?
Because they are outside the safety boundary for ordinary work HTML.
In AO3’s sanitizer configuration, elements such as script, iframe, style, svg, math, and noscript are listed under remove_contents. That means the tag may not be the only thing removed; its contents may be removed as well.2
This affects several common attempts at formatting.
If you paste something like this into the work body:
| |
It will not run like it would on an ordinary webpage. AO3 will clean it out. If you want styling, do not put <style> in the work text; use a Work Skin instead. If you want to embed an external webpage, an iframe is not the way to do it on AO3.
This is a security boundary. Readers should not have to run scripts inserted by an unknown author just because they opened a work page.
Why can <3 cause trouble?
In HTML, < starts a tag. <em> means emphasis, and <p> means paragraph. So what happens when a writer types <3 in the body of a work?
AO3’s HtmlCleaner contains a specific handling step for this case: it converts <3 into <3.3
In other words, AO3 knows many writers use that little heart. It does not simply treat every < as a broken tag. It protects some common input before sanitizing.
Still, the lesson is useful: if you want to display an actual less-than sign in your work, write it as <; for a greater-than sign, use >. AO3’s official HTML FAQ also recommends this, because < may make the parser think you have started writing HTML.1
What can a Work Skin do?
When basic HTML is not enough, Work Skins are the next tool.
AO3’s Skins FAQ explains that a skin is a stylesheet that changes the appearance of AO3 or of an individual work. A Work Skin changes the presentation of a specific work. After creating the Work Skin, the writer also needs to add matching HTML classes in the work text so the CSS knows what to target.4
That is why many chat, text-message, or forum-style layouts look something like this:
| |
Then the Work Skin might contain:
| |
The HTML says what the content is. The Work Skin says how it looks.
Why is Work Skin CSS limited too?
A Work Skin is not unlimited CSS.
AO3’s Skins FAQ says that custom skins can only use a limited set of CSS properties and values. The source file CssCleaner follows the same idea: it parses the CSS, checks whether a property is supported, and then checks whether the value fits the allowed colors, numbers, units, functions, URLs, and other patterns. Unsupported properties or values are cleaned out and reported as errors.45
There is also an important detail in the WorkSkin model: when a Work Skin is saved, AO3 prefixes selectors with #workskin.6
This matters for writers. By default, your Work Skin should affect the work body, not the entire AO3 page. If you write .message, AO3 makes it behave like something closer to #workskin .message. That keeps your CSS from escaping into the navigation, comments, kudos buttons, or the rest of the site interface.
The source code also restricts a few CSS features that can easily cause trouble. Work Skins do not allow CSS custom properties, meaning variables such as --name; they do not allow var(); and they do not allow position: fixed.6
AO3 is not handing each author a full webpage sandbox. It opens a limited part of CSS so writers can adjust how the work body is presented.
Why can a layout look fine in preview but still be fragile?
Because readers do not all read in the same environment.
Work Skins are shown to readers by default, but readers can choose to hide creator styles. AO3’s Skins FAQ explains that readers can select “Hide Creator’s Style” on an individual work page, or choose “Hide work skins” in Preferences to hide custom Work Skins across all works.4
That gives us one practical rule: do not put essential information only in the styling.
For example, this is fragile:
| |
Once a reader hides the Work Skin, the colors disappear, and A/B may no longer be clear.
A safer version puts identity into the text structure:
| |
Chat logs and forum-style sections need this especially. Nice formatting can add atmosphere, but the basic information should not depend only on color, position, background images, or floats.
Why do complex layouts change after downloading?
AO3 works are not read only on the website. Many readers download EPUB, MOBI, PDF, or HTML files. Others read on mobile apps, e-ink devices, or browser reading modes.
A complex Work Skin may look good on the webpage, but the downloaded file may not keep the same CSS effect. Even if some CSS is preserved, a reader’s app may not support it.
If your goal is long-term readability, it helps to think in two layers:
The first layer is content structure. Use paragraphs, quotations, lists, emphasis, and speaker names to make the text itself clear.
The second layer is visual presentation. Use a Work Skin for bubbles, forum boxes, colors, and spacing.
If the second layer fails, the first layer should still be readable.
Common mistakes for writers
First: pasting “dirty” HTML from a rich text editor directly into AO3.
Content copied from Word, Feishu, WeChat editors, or other writing tools often carries a lot of styling and attributes AO3 does not need. AO3 will clean them, but the cleaned result may not be what you wanted. A safer approach is to paste plain text first, or prepare simple HTML locally before checking it in AO3 Preview.
Second: putting CSS in the work body.
The body should contain HTML. CSS belongs in a Work Skin. If you put <style> in the work text, it will usually be removed.
Third: using color to carry all the information.
Chat layouts often rely on colors, left/right position, or avatars to distinguish speakers. But if a reader hides the Work Skin, downloads the work as EPUB, or uses a different site skin, those differences may disappear. It is safer to keep names, times, and scene cues in the text.
Fourth: not previewing.
AO3’s official FAQ also recommends previewing HTML before posting. The parser may add closing tags or adjust structure for you. If tags are nested incorrectly, Preview is usually where you can catch it.1
A practical example: a more durable chat layout
If you want to write a chat-style scene, the easiest trap is to rely only on color, left/right placement, or bubble styling to identify speakers. It may look good on the webpage, but once a reader hides Creator’s Style or downloads the work as EPUB, the conversation can become difficult to follow.
A safer approach is to make the HTML readable first, then use a Work Skin to decorate it.
The work body could look like this. Notice that every message still includes the speaker name. left and right are only for the Work Skin to control placement:
| |
The Work Skin could look like this:
| |
This example is not trying to look exactly like a phone screenshot. Its point is simpler: when the Work Skin is turned off, each message still reads as Lin: Are you here? and Zhou: Still on my way. Bubbles, alignment, and background are only the second layer. The colon is still present in the HTML; it is only hidden when the style is active.
notfun has also made a lightweight preview page for practicing this kind of basic HTML + Work Skin setup: AO3 HTML / Work Skin Previewer. The chat template follows the same idea as the example above: make sure the work remains readable when the Work Skin is off, then let CSS handle bubbles and placement. It is not AO3’s official sanitizer, and it cannot guarantee the exact final rendering on AO3. It is just a quick way to check, before posting, whether your work can still be read when the Work Skin is turned on or off.
What do these rules mean for writers?
Based on AO3’s official FAQ and visible details in AO3’s public source code, AO3 roughly handles writer input according to these principles:
- HTML is not a denylist. It is an allowlist.
- Inline CSS is not allowed in ordinary fields.
- CSS in Work Skins is cleaned.
- Work Skin selectors are scoped under
#workskin. - Unsafe or hard-to-clean tags are removed.
- When the sanitizer changes, AO3 has tools for batch re-sanitizing old fields.7
These implementation details cannot tell us everything. For writers, the most useful thing is not memorizing the allowlist, but understanding AO3’s tradeoff:
AO3 lets you format, but it does not let you turn a work page into a whole custom webpage.
AO3 lets you decorate a work, but readers can hide creator styles.
AO3 lets you build complex formatting, but long-term preservation still depends most on clear text structure.
If you want to write chat logs, forum threads, or text-message scenes, Work Skins can help. Just do not let essential information exist only in color or placement.
If you are not aiming for complicated styling, and only need ordinary bold text, italics, links, lists, or dividers, AO3’s Rich Text Editor may be the better choice. It reduces the maintenance cost of hand-written HTML and makes it less likely that one missing closing tag will pull the rest of the work out of shape.
If you want your work to remain readable years from now, the most durable formatting is often not the flashiest. It is the version where the text still makes sense after the styling is gone.
AO3’s official FAQ “Formatting content on AO3 with HTML” explains that AO3 processes writer input with a parser and sanitizer, and lists allowed HTML tags and attributes. https://archive.transformativeworks.org/faq/formatting-content-on-ao3-with-html?language_id=en ↩︎ ↩︎ ↩︎ ↩︎
AO3’s open-source
sanitizer_config.rbdefines the Archive’s HTML allowlist, attributes, protocols, and elements whose contents are removed. https://github.com/otwcode/otwarchive/blob/master/config/initializers/gem-plugin_config/sanitizer_config.rb ↩︎ ↩︎AO3’s open-source
html_cleaner.rbshows how AO3 handles text, HTML fields, and paragraphs before saving or displaying content, includingfix_bad_characters,sanitize_value, andadd_paragraphs_to_text. https://github.com/otwcode/otwarchive/blob/master/lib/html_cleaner.rb ↩︎AO3’s official “Skins and Archive Interface FAQ” explains that skins are stylesheets used to change how AO3 or an individual work appears; Work Skins need matching HTML classes in the work, and readers can hide creator styles. https://archive.transformativeworks.org/faq/skins-and-archive-interface?language_id=en ↩︎ ↩︎ ↩︎
AO3’s open-source
css_cleaner.rbshows how Work Skin / Skin CSS is parsed and cleaned, including checks for supported properties, supported values, URLs, fonts, and colors. https://github.com/otwcode/otwarchive/blob/master/lib/css_cleaner.rb ↩︎In AO3’s open-source
work_skin.rb, Work Skins callclean_css_codewhen saved and add the#workskinprefix. The model also blocks CSS custom properties,var(), andposition: fixed. https://github.com/otwcode/otwarchive/blob/master/app/models/work_skin.rb ↩︎ ↩︎AO3’s open-source
resanitize_batch_job.rbshows that when the sanitizer version changes, AO3 can batch re-sanitize fields that allow HTML. https://github.com/otwcode/otwarchive/blob/master/app/jobs/resanitize_batch_job.rb ↩︎