mjmx

JSX runtime for MJML email templates. No React required.

$ npm install @mjmx/core mjml
npm version license CI codecov

Features

Zero React Dependency

Pure JSX runtime with its own AST. No react or react-dom needed.

Full TypeScript Support

Template literal types for CSS values, colors, percentages. Autocomplete that works.

All MJML Components

40+ components with strictly typed attributes. Every MJML tag, fully supported.

Component Composition

Reusable email components with PropsWithChildren, fragments, and conditionals.

Two Render Modes

render() for HTML output, serialize() for MJML strings.

Lightweight & Fast

Pure string manipulation. No virtual DOM, no reconciler.

Code Examples

import { render } from '@mjmx/core';

const Email = ({ name }: { name: string }) => (
  <mjml>
    <mj-body>
      <mj-section>
        <mj-column>
          <mj-text font-size="20px" color="#333">
            Hello {name}!
          </mj-text>
          <mj-button href="https://example.com">Click me</mj-button>
        </mj-column>
      </mj-section>
    </mj-body>
  </mjml>
);

const { html, errors } = render(<Email name="World" />);
import { render, type PropsWithChildren } from '@mjmx/core';

const Card = ({ children, title }: PropsWithChildren<{ title: string }>) => (
  <mj-section>
    <mj-column>
      <mj-text font-weight="bold">{title}</mj-text>
      {children}
    </mj-column>
  </mj-section>
);

const Header = () => (
  <mj-section background-color="#2c3e50">
    <mj-column>
      <mj-text color="#fff" font-size="24px">My App</mj-text>
    </mj-column>
  </mj-section>
);

const WelcomeEmail = ({ name }: { name: string }) => (
  <mjml>
    <mj-body>
      <Header />
      <Card title="Welcome!">
        <mj-text>Hello {name}, thanks for joining.</mj-text>
      </Card>
    </mj-body>
  </mjml>
);

mjmx (JSX)

const OrderEmail = ({ items }: Props) => (
  <mjml>
    <mj-body>
      <mj-section>
        {items.map(item => (
          <mj-column>
            <mj-text>{item.name}</mj-text>
            <mj-text>{item.price}</mj-text>
          </mj-column>
        ))}
      </mj-section>
    </mj-body>
  </mjml>
);

MJML + Handlebars

<mjml>
  <mj-body>
    <mj-section>
      {{#each items}}
      <mj-column>
        <mj-text>{{this.name}}</mj-text>
        <mj-text>{{this.price}}</mj-text>
      </mj-column>
      {{/each}}
    </mj-section>
  </mj-body>
</mjml>

<!-- No type safety -->
<!-- No autocomplete -->
<!-- No component reuse -->

Getting Started

1

Install

Add mjmx and mjml to your project.

$ npm install @mjmx/core mjml
2

Configure TypeScript

Set the JSX transform in your tsconfig.json.

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@mjmx/core"
  }
}

Or use the pragma: /** @jsxImportSource @mjmx/core */

3

Write Your First Email

Create a component and render it to HTML.

import { render } from '@mjmx/core';

const { html } = render(
  <mjml>
    <mj-body>
      <mj-section>
        <mj-column>
          <mj-text>Hello World!</mj-text>
        </mj-column>
      </mj-section>
    </mj-body>
  </mjml>
);