npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

svelte-email-plugin

v0.5.2

Published

A Svelte 5 port of react-email — build and render email-safe HTML emails with Svelte components.

Downloads

737

Readme

svelte-email-plugin

Build and send email-client-safe HTML emails with Svelte 5 components. A Vite plugin compiles a folder of .svelte emails — baking Tailwind classes to inline styles and codegenning a typed API — so sending is one call returning [html, text].

Svelte 5 Tailwind v4 License MIT


Features

  • Build-time Vite plugin — point it at a folder; Tailwind classes bake to inline styles, variants hoist into <Head>. No Tailwind/PostCSS/HTML-parser at runtime.
  • Generated typed API — the plugin codegens <dir>/index.ts: a typed emails object with one method per template, each component's props inferred. const [html, text] = await emails.welcome({ name }).
  • Forgiving authoring — write plain HTML (<section>, <p>, <a>, <h1>…); native tags are remapped to components and <Html>/<Head>/<Body> + imports are injected.
  • 18 components — Html, Head, Body, Container, Section, Row, Column, Text, Heading, Link, Button, Img, Hr, Preview, Font, Markdown, CodeInline, CodeBlock.
  • Outlook-ready — Button MSO padding hack, Preview inbox padding, Font @font-face + fallback.
  • render()[html, text] — complete XHTML document + plain-text alternative, in one call.
  • Typed CSS-in-JSstyle={{ color: 'red' }} checked via csstype, plus m/mx/my… margin shorthands.

Install

pnpm add svelte-email-plugin   # peer: svelte@^5

Setup

Add the plugin. It's enforce: 'pre', so it bakes before vite-plugin-svelte regardless of array position.

// vite.config.ts
import { email } from 'svelte-email-plugin/vite';
import { sveltekit } from '@sveltejs/kit/vite';

export default {
	plugins: [email({ dir: 'src/emails' }), sveltekit()]
};

Options:

| Option | Default | Description | | -------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | dir | 'src/emails' | Folder whose .svelte files are compiled. | | index | <dir>/index.ts | Generated registry path. | | importSource | package name | Module the registry + injected imports point at. Use '$lib/index.js' inside this repo. | | tailwind | auto-detect | Your @theme/@config is auto-detected from the project's CSS entry. false for the default theme only; { entry } or { css } to override (see Tailwind). | | forgiving | true | Native-tag remapping + wrapper injection (see Forgiveness). | | preview | off | { enabled, port } — a dev preview server listing every email in dir. |

Write an email

Everything under dir is compiled. Use the components:

<!-- src/emails/welcome.svelte -->
<script lang="ts">
	import { Html, Head, Preview, Body, Container, Heading, Text, Button } from 'svelte-email-plugin';

	let { name = 'there' }: { name?: string } = $props();
</script>

<Html lang="en">
	<Head />
	<Body class="bg-slate-100 font-sans">
		<Preview children="Welcome to Acme — let's get you set up." />
		<Container class="bg-white rounded-2xl p-8">
			<Heading as="h1" class="text-2xl font-bold text-slate-900">Welcome, {name}!</Heading>
			<Text class="text-slate-600">Thanks for signing up.</Text>
			<Button href="https://example.com/start" class="bg-blue-600 text-white rounded-lg px-6 py-3">
				Get started
			</Button>
		</Container>
	</Body>
</Html>

Or skip the wrappers and imports — write plain HTML and let forgiveness inject them and remap native tags.

Tailwind classes only bake inside the plugin's dir. Elsewhere, use inline style={{ … }}.

Send

The plugin writes a typed src/emails/index.ts (gitignore it). Call by name — props are type-checked, and you get back [html, text]:

import { emails } from './emails';

const [html, text] = await emails.welcome({ name: 'Ada' });

File names become camelCase keys, and sub-folders are mirrored as nested objects — the folder layout is the API:

src/emails/
├─ welcome.svelte                 → emails.welcome(props)
├─ order-receipt.svelte           → emails.orderReceipt(props)
└─ auth/
   └─ password/
      └─ reset-password.svelte    → emails.auth.password.resetPassword(props)

Each call returns [html, text] — hand them to any provider (Resend, Nodemailer/SMTP, Postmark…). Example with the Cloudflare Workers Email Sending binding (wrangler.jsonc: { "send_email": [{ "name": "EMAIL" }] }, domain onboarded via wrangler email sending enable yourdomain.com):

// SvelteKit +server.ts on Cloudflare Workers
import { emails } from '../emails';

export const GET = async ({ platform }) => {
	const [html, text] = await emails.welcome({ name: 'Ada' });
	await platform.env.EMAIL.send({
		to: '[email protected]',
		from: { email: '[email protected]', name: 'Acme' },
		subject: 'Welcome',
		html,
		text
	});
	return new Response('sent');
};

Templates

~30 ready-to-copy templates, all Tailwind-styled, brand-configurable (an Acme default), and rendering to [html, text]:

  • auth — welcome, verify-email, magic-link, otp, reset-password, password-changed, new-sign-in, team-invite
  • orders — confirmation, shipped, delivered, cancelled, refunded, return-started, return-complete
  • billing — receipt, payment-failed, trial-started, trial-ending, renewed, cancelled, card-expiring
  • marketing — announcement, promo, newsletter, winback, event-invite, feedback
  • notifications — mention, digest

Copy the ones you want into your project — pick the categories, choose a folder:

npx svelte-email-plugin

It copies the selected categories plus the shared kit (header, footer, button, line-items, Layout) into your folder and rewrites imports to svelte-email-plugin. The templates use stock Tailwind (neutral black-and-white palette) — no theme config to add. Each wraps its body in <Layout preview="…"> and is reachable through the generated registry — emails.auth.welcome({ name }), emails.orders.confirmation({ … }), etc.

Forgiveness

On by default, inside the plugin's dir. It lets you author emails loosely:

  • Missing <Html>/<Head>/<Body> are injected (so every email is a complete document, and variant classes always have a <Head> to hoist into).
  • Native tags are remapped to components: html/head/body, sectionSection, divContainer, pText, hrHr, aLink, imgImg, h1h6Heading. span and table stay native. Components that need props (Button, Font, CodeBlock, Markdown, Preview) are never auto-mapped — use them explicitly.
  • Imports for remapped/injected components are added to the email's <script>.

Tune it via the forgiving option:

email({ dir: 'src/emails', forgiving: false }); // off
email({ dir: 'src/emails', forgiving: { wrap: false } }); // remap only, no wrapper injection
email({ dir: 'src/emails', forgiving: { remap: { tags: { table: 'Section', a: false } } } }); // override the table

render(component, props?, options?)

The registry is sugar over render(). Call it directly on any component:

import { render } from 'svelte-email-plugin';
import Welcome from './emails/welcome.svelte';

const [html, text] = await render(Welcome, { name: 'Ada' }); // RenderResult = [html: string, text: string]
  • html — full XHTML document (<!DOCTYPE …> + <html>…</html>), SSR hydration markers stripped, MSO comments preserved.
  • text — plain-text alternative (html-to-text); skips <img> and the hidden <Preview>.

Destructure what you need: const [html] = … or const [, text] = …. The only option is htmlToTextOptions, forwarded to html-to-text for the text part.

Helpers: toPlainText(html, options?), cleanSvelteMarkup(html), styleToString, mergeStyle, withMargin, parsePadding, pxToPt. The svelte-email-plugin/render subpath imports only the render pipeline (no components).

Components

Every component takes a style prop (a CSSProperties object or raw string) and passes extra attributes (id, data-*, align, …) through to the element.

| Component | Renders | Notes | | ------------ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Html | <html> | lang ("en"), dir ("ltr"). | | Head | <head> | Real <head> (not <svelte:head>). Emits Content-Type + x-apple-disable-message-reformatting meta; hosts <Font> and hoisted Tailwind <style>. | | Body | <body> | Passthrough + style. | | Container | <table> | Centered table, max-width:37.5em. | | Section | <table> | Full-width table. | | Row | <table>/<tr> | Children are the row's cells (use Column). | | Column | <td> | align, valign; width via style. | | Text | <p> | Defaults font-size:14px; line-height:24px; margin:16px 0. Margin shorthands m/mx/my/mt/mr/mb/ml. | | Heading | <h1><h6> | as selects the level ("h1"). Margin shorthands. | | Link | <a> | href, target ("_blank"). Defaults color:#067df7; text-decoration:none. | | Button | <a> | MSO padding hack via style.padding (hidden <i> in <!--[if mso]>…<![endif]-->). target "_blank". | | Img | <img> | src, alt, width, height. Defaults display:block; outline:none; border:none. | | Hr | <hr> | Defaults border-top:1px solid #eaeaea; margin:26px 0. | | Preview | hidden <div> | Place as the first child of <Body>. children is a string. Truncated to ~150 chars, padded with invisible Unicode. Skipped in plain text. | | Font | <style> (@font-face) | fontFamily, fallbackFontFamily, webFont ({ url, format }), fontStyle, fontWeight. Place inside <Head>. | | Markdown | many elements | children is a Markdown source string. markdownCustomStyles / markdownContainerStyles. | | CodeInline | <code> + <span> | Inline code with the Orange.fr fallback (needs a <Head> with <meta>). | | CodeBlock | <pre>/<code> | Prism syntax highlighting. code, language, theme (use the exported xonokai), lineNumbers, fontFamily. Languages beyond Prism's defaults must be loaded via prismjs/components/*. |

<Head>
	<Font
		fontFamily="Roboto"
		fallbackFontFamily="Verdana"
		webFont={{
			url: 'https://fonts.gstatic.com/s/roboto/v30/Roboto-Regular.woff2',
			format: 'woff2'
		}}
	/>
</Head>

<CodeBlock code={`const x = 1;`} language="javascript" theme={xonokai} lineNumbers />

Tailwind

Classes resolve to email-safe values at build (oklch()rgb(), opacity modifiers→rgba(), calc()/rempx, logical→physical, rounded-full9999px) and inline; variants (sm:, hover:, …) hoist into <Head> as a <style>.

Your custom theme is picked up automatically: the plugin finds your CSS entry (src/app.css-style, or a scan of src/) and feeds its @theme / @config / @plugin to the baker, so text-brand, custom fonts, and custom spacing resolve with no config. Override with tailwind: { entry: 'src/email.css' } or tailwind: { css: '@theme { … }' }; disable with tailwind: false.

Class names must be statically extractable — static or conditional-literal:

<Section class="bg-blue-500 px-4 rounded-lg" />
<!-- ok: static -->
<Text class={isError ? 'text-red-500' : 'text-slate-600'} />
<!-- ok: conditional literal -->
<Text class="px-4 {compact ? 'py-1' : 'py-3'}" />
<!-- ok: literal branches -->
<Text class={'bg-' + color} />
<!-- build error: names file + line:column -->

Variant classes need a <Head> to hoist into (forgiveness injects one). Hoisted rules are emitted !important so a variant reliably overrides the base utility it was inlined from (e.g. text-2xl sm:text-3xl resizes as expected).

License

MIT