diff --git a/packages/tailwind/src/tailwind.spec.tsx b/packages/tailwind/src/tailwind.spec.tsx
index 0dfdd9ff72..33f8346997 100644
--- a/packages/tailwind/src/tailwind.spec.tsx
+++ b/packages/tailwind/src/tailwind.spec.tsx
@@ -877,6 +877,321 @@ describe('Tailwind component', () => {
});
});
+ describe('with theme', () => {
+ it('handles empty theme string', async () => {
+ const actualOutput = await render(
+
+ Default utilities
+ ,
+ ).then(pretty);
+
+ expect(actualOutput).toMatchInlineSnapshot(`
+ "
+
+
+ Default utilities
+
+
+ "
+ `);
+ });
+
+ it('supports custom colors', async () => {
+ const theme = `
+ @theme {
+ --color-custom: #1fb6ff;
+ }
+ `;
+
+ const actualOutput = await render(
+
+
+ ,
+ ).then(pretty);
+
+ expect(actualOutput).toMatchInlineSnapshot(`
+ "
+
+
+
+ "
+ `);
+ });
+
+ it('supports custom fonts', async () => {
+ const theme = `
+ @theme {
+ --font-sans: "Graphik", sans-serif;
+ --font-serif: "Merriweather", serif;
+ }
+ `;
+
+ const actualOutput = await render(
+
+
+
+ ,
+ ).then(pretty);
+
+ expect(actualOutput).toMatchInlineSnapshot(`
+ "
+
+
+
+
+ "
+ `);
+ });
+
+ it('supports custom spacing', async () => {
+ const theme = `
+ @theme {
+ --spacing-8xl: 96rem;
+ }
+ `;
+
+ const actualOutput = await render(
+
+
+ ,
+ ).then(pretty);
+ expect(actualOutput).toMatchInlineSnapshot(`
+ "
+
+
+
+ "
+ `);
+ });
+
+ it('supports custom border radius', async () => {
+ const theme = `
+ @theme {
+ --border-radius-4xl: 2rem;
+ }
+ `;
+
+ const actualOutput = await render(
+
+
+ ,
+ ).then(pretty);
+ expect(actualOutput).toMatchInlineSnapshot(`
+ "
+
+
+
+ "
+ `);
+ });
+
+ it('supports custom text alignment', async () => {
+ const theme = `
+ @theme {
+ --text-align-justify: justify;
+ }
+ `;
+
+ const actualOutput = await render(
+
+
+ ,
+ ).then(pretty);
+
+ expect(actualOutput).toMatchInlineSnapshot(`
+ "
+
+
+
+ "
+ `);
+ });
+
+ it('supports both config and theme props together', async () => {
+ const customConfig = {
+ theme: {
+ extend: {
+ colors: {
+ primary: '#ff0000',
+ },
+ },
+ },
+ } satisfies TailwindConfig;
+
+ const customTheme = `
+ @theme {
+ --color-secondary: #00ff00;
+ }
+ `;
+
+ const actualOutput = await render(
+
+ Both config and theme
+ ,
+ ).then(pretty);
+
+ expect(actualOutput).toMatchInlineSnapshot(`
+ "
+
+
+ Both config and theme
+
+
+ "
+ `);
+ });
+ });
+
+ describe('with utilities', () => {
+ it('handles empty utilities string', async () => {
+ const actualOutput = await render(
+
+ Default utilities
+ ,
+ ).then(pretty);
+
+ expect(actualOutput).toMatchInlineSnapshot(`
+ "
+
+
+ Default utilities
+
+
+ "
+ `);
+ });
+
+ it('supports custom utilities', async () => {
+ const utilities = `
+ .custom-shadow {
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ border-radius: 8px;
+ padding: 16px;
+ }
+ `;
+
+ const actualOutput = await render(
+
+
+ ,
+ ).then(pretty);
+
+ expect(actualOutput).toMatchInlineSnapshot(`
+ "
+
+
+
+ "
+ `);
+ });
+
+ it('supports animations', async () => {
+ const utilities = `
+ .pulse-animation {
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+ }
+ `;
+
+ const actualOutput = await render(
+
+
+ ,
+ ).then(pretty);
+
+ expect(actualOutput).toMatchInlineSnapshot(`
+ "
+
+
+
+ "
+ `);
+ });
+
+ it('supports both config and utilities props together', async () => {
+ const customConfig = {
+ theme: {
+ extend: {
+ colors: {
+ primary: '#ff0000',
+ },
+ },
+ },
+ } satisfies TailwindConfig;
+
+ const customUtilities = `
+ .card-base {
+ border: 1px solid #e5e7eb;
+ padding: 20px;
+ }
+ `;
+
+ const actualOutput = await render(
+
+ Config and utilities
+ ,
+ ).then(pretty);
+
+ expect(actualOutput).toMatchInlineSnapshot(`
+ "
+
+
+ Config and utilities
+
+
+ "
+ `);
+ });
+
+ it('supports config, theme, and utilities together', async () => {
+ const customConfig = {
+ theme: {
+ extend: {
+ colors: {
+ primary: '#ff0000',
+ },
+ },
+ },
+ } satisfies TailwindConfig;
+
+ const customTheme = `
+ @theme {
+ --color-secondary: #00ff00;
+ }
+ `;
+
+ const customUtilities = `
+ .special-border {
+ border: 2px dashed #0000ff;
+ }
+ `;
+
+ const actualOutput = await render(
+
+
+ All three props
+
+ ,
+ ).then(pretty);
+
+ expect(actualOutput).toMatchInlineSnapshot(`
+ "
+
+
+ All three props
+
+
+ "
+ `);
+ });
+ });
+
describe('with custom plugins config', () => {
const config = {
plugins: [
diff --git a/packages/tailwind/src/tailwind.tsx b/packages/tailwind/src/tailwind.tsx
index a446255319..aea51acada 100644
--- a/packages/tailwind/src/tailwind.tsx
+++ b/packages/tailwind/src/tailwind.tsx
@@ -10,19 +10,19 @@ import { mapReactTree } from './utils/react/map-react-tree';
import { cloneElementWithInlinedStyles } from './utils/tailwindcss/clone-element-with-inlined-styles';
import { setupTailwind } from './utils/tailwindcss/setup-tailwind';
+export type CSSString = string;
export type TailwindConfig = Omit;
-export interface TailwindProps {
- children: React.ReactNode;
- config?: TailwindConfig;
-}
-
export interface EmailElementProps {
children?: React.ReactNode;
className?: string;
style?: React.CSSProperties;
}
+/**
+ * The pixel based preset is the preset recommended to use for emails.
+ * It is used to style the email with a pixel based system.
+ */
export const pixelBasedPreset: TailwindConfig = {
theme: {
extend: {
@@ -82,15 +82,40 @@ export const pixelBasedPreset: TailwindConfig = {
},
};
-export function Tailwind({ children, config }: TailwindProps) {
+/**
+ * Stringifies the data to a JSON string that is safe to use.
+ * It will replace functions with their string representation.
+ */
+function JSONStringify(data: object) {
+ return JSON.stringify(data, (_key, value) =>
+ typeof value === 'function' ? value.toString() : value,
+ );
+}
+
+export interface TailwindProps {
+ children: React.ReactNode;
+ /** Tailwind config object. Used in Tailwind v3. */
+ config?: TailwindConfig;
+ /** Tailwind theme in CSS. Used in Tailwind v4. */
+ theme?: CSSString;
+ /** Tailwind utilities in CSS. Used in Tailwind v4. */
+ utility?: CSSString;
+}
+
+export function Tailwind({ children, config, theme, utility }: TailwindProps) {
+ const twConfigData = {
+ config,
+ cssConfigs: {
+ theme,
+ utility,
+ },
+ };
const tailwindSetup = useSuspensedPromise(
- () => setupTailwind(config ?? {}),
- JSON.stringify(config, (_key, value) =>
- typeof value === 'function' ? value.toString() : value,
- ),
+ () => setupTailwind(twConfigData),
+ JSONStringify(twConfigData),
);
- let classesUsed: string[] = [];
+ let classesUsed: string[] = [];
let mappedChildren: React.ReactNode = mapReactTree(children, (node) => {
if (React.isValidElement(node)) {
if (node.props.className) {
diff --git a/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.spec.ts b/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.spec.ts
index 56c4d02d47..218bd94b70 100644
--- a/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.spec.ts
+++ b/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.spec.ts
@@ -10,7 +10,7 @@ describe('sanitizeNonInlinableRules()', () => {
sanitizeNonInlinableRules(stylesheet);
expect(generate(stylesheet)).toMatchInlineSnapshot(
- `"/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-red-300: oklch(80.8% 0.114 19.571)!important;--color-gray-900: oklch(21% 0.034 264.665)!important;--text-lg: 1.125rem!important;--text-lg--line-height: calc(1.75 / 1.125)!important}}@layer utilities{.bg-gray-900{background-color:var(--color-gray-900)}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading, var(--text-lg--line-height))}.text-red-300{color:var(--color-red-300)}}"`,
+ `"/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-red-300: oklch(80.8% 0.114 19.571)!important;--color-gray-900: oklch(21% 0.034 264.665)!important;--text-lg: 1.125rem!important;--text-lg--line-height: calc(1.75 / 1.125)!important}}@layer utilities{.bg-gray-900{background-color:var(--color-gray-900)}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading, var(--text-lg--line-height))}.text-red-300{color:var(--color-red-300)}}@layer theme;@layer utilities;"`,
);
});
@@ -27,7 +27,7 @@ describe('sanitizeNonInlinableRules()', () => {
sanitizeNonInlinableRules(stylesheet);
expect(generate(stylesheet)).toMatchInlineSnapshot(
- `"/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-sky-600: oklch(58.8% 0.158 241.966)!important;--color-gray-100: oklch(96.7% 0.003 264.542)!important}}@layer utilities{.hover_text-sky-600{&:hover{@media (hover:hover){color:var(--color-sky-600)!important}}}.sm_focus_outline-none{@media (width>=40rem){&:focus{--tw-outline-style: none!important;outline-style:none!important}}}.md_hover_bg-gray-100{@media (width>=48rem){&:hover{@media (hover:hover){background-color:var(--color-gray-100)!important}}}}.lg_focus_underline{@media (width>=64rem){&:focus{text-decoration-line:underline!important}}}}"`,
+ `"/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-sky-600: oklch(58.8% 0.158 241.966)!important;--color-gray-100: oklch(96.7% 0.003 264.542)!important}}@layer utilities{.hover_text-sky-600{&:hover{@media (hover:hover){color:var(--color-sky-600)!important}}}.sm_focus_outline-none{@media (width>=40rem){&:focus{--tw-outline-style: none!important;outline-style:none!important}}}.md_hover_bg-gray-100{@media (width>=48rem){&:hover{@media (hover:hover){background-color:var(--color-gray-100)!important}}}}.lg_focus_underline{@media (width>=64rem){&:focus{text-decoration-line:underline!important}}}}@layer theme;@layer utilities;"`,
);
});
@@ -45,7 +45,7 @@ describe('sanitizeNonInlinableRules()', () => {
sanitizeNonInlinableRules(stylesheet);
expect(generate(stylesheet)).toMatchInlineSnapshot(
- `"/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--spacing: 0.25rem!important;--container-lg: 32rem!important;--radius-lg: 0.5rem!important}}@layer utilities{.sm_mx-auto{@media (width>=40rem){margin-inline:auto!important}}.sm_max-w-lg{@media (width>=40rem){max-width:var(--container-lg)!important}}.sm_rounded-lg{@media (width>=40rem){border-radius:var(--radius-lg)!important}}.md_px-10{@media (width>=48rem){padding-inline:calc(var(--spacing)*10)!important}}.md_py-12{@media (width>=48rem){padding-block:calc(var(--spacing)*12)!important}}}"`,
+ `"/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--spacing: 0.25rem!important;--container-lg: 32rem!important;--radius-lg: 0.5rem!important}}@layer utilities{.sm_mx-auto{@media (width>=40rem){margin-inline:auto!important}}.sm_max-w-lg{@media (width>=40rem){max-width:var(--container-lg)!important}}.sm_rounded-lg{@media (width>=40rem){border-radius:var(--radius-lg)!important}}.md_px-10{@media (width>=48rem){padding-inline:calc(var(--spacing)*10)!important}}.md_py-12{@media (width>=48rem){padding-block:calc(var(--spacing)*12)!important}}}@layer theme;@layer utilities;"`,
);
});
});
diff --git a/packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts b/packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts
index 251dfdb502..fe975d8733 100644
--- a/packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts
+++ b/packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts
@@ -2,17 +2,17 @@ import { generate } from 'css-tree';
import { setupTailwind } from './setup-tailwind';
test('setupTailwind() and addUtilities()', async () => {
- const { addUtilities, getStyleSheet } = await setupTailwind({});
+ const { addUtilities, getStyleSheet } = await setupTailwind({ config: {} });
addUtilities(['text-red-500', 'sm:bg-blue-300', 'bg-slate-900']);
expect(generate(getStyleSheet())).toMatchInlineSnapshot(
- `"/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-red-500: oklch(63.7% 0.237 25.331);--color-blue-300: oklch(80.9% 0.105 251.813);--color-slate-900: oklch(20.8% 0.042 265.755)}}@layer utilities{.bg-slate-900{background-color:var(--color-slate-900)}.text-red-500{color:var(--color-red-500)}.sm\\:bg-blue-300{@media (width>=40rem){background-color:var(--color-blue-300)}}}"`,
+ `"/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-red-500: oklch(63.7% 0.237 25.331);--color-blue-300: oklch(80.9% 0.105 251.813);--color-slate-900: oklch(20.8% 0.042 265.755)}}@layer utilities{.bg-slate-900{background-color:var(--color-slate-900)}.text-red-500{color:var(--color-red-500)}.sm\\:bg-blue-300{@media (width>=40rem){background-color:var(--color-blue-300)}}}@layer theme;@layer utilities;"`,
);
addUtilities(['bg-red-100']);
expect(generate(getStyleSheet())).toMatchInlineSnapshot(
- `"/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-red-100: oklch(93.6% 0.032 17.717);--color-red-500: oklch(63.7% 0.237 25.331);--color-blue-300: oklch(80.9% 0.105 251.813);--color-slate-900: oklch(20.8% 0.042 265.755)}}@layer utilities{.bg-red-100{background-color:var(--color-red-100)}.bg-slate-900{background-color:var(--color-slate-900)}.text-red-500{color:var(--color-red-500)}.sm\\:bg-blue-300{@media (width>=40rem){background-color:var(--color-blue-300)}}}"`,
+ `"/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-red-100: oklch(93.6% 0.032 17.717);--color-red-500: oklch(63.7% 0.237 25.331);--color-blue-300: oklch(80.9% 0.105 251.813);--color-slate-900: oklch(20.8% 0.042 265.755)}}@layer utilities{.bg-red-100{background-color:var(--color-red-100)}.bg-slate-900{background-color:var(--color-slate-900)}.text-red-500{color:var(--color-red-500)}.sm\\:bg-blue-300{@media (width>=40rem){background-color:var(--color-blue-300)}}}@layer theme;@layer utilities;"`,
);
});
diff --git a/packages/tailwind/src/utils/tailwindcss/setup-tailwind.ts b/packages/tailwind/src/utils/tailwindcss/setup-tailwind.ts
index fc1ec69b39..4cff011572 100644
--- a/packages/tailwind/src/utils/tailwindcss/setup-tailwind.ts
+++ b/packages/tailwind/src/utils/tailwindcss/setup-tailwind.ts
@@ -1,6 +1,6 @@
import { parse, type StyleSheet } from 'css-tree';
import { compile } from 'tailwindcss';
-import type { TailwindConfig } from '../../tailwind';
+import type { CSSString, TailwindConfig } from '../../tailwind';
import indexCss from './tailwind-stylesheets/index';
import preflightCss from './tailwind-stylesheets/preflight';
import themeCss from './tailwind-stylesheets/theme';
@@ -8,20 +8,35 @@ import utilitiesCss from './tailwind-stylesheets/utilities';
export type TailwindSetup = Awaited>;
-export async function setupTailwind(config: TailwindConfig) {
+interface CSSConfigs {
+ theme?: CSSString;
+ utility?: CSSString;
+}
+
+interface SetupTailwindProps {
+ config?: TailwindConfig;
+ cssConfigs?: CSSConfigs;
+}
+export async function setupTailwind({
+ config,
+ cssConfigs,
+}: SetupTailwindProps) {
const baseCss = `
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);
+@import "custom-theme.css" layer(theme);
+@import "custom-utilities.css" layer(utilities);
@config;
`;
+
const compiler = await compile(baseCss, {
async loadModule(id, base, resourceHint) {
if (resourceHint === 'config') {
return {
path: id,
base: base,
- module: config,
+ module: config ?? {},
};
}
@@ -63,6 +78,22 @@ export async function setupTailwind(config: TailwindConfig) {
};
}
+ if (id === 'custom-theme.css') {
+ return {
+ base,
+ path: id,
+ content: cssConfigs?.theme ?? '',
+ };
+ }
+
+ if (id === 'custom-utilities.css') {
+ return {
+ base,
+ path: id,
+ content: cssConfigs?.utility ?? '',
+ };
+ }
+
throw new Error(
'stylesheet not supported, you can only import the ones from tailwindcss',
);
diff --git a/playground/README.md b/playground/README.md
index 6a09c01c0f..d010ef92e6 100644
--- a/playground/README.md
+++ b/playground/README.md
@@ -8,9 +8,9 @@ It includes all components directly from source with a path alias import of `@re
### 1. Create an email template
-Create a new file at `playground/emails/testing.tsx`
+Create a new file at `/emails`
-```tsx emails/testing.tsx
+```tsx /playground/emails/testing.tsx
import { Html, Head, Body, Tailwind, Text } from '@react-email/components';
export default function Testing() {
diff --git a/playground/emails/.gitignore b/playground/emails/.gitignore
index bf750e163d..44e6e42710 100644
--- a/playground/emails/.gitignore
+++ b/playground/emails/.gitignore
@@ -1,2 +1,3 @@
-**/*.tsx
+**/*
+# Ignore all files in the emails directory except for the example.tsx file
!example.tsx