Our transactional and campaign emails share one branded shell: a header with the logo, the body, a footer with the unsubscribe lines. I wrote the first version the way I would write a component, because it is 2026 and I had forgotten what email is.
<div style="display:inline-flex;gap:8px;align-items:center">
<svg width="19" height="19">...</svg>
<span style="font-size:19px;font-weight:700">CogniPrep</span>
</div>
In a browser preview: perfect. In Gmail: no logo at all. In Outlook: no logo, no gap, and the text sitting somewhere it was not asked to sit.
Two independent failures in four lines.
SVG is not a thing in email
Gmail strips <svg> entirely. Outlook's desktop client renders through the Word engine, which ignores it. Between them that is most of our recipients, so for most people the header was a wordmark with a hole to the left of it.
The fix is a hosted raster image:
<img src="https://cogniprep.app/logo-light.png" width="38" height="38" alt=""
style="display:block;width:38px;height:38px;border:0;outline:none;text-decoration:none">
Four details in that tag, none decorative:
width and height as attributes, not only CSS. Outlook needs them there. Set only in the style attribute and it will size the image however it likes, usually at its natural resolution, which for a 512px source is a comedy.
display:block. As an inline image it sits on the text baseline of its cell, which leaves a few pixels of descender space underneath and pushes the mark visibly above the centre of the text beside it. Blocking it removes that gap so vertical-align:middle can do its job. This is the single most common "why is my email logo slightly too high" cause.
border:0;outline:none;text-decoration:none. Some clients decorate images inside links. Belt and braces, and it costs nothing.
alt="", deliberately empty. The wordmark text sits right next to it. A non-empty alt would announce the brand name twice to a screen reader and, when images are blocked, render it twice on screen.
One sizing note that surprised me: the image is displayed at 38px next to 19px text. The PNG is a 512px square with the mark painted inside roughly the middle 60%, so the part that actually shows is about a quarter smaller than the box it is drawn in. Sizing the box to match the text left the visible mark looking undersized. Measure the ink, not the canvas.
Flexbox is not a thing in email either
display:inline-flex with gap is dropped by both the Word engine and Gmail. When it is dropped, gap goes with it, so the mark ends up jammed against the wordmark.
The replacement is a two-cell table, which is what email layout has always been:
<table cellpadding="0" cellspacing="0" role="presentation" align="center" style="margin:0 auto">
<tr>
<td style="vertical-align:middle;line-height:1;padding-right:10px">
<a href="..."><img ...></a>
</td>
<td style="vertical-align:middle;line-height:1">
<a href="..." style="font-size:19px;font-weight:700;text-decoration:none">CogniPrep</a>
</td>
</tr>
</table>
- A padding cell supplies the gap, because
padding-righton a<td>is understood everywhere. -
vertical-align:middleon both cells does the centring thatalign-itemswas supposed to do. -
line-height:1stops inherited line height from adding invisible space above and below the cell contents. -
align="center"andmargin:0 auto, because different clients honour different ones. -
role="presentation"tells screen readers this is layout rather than data, which is the one accessibility obligation that comes with using tables for layout.
Everything is inline-styled. Gmail strips <style> blocks in several contexts, so a class-based stylesheet is a stylesheet that sometimes exists.
Two smaller things worth copying
Hardcode the site URL in the email shell. Ours is a constant in the layout module rather than process.env.NEXT_PUBLIC_WEBSITE_URL:
const SITE_URL = 'https://cogniprep.app';
The env var is right for the app, where local development should link to localhost. It is wrong for email, because a message sent from a machine whose env points at localhost contains a logo that cannot load and a header link that goes nowhere. The shell's links always mean the real site.
Make the unsubscribe block optional, not always-on. Our footer takes unsubscribeUrl: string | null, and when it is null the marketing unsubscribe lines are dropped entirely. That covers one-off operator emails to people who may not be users at all, where there is no valid unsubscribe token to mint and the message is not a broadcast that needs an opt-out. Rendering a broken unsubscribe link is worse than rendering none.
Why this is worth an afternoon
Email is the one surface where you cannot ship a fix to a message already delivered. A web layout bug is live until your next deploy; an email layout bug is permanent in every inbox that received it.
The general rule: write email HTML as if it is 2005, because in the Word rendering engine it is. Tables for layout, inline styles, attribute-sized raster images, no flex, no grid, no SVG. It feels like regression and it is simply the target platform.
Sign up at https://cogniprep.app and the verification email that arrives is the shell described above. Open it in Gmail and in Outlook and compare the header: that it looks identical in both is the entire point of the table.
Top comments (0)