Manual Install
MailingUI is a set of opinionated React components, built on top of React.email (opens in a new tab), designed to make the creation of emails easier. Before you continue, make sure to read Getting Started - Introduction.
Install dependencies
We need React.email (opens in a new tab) components as our baseline and their render utility function to transform your emails into HTML.
npm install @react-email/components @react-email/render
If you are planning to write your emails using MDX (opens in a new tab). You will also need:
npm install @mdx-js/react
We assume you have an integration to compile MDX to JS, such as
@mdx-js/esbuild
, @mdx-js/loader
, @mdx-js/node-loader
, or
@mdx-js/rollup
in place. If you don't, please refer to MDX
Packages (opens in a new tab).
Create your working sub-directories
Whether you call it MailingUI or not is up to you. We recommend setting up a working directories where your emails, their components, theming, utils and assets will be.
Set up themes and utils
Emails just work differently, there is no good solution for theming. Our components are styled using a theme config (fancy way of calling our theme object) that you can modify to fit your branding needs for quick and easy customization.
We recommend creating an index file to manage all the exports from these files, specially if using path aliases. One for themes, one for utils.
import * as React from "react";
// ⚠️ Proceed with caution
export type Colors<T extends string> = Partial<
Record<T, React.CSSProperties["color"]>
>;
type StyleKey<T extends string> = keyof JSX.IntrinsicElements | T;
export type Styles<T extends string> = Partial<
Record<StyleKey<T>, React.CSSProperties>
>;
import type { Colors, Styles } from "./types";
// HELPERS
const round = (num: number) =>
num
.toFixed(7)
.replace(/(\.[0-9]+?)0+$/, "$1")
.replace(/\.0$/, "");
export const remToPx = (rem: number) => `${round(rem * 16)}px`;
// TYPE DEFINITIONS
/**
* Color variants for MailingUI Components
*
* Not meant to be exported
*/
type ColorVariants =
| "global"
| "muted"
| "muted-background"
| "primary"
| "primary-foreground"
| "destructive"
| "destructive-foreground";
export type ThemeColors<T extends string = never> = Colors<ColorVariants | T>;
/**
* Theme variants for MailingUI Components
*
* Not meant to be exported
*/
type ThemeVariants =
| "global"
| "headings"
| "text"
| "muted"
| "lead"
| "small"
| "block"
| "compact"
| "primary"
| "secondary"
| "destructive"
| "rounded";
export type Theme<T extends string = never> = Styles<ThemeVariants | T>;
// COLORS
/**
* Themed colors for MailingUI Components
*
* Add any colors with CSS for consistency
* in your styles object
*
* Not meant to be exported
*
*/
const colors: ThemeColors = {
global: "#262626", //neutral-800
muted: "#737373", // neutral-500
"muted-background": "#f5f5f5", //neutral-100
primary: "#171717", // neutral-900
"primary-foreground": "#fafafa", // neutral-50
destructive: "#b91c1c", // red-700
"destructive-foreground": "#fef2f2", // red-50
};
// THEME
/**
* Theme for MailingUI Components
*
* Add any variants with CSS to access their
* styles using the object's key or
* as a utility class combined with `cx`
*
*/
export const theme: Theme = {
global: {
fontFamily: "system-ui, sans-serif",
color: colors.global,
marginBottom: `${remToPx(1.75)}`,
},
headings: {
fontFamily: "system-ui, sans-serif",
color: colors.global,
letterSpacing: remToPx(-0.05),
marginTop: `${remToPx(2.5)}`,
fontWeight: 300,
},
text: {
fontSize: remToPx(1.125),
lineHeight: remToPx(2),
fontWeight: 300,
},
h1: {
fontSize: remToPx(3.75),
lineHeight: remToPx(3.75),
letterSpacing: remToPx(-0.05),
marginTop: 0,
},
h2: {
fontSize: remToPx(3),
lineHeight: remToPx(3),
},
h3: {
fontSize: remToPx(2.25),
lineHeight: remToPx(2.5),
},
h4: {
fontSize: remToPx(1.875),
lineHeight: remToPx(2.25),
},
p: {
marginTop: 0,
},
blockquote: {
fontStyle: "italic",
fontWeight: 500,
marginLeft: `${remToPx(0)}`,
marginRight: `${remToPx(0)}`,
padding: `0 0 0 ${remToPx(1)}`,
borderLeft: `${remToPx(0.25)} solid ${colors.muted}`,
},
hr: {
marginTop: `${remToPx(2)}`,
width: "100%",
border: "none",
borderTop: `${remToPx(0.1)} solid ${colors.muted}`,
},
code: {
fontFamily:
"ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace",
whiteSpace: "nowrap",
borderRadius: `${remToPx(0.25)}`,
padding: `${remToPx(0.125)} ${remToPx(0.25)}`,
backgroundColor: colors["muted-background"],
color: colors.muted,
},
a: {
textDecoration: "underline",
textUnderlineOffset: "6px",
color: "inherit",
},
ul: {
paddingLeft: `${remToPx(1.625)}`,
},
ol: {
paddingLeft: `${remToPx(1.625)}`,
},
li: {
paddingLeft: `${remToPx(0.375)}`,
marginBottom: `${remToPx(0.625)}`,
},
figure: {
margin: 0,
width: "100%",
},
img: {
maxWidth: "100%",
display: "block",
outline: "none",
border: "none",
textDecoration: "none",
},
figcaption: {
paddingTop: `${remToPx(0.5)}`,
textAlign: "center",
},
muted: {
color: colors.muted,
},
primary: {
border: `1px solid ${colors.primary}`,
backgroundColor: colors.primary,
color: colors["primary-foreground"],
},
secondary: {
border: `1px solid ${colors.primary}`,
backgroundColor: "transparent",
},
destructive: {
border: `1px solid ${colors.destructive}`,
backgroundColor: colors.destructive,
color: colors["destructive-foreground"],
},
lead: {
color: colors.muted,
fontSize: remToPx(1.5),
},
small: {
fontSize: remToPx(0.875),
lineHeight: remToPx(1.5),
},
block: {
display: "block",
marginBottom: `${remToPx(1)}`,
},
compact: {
marginTop: 0,
marginBottom: 0,
},
rounded: {
borderRadius: remToPx(1),
},
};
import * as React from "react";
import { Row, Column } from "@react-email/components";
import { type MDXComponents } from "mdx/types";
import { Badge } from "../components/badge/Badge";
import { Button } from "../components/button/Button";
import { Typography } from "../components/typography/Typography";
import * as mailingUIComponents from "@mailingui/components";
import { theme, type Theme } from "@mailingui/themes";
export const cx = (
inputStyles: (keyof Theme | React.CSSProperties | undefined | boolean)[],
config: {
theme?: Theme;
} = { theme }
): React.CSSProperties =>
inputStyles
.filter((s): s is keyof Theme | React.CSSProperties => Boolean(s))
.reduce<React.CSSProperties>((mergedStyles, style) => {
if (typeof style === "string") {
return { ...mergedStyles, ...theme[style], ...config.theme?.[style] };
}
return { ...mergedStyles, ...style };
}, {});
type Without<T, K> = Pick<T, Exclude<keyof T, K>>;
type MUICompType = typeof mailingUIComponents;
type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;
type ApplyThemeType = Partial<MUICompType>;
export function applyTheme<T extends ApplyThemeType>(
components: T,
theme: Theme
): Without<T, "getMDXComponents" | "Grid" | "Cell"> {
const themables = {};
Object.entries(components).forEach(([key, comp]) => {
if (typeof comp !== "function") return;
const Comp = comp as React.ComponentType<{ theme: Theme }>;
Object.assign(themables, {
[key]: (props: ComponentProps<typeof Comp>) => (
<Comp {...props} theme={theme} />
),
});
});
return themables as T;
}
export function getMDXComponents({
components,
theme,
baseUrl,
}: {
components?: MDXComponents;
theme: Theme;
baseUrl?: string;
}): MDXComponents {
const defaultComponents: MDXComponents = {
// HTML Mappings
h1: (props) => <Typography.H1 theme={theme} {...props} />,
h2: (props) => <Typography.H2 theme={theme} {...props} />,
h3: (props) => <Typography.H3 theme={theme} {...props} />,
h4: (props) => <Typography.H4 theme={theme} {...props} />,
p: (props) => <Typography.P theme={theme} {...props} />,
blockquote: (props) => <Typography.Blockquote theme={theme} {...props} />,
hr: (props) => <Typography.HR theme={theme} {...props} />,
code: (props) => <Typography.Code theme={theme} {...props} />,
a: (props) => <Typography.Link theme={theme} {...props} />,
ul: (props) => <Typography.UL theme={theme} {...props} />,
ol: (props) => <Typography.OL theme={theme} {...props} />,
li: (props) => <Typography.LI theme={theme} {...props} />,
img: ({ title, src, ...props }) => (
<Typography.Img
theme={theme}
caption={title}
src={`${baseUrl ?? ""}${src}`}
{...props}
/>
),
// React Email Components
Row: (props) => <Row {...props} />,
Column: (props) => <Column {...props} />,
// MailingUI Components
H1: (props) => <Typography.H1 theme={theme} {...props} />,
H2: (props) => <Typography.H2 theme={theme} {...props} />,
H3: (props) => <Typography.H3 theme={theme} {...props} />,
H4: (props) => <Typography.H4 theme={theme} {...props} />,
P: (props) => <Typography.P theme={theme} {...props} />,
Blockquote: (props) => <Typography.Blockquote theme={theme} {...props} />,
HR: (props) => <Typography.HR theme={theme} {...props} />,
Code: (props) => <Typography.Code theme={theme} {...props} />,
Link: (props) => <Typography.Link theme={theme} {...props} />,
UL: (props) => <Typography.UL theme={theme} {...props} />,
OL: (props) => <Typography.OL theme={theme} {...props} />,
LI: (props) => <Typography.LI theme={theme} {...props} />,
Img: ({ title, src, ...props }) => (
<Typography.Img
theme={theme}
caption={title}
src={`${baseUrl ?? ""}${src}`}
{...props}
/>
),
Badge: (props) => <Badge theme={theme} {...props} />,
Button: (props) => <Button theme={theme} {...props} />,
};
return { ...defaultComponents, ...components };
}
This theme config is completely arbitrary but completely type safe, feel free to take aid in its types and modify it to fit your needs. Just make sure to check your components.
Configure your path aliases (optional)
Create path aliases to help you write email templates easier while using MailingUI
{
"compilerOptions": {
{...}
"baseUrl": ".",
"paths": {
"@mailingui/components": ["./src/mailingui/components/index.ts"],
"@mailingui/themes": ["./src/mailingui/themes/index.ts"],
"@mailingui/utils": ["./src/mailingui/utils/index.ts"]
}
},
}
Path alias depends on where you decide to install your MailingUI components. Example above shows installation in ./src/mailingui
Congratulations! 🥳 Once you've install these dependencies you can copy and paste the components you need. See components for the full list.