Skip to main content

Tailwind to Inline Styles

Better Svelte Email automatically converts Tailwind CSS classes into inline styles that work across all email clients. This happens transparently during the rendering process.

Basic Example

<div class="bg-blue-500 text-white p-4 rounded-lg">
  Hello World
</div>

How It Works

1

Class Detection

The renderer scans your component for all Tailwind classes.
2

CSS Generation

Tailwind CSS v4 generates only the CSS rules for classes you actually use.
3

Style Classification

Rules are classified as either inlinable or non-inlinable based on their properties.
4

Inline Conversion

Inlinable rules are converted to inline style attributes, while non-inlinable rules (media queries, pseudo-selectors) are placed in a <style> tag.

Responsive Breakpoints

Better Svelte Email supports all Tailwind responsive breakpoints. These classes cannot be inlined and are added to the <head> as media queries.

Supported Breakpoints

BreakpointMin WidthExample
sm:640pxsm:text-center
md:768pxmd:text-left
lg:1024pxlg:px-8
xl:1280pxxl:max-w-screen-xl
2xl:1536px2xl:text-4xl

Responsive Example

<script>
  import { Html, Head, Body } from 'better-svelte-email';
</script>

<Html>
  <Head />
  <Body>
    <div class="text-center md:text-left lg:text-right">
      Responsive Text
    </div>
  </Body>
</Html>
Responsive classes require a <head> element to work properly. Without it, the media queries have nowhere to go and rendering will fail.Always use the <Head> component or a regular <head> tag when using responsive classes.

Inlinable vs Non-Inlinable

Inlinable Styles

These styles can be safely converted to inline style attributes:
  • Layout: w-full, h-screen, p-4, m-2
  • Typography: text-lg, font-bold, text-center
  • Colors: bg-blue-500, text-white, border-gray-300
  • Spacing: mt-4, px-8, gap-2
  • Borders: border, rounded-lg, border-t-2
Example:
<div class="bg-red-500 text-white font-bold p-4">
  All classes here will be inlined
</div>

Non-Inlinable Styles

These styles must remain in <style> tags:

1. Responsive Breakpoints

<div class="sm:p-2 md:p-4 lg:p-6">
  Padding changes at different screen sizes
</div>

2. Pseudo-Selectors

<a class="hover:text-blue-600 hover:underline">
  Link with hover effect
</a>

3. Pseudo-Elements

<div class="before:content-['→'] after:content-['←']">
  Content with pseudo-elements
</div>

4. State Variants

<input class="focus:ring-2 disabled:opacity-50" />
From src/lib/render/utils/css/is-rule-inlinable.ts:36-38:
// Check for pseudo selectors in the selector string
// Matches :hover, ::before, :nth-child(), etc.
const hasPseudoSelector = /::?[\w-]+(\([^)]*\))?/.test(rule.selector);

Custom Tailwind Configuration

Extend Tailwind’s default theme to match your brand:
import Renderer from 'better-svelte-email/renderer';

const renderer = new Renderer({
  tailwindConfig: {
    theme: {
      extend: {
        colors: {
          brand: '#FF3E00',
          accent: '#40B3FF'
        },
        fontFamily: {
          sans: ['Inter', 'system-ui', 'sans-serif']
        },
        spacing: {
          '72': '18rem',
          '84': '21rem'
        }
      }
    }
  }
});
Now use your custom values:
<div class="bg-brand text-accent font-sans p-72">
  Custom themed content
</div>

Custom CSS Injection

Inject custom CSS for app theme integration (perfect for shadcn-svelte themes):
import appStyles from './app.css?raw';

const renderer = new Renderer({
  customCSS: appStyles
});

Example: CSS Variables

:root {
  --brand-primary: #FF3E00;
  --brand-secondary: #40B3FF;
  --spacing-base: 8px;
}

.brand-button {
  background: var(--brand-primary);
  padding: calc(var(--spacing-base) * 2);
}
Custom CSS is injected during Tailwind compilation, so CSS variables and custom classes are available for processing and can be resolved to inline styles.

Arbitrary Values

Use arbitrary values for one-off customizations:

Colors

<div class="bg-[#1da1f2] text-[rgb(220,38,38)]">
  Twitter blue background
</div>

Sizes

<div class="w-[350px] h-[calc(100vh-4rem)] p-[12px]">
  Custom dimensions
</div>

Spacing

<div class="mt-[17px] mb-[2.5rem]">
  Precise spacing
</div>

CSS Variable Resolution

Better Svelte Email automatically resolves CSS variables to their computed values for email client compatibility.

Before Resolution

.button {
  background: var(--brand-color);
  padding: var(--spacing-md);
}

After Resolution

.button {
  background: #FF3E00;
  padding: 16px;
}
From src/lib/render/utils/css/sanitize-stylesheet.ts:10-13:
export function sanitizeStyleSheet(root: Root, config?: SanitizeConfig) {
  resolveAllCssVariables(root);
  resolveCalcExpressions(root, config);
  sanitizeDeclarations(root, config);
}

Calc() Expression Resolution

Complex calc() expressions are resolved to absolute values:
<!-- Input -->
<div class="w-[calc(100%-20px)] p-[calc(1rem+5px)]">
  Calculated values
</div>

<!-- Output (with baseFontSize: 16) -->
<div style="width: calc(100% - 20px); padding: 21px;">
  Calculated values
</div>
Set baseFontSize in renderer options to control rem/em conversion:
const renderer = new Renderer({ baseFontSize: 16 });

Email Client Compatibility

Fully Supported

  • Apple Mail (iOS, macOS)
  • Gmail (Web, iOS, Android)
  • Outlook (Web)
  • Hey
  • Superhuman

Partial Support

  • Outlook (Windows) - Limited media query support
  • Outlook (macOS) - Some CSS3 features unsupported

Not Supported

  • Complex pseudo-selectors (:nth-child(2n+1))
  • CSS Grid in older clients
  • Advanced flexbox in Outlook Windows
Always test your emails in multiple clients. Use tools like Litmus or Email on Acid for comprehensive testing.

Best Practices

Use Mobile-First Approach

<!-- Bad: Desktop-first -->
<div class="text-lg md:text-base">
  Text size decreases on mobile
</div>

<!-- Good: Mobile-first -->
<div class="text-base md:text-lg">
  Text size increases on desktop
</div>

Prefer Inline Styles for Critical Design

<!-- Critical branding should not rely on media queries -->
<button class="bg-brand text-white font-bold py-3 px-6 rounded">
  Call to Action
</button>

Test Without Media Queries

Your email should look good even if media queries are stripped:
<!-- Looks good with or without responsive classes -->
<div class="w-full max-w-[600px] p-6 md:p-8">
  Content adapts gracefully
</div>

Troubleshooting

Classes Not Being Inlined

Problem: Classes remain in the output instead of being inlined. Cause: Classes contain pseudo-selectors or media queries. Solution: Check if you’re using responsive or state variants. These are intentionally kept in <style> tags.

Styles Not Applying

Problem: Custom classes don’t have any effect. Cause: Classes not recognized by Tailwind. Solution: Either add them to your tailwindConfig or inject them via customCSS.

Media Queries Not Working

Problem: Responsive classes have no effect. Cause: Missing <head> element. Solution: Add <Head /> component to your email:
import { Html, Head, Body } from 'better-svelte-email';

<Html>
  <Head />
  <Body>
    <!-- Your content -->
  </Body>
</Html>

Next Steps

Rendering Process

Deep dive into the rendering pipeline

Plain Text

Generate accessible plain text versions

Build docs developers (and LLMs) love