React Email, szablony wiadomości z komponentów
React Email to zestaw komponentów Reacta i narzędzie wiersza poleceń, które zamieniają szablon w JSX na HTML zrozumiały dla klientów pocztowych. Pakiet react-email ma wersję 6.9.2 z 7 sierpnia 2026 roku, repozytorium resend/react-email około 19,6 tysiąca gwiazdek, a licencja to MIT bez wariantu płatnego.
Dlaczego HTML wiadomości rządzi się innymi prawami
Strona internetowa ma jednego odbiorcę technicznego: przeglądarkę. Wiadomość e-mail ma ich kilkanaście i żaden nie musi się zgadzać z pozostałymi. Ten sam kod trafia do Gmaila w przeglądarce, do Gmaila w aplikacji na telefonie, do Outlooka w wersji na Windowsa, do Apple Mail, do Thunderbirda i do kilkunastu dostawców krajowych. Każdy z nich sam decyduje, ile z Twojego CSS przepuści.
Konsekwencje są konkretne. Arkusz stylów dołączany z zewnętrznego adresu odpada, bo klient pocztowy go nie pobierze. JavaScript odpada w całości. Reguły zapisane w znaczniku <style> działają nierówno, co dokumentacja komponentu Tailwind opisuje wprost, odsyłając do bazy caniemail.com z tabelą wsparcia dla tej funkcji. Zmienne CSS mają słabe wsparcie. Nawet zapis koloru ma znaczenie: składnia rgb(255 255 255 / 1) z odstępami, której używa nowoczesny Tailwind, nie jest powszechnie obsługiwana, a bezpieczna forma to rgb(255,255,255) z przecinkami.
Skoro nie można polegać na arkuszu stylów, style trafiają do atrybutu style przy każdym elemencie. Skoro nie można polegać na flex ani na grid, układ opisuje się zagnieżdżonymi tabelami. Tak wyglądał ręcznie pisany szablon wiadomości przez ostatnie dwie dekady i nadal tak wygląda wynik, który dostaje odbiorca.
React Email nie zmienia tych reguł, bo zmienić ich nie można. Przenosi tylko miejsce, w którym pracujesz. Piszesz komponenty, a warstwa renderująca produkuje tabele i style w atrybutach. Sprawdzenie, że tak jest naprawdę, zajmuje chwilę: rozpakowany pakiet @react-email/section, @react-email/row i @react-email/container w skompilowanym kodzie ma wprost element table. To nie są nazwy semantyczne udające coś innego, to dosłownie generatory tabel.
Jest jeden wyjątek od zasady wstawiania stylów do atrybutów. Zapytania medialne, czyli @media, nie dają się zapisać w atrybucie style, więc muszą wylądować w znaczniku <style> w sekcji <head>. Dokumentacja komponentu Tailwind opisuje to jako świadomy kompromis wraz z drugą decyzją: nazwy klas w tym znaczniku są oczyszczane, żeby uniknąć znaków wymagających poprzedzania ukośnikiem, bo to potrafi rozłożyć część klientów.
Wersja, licencja i skład pakietu
Rozróżnienie trzech pakietów oszczędza sporo zamieszania przy pierwszej instalacji.
Pierwszy to react-email w wersji 6.9.2, wydany 7 sierpnia 2026 roku. To narzędzie wiersza poleceń, udostępniające polecenie email. W środku siedzą esbuild, chokidar, socket.io, commander, tailwindcss oraz @babel/parser i @babel/traverse, czyli komplet potrzebny do serwera podglądu z przeładowaniem na żywo. Ciekawostka z kodu startowego: plik wykonywalny sprawdza, czy proces Node dostał flagi --experimental-vm-modules oraz --disable-warning=ExperimentalWarning, a jeśli nie, uruchamia sam siebie ponownie z tymi flagami. Nie musisz o nich pamiętać, ale jeśli osadzasz to w skrypcie, wiesz już, skąd bierze się dodatkowy proces potomny.
Drugi to @react-email/components w wersji 1.0.12, czyli paczka zbiorcza. Sama nic nie renderuje, tylko wciąga dwadzieścia osobnych pakietów: body, button, code-block, code-inline, column, container, font, head, heading, hr, html, img, link, markdown, preview, render, row, section, tailwind i text. Każdy ma własną numerację wersji i własny cykl wydawniczy.
Trzeci to @react-email/render, który jest jednocześnie zależnością obu powyższych i osobnym pakietem. Bieżące wydanie w rejestrze to 2.1.0, natomiast @react-email/components w wersji 1.0.12 przypina wewnętrznie 2.0.6. Jeśli w projekcie sprawdzasz wersje zależności, ta różnica jest normalna i wynika z tego, że paczka zbiorcza przypina dokładne wersje z chwili swojego wydania.
Licencja jest jednoznaczna i to rzadkość warta odnotowania. Plik LICENSE.md w katalogu głównym repozytorium zawiera pełny tekst MIT z notą „Copyright 2024 Plus Five Five, Inc". Pole license w rejestrze npm dla react-email, dla @react-email/components i dla pakietów składowych ma wartość MIT. Interfejs programistyczny GitHuba raportuje dla repozytorium MIT License z identyfikatorem SPDX MIT. Nie ma katalogu z osobną licencją komercyjną, nie ma podwójnego wyboru, nie ma dodatku, którego instalacja wciąga pakiet zastrzeżony. Cała funkcjonalność jest w wydaniu otwartym.
Stan projektu: 19638 gwiazdek, 1076 rozgałęzień, 31 otwartych zgłoszeń, ostatnia zmiana w gałęzi głównej z 19 sierpnia 2026 roku, repozytorium założone we wrześniu 2022 i niezarchiwizowane. Wymagania środowiska są łagodne, bo zależności równorzędne dopuszczają Reacta w wersji 18 albo 19 razem z odpowiadającym react-dom.
Instalacja i podgląd lokalny
Wejście do istniejącego projektu wygląda tak.
# narzędzie wiersza poleceń i komponenty
npm install --save-dev react-email
npm install @react-email/components
# serwer podglądu, domyślnie katalog ./emails i port 3000
npx email dev
# własny katalog i port
npx email dev --dir ./src/emails --port 3001
# ostrzeżenia o zgodności dla wybranych klientów
npx email dev --clients gmail,outlook,protonmail
# wygenerowanie plików HTML do katalogu out
npx email export --outDir out --pretty
# wersja tekstowa zamiast HTML
npx email export --plainText
# własne rozszerzenie pliku wynikowego
npx email export --dir ./src/emails --extension blade.php
# nowy projekt od zera
npx create-email@latestPolecenie email dev uruchamia aplikację podglądu w przeglądarce z listą szablonów z katalogu i przeładowaniem po zapisie pliku. Flaga --clients przyjmuje listę rozdzielaną przecinkami i włącza ostrzeżenia o zgodności. Dopuszczalne wartości to gmail, outlook, yahoo, apple-mail, aol, thunderbird, microsoft, samsung-email, sfr, orange, protonmail, hey, mail-ru, fastmail, laposte, t-online-de, free-fr, gmx, web-de, ionos-1and1, rainloop i wp-pl, czyli dwadzieścia dwie pozycje z polskim wp-pl włącznie. Tę samą listę można podać zmienną środowiskową COMPATIBILITY_EMAIL_CLIENTS, a flaga ją nadpisuje.
Polecenia email build i email start służą do zbudowania i uruchomienia samej aplikacji podglądu, którą narzędzie kopiuje do katalogu .react-email. Przydają się, gdy chcesz wystawić podgląd szablonów zespołowi marketingowemu pod stałym adresem, zamiast prosić o uruchamianie serwera lokalnie.
Najważniejsze jest email export. Bierze szablony z katalogu i zapisuje wynik jako pliki. Opcja --extension pozwala nadać im dowolne rozszerzenie, a dokumentacja polecenia podaje jako przykład blade.php. Dla oceny przywiązania do dostawcy to jest właśnie ten szczegół, który rozstrzyga sprawę: wynikiem pracy React Email jest zwykły plik tekstowy, który możesz oddać dowolnemu systemowi, także szablonizatorowi w innym języku.
Osobno publikowany pakiet create-email w wersji 1.2.5 zakłada nowy projekt z przykładowymi szablonami. Ma trzy zależności i sprowadza się do skopiowania katalogu startowego.
Anatomia szablonu
Poniżej szablon powitalny korzystający z komponentów, których nazwy pól sprawdziłem w opublikowanych deklaracjach typów.
import {
Body, Button, Column, Container, Font, Head, Heading,
Hr, Html, Img, Link, Preview, Row, Section, Text
} from '@react-email/components'
interface WelcomeEmailProps {
userName: string
activationUrl: string
}
export default function WelcomeEmail({ userName, activationUrl }: WelcomeEmailProps) {
return (
<Html lang="pl" dir="ltr">
<Head>
<Font
fontFamily="Inter"
fallbackFontFamily={['Helvetica', 'Arial', 'sans-serif']}
webFont={{
url: 'https://example.com/fonts/inter-regular.woff2',
format: 'woff2'
}}
fontWeight={400}
fontStyle="normal"
/>
</Head>
<Preview>Potwierdź adres i zacznij korzystać z konta</Preview>
<Body style={{ backgroundColor: '#f4f4f5', margin: 0, padding: '24px 0' }}>
<Container style={{ backgroundColor: '#ffffff', padding: '32px', maxWidth: '600px' }}>
<Img
src="https://example.com/logo.png"
alt="Logo firmy"
width="120"
height="32"
/>
<Heading as="h1" mt={24} mb={8} style={{ fontSize: '24px' }}>
Cześć {userName}
</Heading>
<Text style={{ fontSize: '16px', lineHeight: '24px', color: '#3f3f46' }}>
Zostało jedno kliknięcie. Potwierdź adres, żeby aktywować konto.
</Text>
<Section style={{ marginTop: '24px' }}>
<Button
href={activationUrl}
style={{
backgroundColor: '#2563eb',
color: '#ffffff',
padding: '12px 20px',
borderRadius: '6px',
msoPaddingAlt: '0px'
}}
>
Aktywuj konto
</Button>
</Section>
<Hr style={{ borderColor: '#e4e4e7', margin: '32px 0' }} />
<Row>
<Column>
<Text style={{ fontSize: '12px', color: '#71717a' }}>
Nie zakładałeś konta? Zignoruj tę wiadomość.
</Text>
</Column>
<Column align="right">
<Link href="https://example.com/pomoc" style={{ fontSize: '12px' }}>
Pomoc
</Link>
</Column>
</Row>
</Container>
</Body>
</Html>
)
}Kilka szczegółów z tego pliku zasługuje na komentarz. Font musi stać wewnątrz Head, wymaga pól fontFamily i fallbackFontFamily, a webFont przyjmuje obiekt z adresem i formatem z listy woff, woff2, truetype, opentype, embedded-opentype i svg. Wartości zapasowe wybiera się z zamkniętego zbioru czcionek systemowych, a kolejność tablicy jest kolejnością priorytetu. Sama deklaracja typu ostrzega, że nie wszystkie klienty obsługują czcionki sieciowe, i odsyła do bazy zgodności.
Preview przyjmuje wyłącznie tekst, bo generuje ukryty fragment, który klient pocztowy pokazuje obok tematu na liście wiadomości. Bez niego zobaczysz tam pierwsze słowa treści, często adres obrazka albo powitanie bez informacji.
Heading ma pole as przyjmujące wartości od h1 do h6 oraz osobne pola marginesów m, mx, my, mt, mr, mb i ml. Button jest zwykłym odnośnikiem i przyjmuje wszystkie atrybuty elementu a, w tym href i target. Ten pakiet dokłada do typu React.CSSProperties dwa własne pola, msoPaddingAlt oraz msoTextRaise, i to jest miejsce, w którym specyfika klientów pocztowych wychodzi wprost do interfejsu programistycznego.
Tailwind CSS wewnątrz wiadomości
Komponent Tailwind z pakietu @react-email/tailwind pozwala pisać klasy zamiast obiektów stylu. Bieżąca wersja to 2.0.7 i zależy od tailwindcss w wersji co najmniej 4.1.18, czyli od czwartej linii Tailwind CSS.
import { Button, Container, Section, Tailwind, Text } from '@react-email/components'
export default function OfferEmail({ discountCode }: { discountCode: string }) {
return (
<Tailwind
config={{
theme: {
extend: {
colors: {
brand: '#2563eb',
muted: '#71717a'
}
}
}
}}
>
<Container className="mx-auto max-w-[600px] bg-white p-8">
<Section>
<Text className="text-[18px] font-semibold text-brand">
Kod rabatowy: {discountCode}
</Text>
<Text className="text-[14px] text-muted">
Kod działa do końca miesiąca.
</Text>
<Button
href="https://example.com/sklep"
className="rounded-md bg-brand px-5 py-3 text-white"
>
Przejdź do sklepu
</Button>
</Section>
</Container>
</Tailwind>
)
}Pole config przyjmuje konfigurację Tailwind pozbawioną klucza content, co widać w deklaracji typu jako Omit<Config, 'content'>. Klucz content odpada, bo nie ma tu skanowania plików: komponent widzi drzewo Reacta, które opakowuje, i tyle mu wystarczy.
Mechanizm działania jest opisany w pliku readme pakietu i dobrze go znać, zanim zdziwisz się wynikiem. Klasy są zamieniane na style wpisywane do atrybutu style renderowanych elementów, a nie do wspólnego arkusza. Zapytania medialne, których nie da się w ten sposób wstawić, trafiają do znacznika <style> w sekcji <head> wraz z powiązanymi nazwami klas, przy czym nazwy są sanityzowane. Zmienne CSS, na których czwarty Tailwind opiera dużą część swojej konfiguracji, rozwiązuje osobna wtyczka PostCSS, a te, których nie potrafi rozwiązać, zostawia bez zmian. Kolory zapisane składnią z odstępami są przepisywane na składnię z przecinkami, a przezroczystość podana ukośnikiem staje się czwartym argumentem funkcji rgb.
Praktyczny wniosek jest taki, że część możliwości Tailwind przechodzi bez zastrzeżeń, a część cicho wypada. Odstępy, kolory, rozmiary tekstu i zaokrąglenia zamieniają się na proste właściwości i działają. Wszystko, co opiera się na flex, grid albo pozycjonowaniu, wymaga ostrożności, bo klasa się wygeneruje, styl trafi do atrybutu, a klient pocztowy i tak może go zignorować. Układ nadal buduj z Section, Row i Column, a Tailwind traktuj jako wygodniejszy zapis wartości, nie jako sposób opisu siatki.
Renderowanie i wysyłka dowolnym dostawcą
Cały kontakt szablonu ze światem zewnętrznym przechodzi przez jedną funkcję.
import { render } from '@react-email/render'
import nodemailer from 'nodemailer'
import WelcomeEmail from './emails/welcome'
const element = WelcomeEmail({
userName: 'Anna',
activationUrl: 'https://example.com/aktywacja?token=abc'
})
const html = await render(element, { pretty: true })
const text = await render(element, {
plainText: true,
htmlToTextOptions: { wordwrap: 80 }
})
const transporter = nodemailer.createTransport({
host: 'smtp.example.com',
port: 587,
auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS }
})
await transporter.sendMail({
from: 'powitania@example.com',
to: 'anna@example.com',
subject: 'Potwierdź adres',
html,
text
})Funkcja render zwraca obietnicę z łańcuchem znaków. Zestaw opcji jest krótki i cały wynika z deklaracji typu Options: pretty formatuje wynik przez Prettier, plainText przełącza wyjście na tekst, htmlToTextOptions przekazuje ustawienia wprost do biblioteki html-to-text, a unstableTextConversion włącza własny konwerter tekstowy zamiast tamtej biblioteki i wtedy htmlToTextOptions przestaje być dostępne. Nazwa z przedrostkiem unstable jest ostrzeżeniem, nie ozdobą.
Tu dochodzimy do kwestii, która najczęściej budzi wątpliwości. Projekt powstał w Resend i należy do tej samej firmy, co widać w nocie licencyjnej, ale nie tworzy żadnego przywiązania w czasie działania. Wynikiem render jest łańcuch z HTML. Ten łańcuch podajesz jako pole html dowolnemu klientowi SMTP albo dowolnemu interfejsowi HTTP dostawcy. Działa z Resend, z Postmark, z Loops, z własnym serwerem pocztowym i z każdym innym rozwiązaniem, które przyjmuje HTML.
Jedyne miejsce, w którym Resend pojawia się w samym narzędziu, to para poleceń email resend setup i email resend reset. Pierwsze zapisuje klucz interfejsu programistycznego w konfiguracji narzędzia, drugie go usuwa. Służy to wysyłaniu testowych wiadomości z poziomu podglądu. Jest w pełni opcjonalne, a bez uruchomienia tych poleceń narzędzie nigdzie nie sięga.
Osobna sprawa to zakres odpowiedzialności. React Email nie wysyła wiadomości, nie prowadzi list odbiorców, nie obsługuje wypisania się z subskrypcji, nie zbiera statystyk otwarć i nie zarządza reputacją domeny nadawcy. Nie zastępuje też uwierzytelniania SPF, DKIM i DMARC. To warstwa szablonów, a nie platforma pocztowa, i mieszanie tych ról prowadzi do rozczarowania. Renderowanie po stronie serwera w Next.js czy w dowolnym środowisku Node robisz sam, w miejscu, w którym i tak wywołujesz dostawcę.
React Email a alternatywy
| Cecha | React Email | MJML | Maizzle | jsx-email | Ręczny HTML |
|---|---|---|---|---|---|
| Język szablonu | JSX i komponenty Reacta | własne znaczniki XML | HTML z klasami narzędziowymi | JSX i komponenty Reacta | HTML pisany bezpośrednio |
| Model wykonania | renderowanie Reacta do HTML | kompilacja znaczników do HTML | kompilacja z inline'owaniem CSS | renderowanie Reacta do HTML | brak kroku budowania |
| Integracja z Tailwind CSS | komponent Tailwind, linia 4 | poza rdzeniem | wbudowana, rdzeń rozwiązania | dostępna jako dodatek | nie dotyczy |
| Bieżąca wersja | 6.9.2 | 5.4.0 | 6.1.0 | 3.2.1 | nie dotyczy |
| Licencja | MIT | MIT | MIT | MIT | nie dotyczy |
Wybór rozstrzyga się na jednym pytaniu: czy Twój zespół i tak pisze w Reakcie. Jeśli tak, React Email pozwala trzymać szablony w tym samym repozytorium, w tym samym języku i pod tą samą kontrolą typów, co reszta interfejsu. Właściwości szablonu są zwykłym interfejsem TypeScript, więc zmiana nazwy pola w danych psuje kompilację zamiast psuć wiadomość u odbiorcy.
Jeśli szablony pisze osoba, która nie zna Reacta, rachunek wygląda inaczej. MJML ma prostszą składnię i nie wymaga środowiska Node w codziennej pracy nad treścią. Maizzle jest dobrym wyborem tam, gdzie punktem wyjścia jest gotowy HTML i chodzi głównie o wygodne inline'owanie stylów. Ręczne pisanie tabel nadal ma sens przy jednym szablonie transakcyjnym, którego nikt nie rusza od dwóch lat, bo każde narzędzie to kolejna zależność do aktualizowania.
Typowe błędy
Pierwszy to pisanie szablonu tak, jak pisze się stronę. display: flex, position: absolute, obraz w tle i pseudoelementy nie mają gwarancji działania. Do układu służą Section, Row i Column, które renderują tabele, i to jest jedyna konstrukcja, na której można polegać wszędzie.
Drugi to pominięcie wersji tekstowej. Część odbiorców i część filtrów widzi tylko ją. Wystarczy drugie wywołanie render z opcją plainText: true i przekazanie wyniku jako pole text przy wysyłce.
Trzeci to obrazy bez wymiarów i bez tekstu alternatywnego. Wiele klientów domyślnie blokuje pobieranie obrazów, więc odbiorca zobaczy najpierw ramkę z tekstem alt. Adres w polu src musi być bezwzględny i publicznie dostępny, bo ścieżka względna nie ma tu do czego się odnieść.
Czwarty to brak komponentu Preview. Bez niego lista wiadomości pokazuje początek treści, który zwykle jest powitaniem albo tekstem alternatywnym pierwszego obrazka.
Piąty to poleganie na zmiennych CSS w konfiguracji Tailwind. Wtyczka rozwiązuje je przed renderowaniem, ale te, których rozwiązać nie potrafi, zostawia w wyniku bez zmian, a klient pocztowy nie zrobi z nimi nic sensownego.
Szósty to mylenie podglądu w przeglądarce z testem w skrzynce. Serwer email dev renderuje w przeglądarce, czyli w środowisku znacznie łagodniejszym niż docelowe. Flaga --clients daje ostrzeżenia o zgodności, ale ostatecznym sprawdzeniem pozostaje wysłanie wiadomości na realne konta.
Siódmy to instalowanie samego react-email i szukanie w nim komponentów. Ten pakiet zawiera narzędzie wiersza poleceń. Komponenty siedzą w @react-email/components albo w pakietach jednostkowych i trzeba je dodać osobno.
FAQ
Czy React Email wiąże mnie z Resend?
Nie w warstwie technicznej. Wynikiem działania jest łańcuch z HTML, który podajesz dowolnemu dostawcy albo serwerowi SMTP. Polecenia email resend setup i email resend reset są opcjonalne i dotyczą tylko wysyłania testów z podglądu. Powiązanie jest własnościowe, bo projekt należy do firmy stojącej za Resend, i to warto mieć na uwadze przy ocenie kierunku rozwoju.
Czy muszę używać Reacta w aplikacji, żeby użyć React Email?
Nie w warstwie produkcyjnej frontendu, ale potrzebujesz Reacta i react-dom jako zależności w środowisku, które renderuje wiadomości. Zależności równorzędne dopuszczają wersje 18 i 19. Możesz też pójść drogą email export i traktować React Email jako generator plików HTML uruchamiany poza aplikacją.
Czy Tailwind naprawdę działa w wiadomościach?
Częściowo i to jest uczciwa odpowiedź. Klasy zamieniają się na style w atrybucie style, zapytania medialne trafiają do znacznika <style>, kolory są przepisywane na składnię obsługiwaną przez klienty, a zmienne CSS rozwiązuje wtyczka PostCSS. Wszystko, co dotyczy odstępów, kolorów i typografii, przechodzi dobrze. Układ oparty na flex czy grid nadal jest ryzykowny.
Jak wygenerować wersję tekstową wiadomości?
Wywołaniem render(element, { plainText: true }). Domyślnie konwersję wykonuje biblioteka html-to-text, którą można nastroić przez htmlToTextOptions. Opcja unstableTextConversion włącza własny konwerter projektu i wtedy przekazywanie ustawień tamtej biblioteki nie jest dostępne.
Czy podgląd lokalny zastępuje testy w klientach pocztowych?
Nie. Serwer email dev pokazuje szablon w przeglądarce, która przepuści znacznie więcej niż docelowe programy pocztowe. Flaga --clients włącza ostrzeżenia o zgodności dla dwudziestu dwóch klientów, w tym wp-pl, ale to nadal analiza statyczna, a nie realny render.
Czy React Email jest płatny?
Nie. Pakiety react-email, @react-email/components i wszystkie składowe mają w rejestrze npm licencję MIT, a plik LICENSE.md w repozytorium zawiera pełny tekst MIT. Nie ma wariantu komercyjnego, nie ma funkcji zamkniętej za dopłatą i nie ma limitu użycia. Płatna jest usługa wysyłki, jeśli ją wybierzesz, ale to osobny produkt.
Dokumentację znajdziesz na stronie react.email, kod źródłowy w repozytorium na GitHubie, a tabele wsparcia dla poszczególnych własności CSS w bazie caniemail.