A production-ready Maily email editor, installed as source through shadcn.
Bring your content, use the pre-designed blocks, and own every line that lands in your app.
maily.cn is a fork of maily.to, the
TipTap-based WYSIWYG editor for composing beautiful, mobile-ready emails from
pre-designed blocks.
The upstream project publishes Maily as npm packages. This fork takes a different distribution path: install only the editor, optional mailbox, and renderer source you actually use through the shadcn registry. You own those files, theme them, and change them like any other shadcn component.
Try the editor and mailbox components in the
live playground, including light,
dark, and system mode selection powered by shadcn-theme-provider.
Maily itself was created by Arik Chakma and its contributors.
maily.cnmaintains a source-owned shadcn distribution and the production hardening documented below.
Add the registry namespace to your existing shadcn project's components.json:
Then install the editor:
npx shadcn@latest add @maily/maily-editorAdd the optional parts only when the application uses them:
npx shadcn@latest add @maily/maily-mailbox
npx shadcn@latest add @maily/maily-rendermaily-mailbox depends on maily-editor, so adding it to a fresh project installs
both. @maily/maily remains available as the backward-compatible full install.
Every item declares only the npm packages and stock shadcn primitives imported by
its own emitted source. The editor item also wires the Tailwind typography plugin
used by the writing surface.
Designing email that behaves consistently across clients is hard. Maily gives you an
opinionated editor with reusable blocks, while maily.cn makes that editor fit the
way modern shadcn applications are built.
- Source-owned installation — no opaque editor UI dependency after installation.
- Real shadcn primitives — works with both current Radix and Base UI component styles.
- Host-controlled theming — plain Tailwind v4 utilities and the consumer's shadcn tokens.
- Generic i18n — replace every user-facing label without coupling to an i18n framework.
- Caller-driven image upload — provide your own upload handler and storage backend.
- Email-safe rendering — turn the editor JSON into HTML independently of editor theming.
- Dependency-light custom HTML — edit and preview HTML without shipping a client-side syntax-language bundle.
- Consumer icon choice — icon placeholders resolve to the library selected in
components.json.
The maintained shadcn alignment boundary lists every host-owned primitive and explains the remaining editor-specific composites.
- Logo and cover layouts
- Buttons and variants
- Variables
- Text formatting and headings
- Images and inline images
- Alignment and spacing controls
- Dividers and spacers
- Footers
- Inline code and HTML
- Link cards
- Sections and columns
- Repeat blocks
- Conditional content
| Registry item | Target | Import | Purpose |
|---|---|---|---|
maily-editor |
components/maily/** |
@/components/maily |
The <Editor /> WYSIWYG email composer. |
maily-mailbox |
components/maily/mailbox/** |
@/components/maily/mailbox |
Optional inbox / sent / drafts view. |
maily-render |
lib/maily-render/** |
@/lib/maily-render |
Editor JSON to email-safe HTML. |
maily |
All targets above | All imports above | Backward-compatible full install. |
The root @/components/maily barrel intentionally exports the editor only.
Import mailbox code from @/components/maily/mailbox so editor-only consumers
do not compile the optional mailbox surface.
- React 18 or 19
- Tailwind CSS v4 with standard shadcn theme tokens
- A project initialized with the shadcn CLI (
components.jsonpresent)
The registry automatically declares its stock shadcn dependencies and adds
@plugin "@tailwindcss/typography"; for the editor's prose content area.
import { Editor } from '@/components/maily';
export function ComposeEmail() {
return (
<Editor
contentJson={{ type: 'doc', content: [] }}
onUpdate={(editor) => {
console.log(editor.getJSON());
}}
/>
);
}<Editor /> includes the toolbar, slash-command menu, bubble menus, and writing
surface. Its chrome and canvas inherit the host application's light or dark theme.
Interactive chrome also inherits the host's installed shadcn primitives: buttons,
toggles, toggle groups, menus, popovers, tabs, command lists, inputs, tooltips, separators, and
keyboard hints come from the consumer's selected shadcn style.
All props are optional. The editor accepts initial JSON or HTML and reports changes through callbacks.
| Prop | Type | Description |
|---|---|---|
contentJson |
JSONContent |
Initial TipTap JSON: a document node or an array of nodes. |
contentHtml |
string |
Initial HTML, used when contentJson is absent. |
onCreate |
(editor) => void |
Called when the editor instance is ready. |
onUpdate |
(editor) => void |
Called on changes; read editor.getJSON() here. |
editable |
boolean |
Read-only toggle. Defaults to true. |
extensions |
AnyExtension[] |
Additional TipTap extensions merged with the defaults. |
blocks |
BlockGroupItem[] |
Replacement slash-command block list. |
config |
object |
Chrome toggles and class hooks. |
config supports hasMenuBar, hideContextMenu, spellCheck, autofocus,
immediatelyRender, initialViewMode, renderPreview, wrapClassName,
toolbarClassName, bodyClassName, renderPreviewClassName, and
contentClassName.
The toolbar includes a Design / Render toggle. Design mode keeps the editable
TipTap canvas active. Render mode shows a read-only iframe preview from
editor.getHTML() by default; pass config.renderPreview when your app wants to
mount its own preview component backed by @/lib/maily-render or a server-side
rendering endpoint.
Every user-facing string is read through the framework-agnostic labels contract.
Omit it for the English defaults or provide a complete MailyLabels object.
import { Editor, defaultLabels, type MailyLabels } from '@/components/maily';
const labels: MailyLabels = {
...defaultLabels,
'toolbar.bold': 'Pogrubienie',
'toolbar.italic': 'Kursywa',
};
<Editor labels={labels} />;MailyLabels is intentionally exhaustive. When the editor adds new UI copy, a
complete language file fails TypeScript until that key is translated.
Storage stays under the consumer's control. Configure the image upload extension with a handler that accepts a file and returns its public URL:
import { ImageUploadExtension } from '@/components/maily/editor/extensions';
<Editor
extensions={[
ImageUploadExtension.configure({
onImageUpload: async (file) => uploadImage(file),
}),
]}
/>;Users can still paste a URL when an upload handler is not appropriate.
<MailboxView /> is an optional application-shell component for the CRM/Veyme
style inbox / sent / drafts / bounced surface. It owns local folder, search,
selection, compose, draft, and polling state inside a shadcn
ResizablePanelGroup shell, but it does not assume a backend. Wire your own API
through the dataSource adapter.
import {
MailboxView,
defaultMailboxLabels,
type MailyMailboxDataSource,
} from '@/components/maily/mailbox';
const dataSource: MailyMailboxDataSource = {
listMessages: ({ folder, q }) => api.mail.messages({ folder, q }),
getMessage: (messageId) => api.mail.message(messageId),
getCounts: () => api.mail.counts(),
listContactSuggestions: ({ q, limit }) => api.contacts.search({ q, limit }),
createDraft: (draft) => api.mail.createDraft(draft),
updateDraft: (messageId, draft) => api.mail.updateDraft(messageId, draft),
discardDraft: (messageId) => api.mail.discardDraft(messageId),
sendDraft: (messageId) => api.mail.sendDraft(messageId),
runMessageAction: ({ messageId, action, value }) =>
api.mail.runMessageAction(messageId, action, value),
};
<MailboxView
account={{ address: 'hello@example.com' }}
dataSource={dataSource}
labels={defaultMailboxLabels}
/>;Recipient fields support autocomplete when the host provides contacts through
dataSource.listContactSuggestions or the contactSuggestions prop. Each
suggestion uses { address, displayName } and is matched by both email address
and display name.
Reader actions are Gmail-like but backend-agnostic. Reply and forward seed the
existing draft flow; favorite, archive, delete, mark unread, report, print,
download, and show-original controls call dataSource.runMessageAction when the
host supplies it.
defaultMailboxLabels is exhaustive, matching the editor translation contract:
copy it, translate every value, and pass the complete object back as labels.
Select text in the editor to choose from the complete Fontsource catalog. The picker fetches catalog metadata only when opened, virtualizes its results, and loads preview WOFF2 files only for visible rows, so the font library is not bundled into the installed component.
The chosen Fontsource package version, subset, weights, fallback, and family are
stored on TipTap's textStyle mark. Saved editor JSON is therefore enough for
the server-side renderer to emit version-pinned @font-face declarations without
querying Fontsource while an email is being sent. Unsupported email clients use
the family-specific email-safe fallback.
The Fontsource API and jsDelivr font CDN must be permitted by the host application's content security policy for catalog previews. Sent emails still render readable fallback fonts when a client blocks remote web fonts.
Install @maily/maily-render before using the server-side renderer.
import { render } from '@/lib/maily-render';
const html = await render(editorJson, {
preview: 'Inbox preview text',
theme: {
/* optional rendered-email theme overrides */
},
});The renderer is independent of the editor's on-screen theme and produces the final email-client-safe HTML from the saved JSON.
The shadcn CLI overwrites current registry files but does not delete files removed from a newer item. After upgrading, remove these obsolete private primitive or icon copies if they still exist and are not locally modified:
components/maily/editor/components/popover.tsx
components/maily/editor/components/ui/divider.tsx
components/maily/editor/components/ui/tooltip.tsx
components/maily/editor/components/icons/border-color.tsx
components/maily/editor/components/icons/grid-lines.tsx
components/maily/editor/components/icons/text-direction-icon.tsx
When intentionally moving from the legacy full item to maily-editor, also remove
components/maily/mailbox/** and lib/maily-render/** after confirming the
application no longer imports them.
- Change the host's shadcn theme tokens to restyle the editor globally.
- Pass layout utilities through the editor's
configclass hooks. - Edit the installed source for deeper changes; that is the point of this distribution.
- The actual sent email is styled by the renderer, not by the editor chrome.
If maily.cn saves you time, helps you ship, or makes you money, sponsorships are
very welcome. I will happily accept them xD.
You can also support the original project through Arik Chakma's GitHub Sponsors.
This is a pnpm and Turborepo monorepo. packages/core, packages/render, and
packages/shared are the source of truth. registry/**, registry.json, and the
served playground registry are generated.
pnpm install
pnpm dev
pnpm test
pnpm registry:build
pnpm playground:syncRead AGENTS.md for the architecture and repository conventions. The real consumer harness is documented in playground/README.md.
maily.cn is built on maily.to by
Arik Chakma and contributors. The fork is maintained
by brokeboiflex.
MIT. See license.

{ "registries": { "@maily": "https://raw.githubusercontent.com/brokeboiflex/maily.cn/main/playground/public/r/{name}.json", }, }