zenith-editor
v1.6.1
Published
A modern, production-ready WYSIWYG editor built with React and TypeScript featuring font selection, custom font loading, and comprehensive rich text editing capabilities
Maintainers
Readme
Zenith Editor
A modern, production-ready WYSIWYG editor built with React, TypeScript, and Tiptap. Perfect for Next.js applications with full SSR support and comprehensive font management.
✨ Features
- 🎨 Modern UI: Clean, minimalist floating toolbar design
- 🔧 TypeScript: Fully typed for excellent developer experience
- 🚀 SSR Ready: Full Next.js Server-Side Rendering compatibility
- 📱 Responsive: Works perfectly on desktop and mobile devices
- 🎯 Lightweight: Only open-source dependencies, no vendor lock-in
- 🧩 Extensible: Built on Tiptap for maximum customization
- 🎨 Tailwind CSS: Styled with Tailwind for easy customization
- ✨ Font Selector: Built-in font selector with system and custom fonts
Built-in Features
- 🎨 Font Selection: Intuitive font selector dropdown with system fonts and custom font integration
- 🌈 Color Picker: Built-in color picker for text with predefined colors and custom color input
- Text Formatting: Bold, Italic, Underline, Strikethrough with color support
- Custom Font Loading: Load fonts using FontFace Web API with fallback support
- Content Styling: Custom fonts, colors, spacing, and typography
- Multi-language Support: Full Unicode support with custom font loading
- Headings: H1, H2, H3 with proper hierarchy
- Lists: Ordered and unordered lists
- Links: Easy link creation and editing with popup dialog
- Images: Drag & drop or paste images with custom upload handling
- Code Blocks: Syntax-highlighted code blocks with language-specific highlighting
- Blockquotes: Beautiful blockquote styling
- History: Full undo/redo support
- Output: Clean HTML and JSON export
📦 Installation
# Using pnpm (recommended)
pnpm add zenith-editor
# Using npm
npm install zenith-editor
# Using yarn
yarn add zenith-editorPeer Dependencies
pnpm add react react-dom🚀 Quick Start
Basic Usage
import React, { useState } from 'react';
import { ZenithEditor } from 'zenith-editor';
import 'zenith-editor/styles';
function MyComponent() {
const [content, setContent] = useState('<p>Hello world!</p>');
return (
<ZenithEditor
initialContent={content}
placeholder="Start typing..."
onUpdate={({ html, json }) => {
setContent(html);
console.log('Content updated:', { html, json });
}}
/>
);
}Next.js App Router Usage
For Next.js applications, use dynamic imports to ensure proper SSR compatibility:
'use client';
import React, { useState } from 'react';
import dynamic from 'next/dynamic';
// Import the editor dynamically to avoid SSR issues
const ZenithEditor = dynamic(
() => import('zenith-editor').then((mod) => mod.ZenithEditor),
{
ssr: false,
loading: () => <p>Loading editor...</p>,
}
);
export default function EditorPage() {
const [content, setContent] = useState('');
return (
<div className="max-w-4xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-4">My Editor</h1>
<ZenithEditor
initialContent="<p>Start writing your content here...</p>"
placeholder="What's on your mind?"
onUpdate={({ html, json }) => {
setContent(html);
}}
onImageUpload={async (file) => {
// Implement your image upload logic
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
});
const { url } = await response.json();
return url;
}}
/>
<div className="mt-4">
<h2 className="text-lg font-semibold">Output:</h2>
<pre className="bg-gray-100 p-4 rounded mt-2 text-sm">{content}</pre>
</div>
</div>
);
}Image Upload Implementation
import { ZenithEditor } from 'zenith-editor';
function EditorWithImageUpload() {
const handleImageUpload = async (file: File): Promise<string> => {
try {
// Option 1: Upload to your API
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/upload-image', {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error('Upload failed');
}
const { url } = await response.json();
return url;
// Option 2: Upload to cloud storage (e.g., AWS S3)
// const { url } = await uploadToS3(file);
// return url;
// Option 3: Convert to base64 (not recommended for production)
// return new Promise((resolve) => {
// const reader = new FileReader();
// reader.onload = () => resolve(reader.result as string);
// reader.readAsDataURL(file);
// });
} catch (error) {
console.error('Image upload failed:', error);
throw error;
}
};
return (
<ZenithEditor
onImageUpload={handleImageUpload}
placeholder="Type something or drag an image..."
/>
);
}🔧 API Reference
ZenithEditor Props
| Prop | Type | Default | Description |
| -------------------- | --------------------------------------- | ------------------- | --------------------------------------------- |
| initialContent | string \| JSONContent | '' | Initial content as HTML string or Tiptap JSON |
| placeholder | string | 'Start typing...' | Placeholder text when editor is empty |
| editable | boolean | true | Whether the editor should be editable |
| showToolbar | boolean | true | Whether to show the toolbar |
| autoFocus | boolean \| 'start' \| 'end' \| number | false | Auto focus behavior on mount |
| className | string | undefined | Additional CSS class for the editor |
| containerClassName | string | undefined | Additional CSS class for the container |
| editorClassName | string | undefined | Additional CSS class for the editor content |
| style | React.CSSProperties | undefined | Inline styles for the container |
| contentStyle | React.CSSProperties | undefined | Inline styles for the editor content |
| customFonts | CustomFontDefinition[] | [] | Custom fonts to load using FontFace API |
| fontLoadOptions | FontLoadOptions | {} | Options for font loading behavior |
| onFontsLoaded | (fonts: string[]) => void | undefined | Callback when fonts are successfully loaded |
| onFontLoadError | (error) => void | undefined | Callback when font loading fails |
| onUpdate | (props) => void | undefined | Callback when content changes |
| onImageUpload | (file: File) => Promise<string> | undefined | Custom image upload handler |
| extensions | Extension[] | [] | Additional Tiptap extensions |
| toolbar | React.ComponentType | Toolbar | Custom toolbar component |
onUpdate Callback
onUpdate: ({ editor, html, json }) => {
// editor: Tiptap Editor instance
// html: Current content as HTML string
// json: Current content as Tiptap JSON object
};useZenithEditor Hook
For advanced use cases, you can use the hook directly:
import { useZenithEditor } from 'zenith-editor';
function CustomEditor() {
const {
editor,
getHTML,
getJSON,
setContent,
clearContent,
focus,
isEmpty,
canUndo,
canRedo,
undo,
redo,
} = useZenithEditor({
initialContent: '<p>Hello</p>',
onUpdate: ({ html }) => console.log(html),
});
return (
<div>
<button onClick={() => setContent('<p>New content</p>')}>
Set Content
</button>
<button onClick={clearContent}>Clear</button>
<button onClick={focus}>Focus</button>
<button onClick={undo} disabled={!canUndo}>
Undo
</button>
<button onClick={redo} disabled={!canRedo}>
Redo
</button>
<EditorContent editor={editor} />
<div>
<strong>Empty:</strong> {isEmpty ? 'Yes' : 'No'}
</div>
</div>
);
}Imperative API with Refs
import { useRef } from 'react';
import { ZenithEditor, ZenithEditorRef } from 'zenith-editor';
function EditorWithRef() {
const editorRef = useRef<ZenithEditorRef>(null);
const handleSave = () => {
if (editorRef.current) {
const html = editorRef.current.getHTML();
const json = editorRef.current.getJSON();
// Save content...
console.log({ html, json });
}
};
const handleClear = () => {
editorRef.current?.clearContent();
};
return (
<div>
<ZenithEditor ref={editorRef} />
<div className="mt-4 space-x-2">
<button onClick={handleSave}>Save</button>
<button onClick={handleClear}>Clear</button>
</div>
</div>
);
}🎨 Customization
Content Styling
Customize the appearance of your editor content with the contentStyle prop. This allows you to apply custom fonts, colors, spacing, and other CSS properties directly to the editor content:
import { ZenithEditor } from 'zenith-editor';
function StyledEditor() {
return (
<ZenithEditor
initialContent="<h2>Custom Styled Content</h2><p>This content has custom styling applied.</p>"
contentStyle={{
fontFamily: 'Georgia, serif',
fontSize: '18px',
lineHeight: '1.8',
color: '#2c3e50',
letterSpacing: '0.5px'
}}
/>
);
}Content Styling Examples
Elegant Serif Style:
<ZenithEditor
contentStyle={{
fontFamily: 'Georgia, "Times New Roman", serif',
fontSize: '16px',
lineHeight: '1.7',
color: '#2c3e50'
}}
/>Modern Sans-Serif Style:
<ZenithEditor
contentStyle={{
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
fontSize: '15px',
lineHeight: '1.6',
color: '#374151',
letterSpacing: '0.025em'
}}
/>Developer/Technical Style:
<ZenithEditor
contentStyle={{
fontFamily: '"JetBrains Mono", "Fira Code", Consolas, monospace',
fontSize: '14px',
lineHeight: '1.5',
color: '#1f2937',
backgroundColor: '#f8fafc'
}}
/>Creative/Artistic Style:
<ZenithEditor
contentStyle={{
fontFamily: '"Playfair Display", Georgia, serif',
fontSize: '17px',
lineHeight: '1.8',
color: '#7c3aed',
textAlign: 'center'
}}
/>The contentStyle prop accepts any valid CSS properties and applies them to the editor content area, giving you complete control over the visual appearance of your text.
Text Color Selection
Zenith Editor includes a built-in color picker that allows users to change text colors easily. The color picker features:
- Predefined Colors: A palette of commonly used colors
- Custom Color Input: HTML5 color picker for any custom color
- Remove Color: Option to remove color formatting and revert to default
- Intuitive UI: Clean, accessible interface with proper focus management
Using the Color Picker
The color picker is automatically available in the toolbar. Users can:
- Select text they want to color
- Click the color picker button (🎨) in the toolbar
- Choose from predefined colors or use the custom color picker
- Apply the color or remove existing color formatting
Customizing Available Colors
You can customize the predefined colors by creating a custom toolbar:
import { ColorPicker } from 'zenith-editor';
const customColors = [
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FECA57',
'#FF9FF3', '#54A0FF', '#5F27CD', '#FF3838', '#00D2D3'
];
function CustomToolbarWithColors({ editor, onImageUpload, loadedFonts }) {
return (
<div className="toolbar">
{/* Other toolbar buttons */}
<ColorPicker
editor={editor}
colors={customColors}
showRemoveColor={true}
onColorSelect={(color) => {
console.log('Color selected:', color);
}}
/>
</div>
);
}Programmatic Color Control
You can also control text colors programmatically:
// Set text color
editor.chain().focus().setColor('#FF6B6B').run();
// Remove text color
editor.chain().focus().unsetColor().run();
// Check if text has color
const hasColor = editor.isActive('textStyle', { color: '#FF6B6B' });Custom Font Loading
Zenith Editor supports loading custom fonts using the modern FontFace Web API. This allows you to use any web font, including Google Fonts, Adobe Fonts, or your own hosted font files.
Basic Font Loading
import { ZenithEditor } from 'zenith-editor';
function EditorWithCustomFont() {
return (
<ZenithEditor
initialContent="<p>This text uses a custom font!</p>"
customFonts={[
{
fontFamily: 'Roboto',
src: 'https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.woff2',
format: 'woff2',
fontDisplay: 'swap'
}
]}
contentStyle={{
fontFamily: 'Roboto, sans-serif',
fontSize: '16px'
}}
/>
);
}Multiple Font Weights and Styles
<ZenithEditor
customFonts={[
{
fontFamily: 'Inter',
src: 'https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiJ-Ek-_EeA.woff2',
fontWeight: '400',
fontStyle: 'normal'
},
{
fontFamily: 'Inter',
src: 'https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuFuYAZ9hiJ-Ek-_EeA.woff2',
fontWeight: '700',
fontStyle: 'normal'
},
{
fontFamily: 'Inter',
src: 'https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuI6fAZ9hiJ-Ek-_EeA.woff2',
fontWeight: '400',
fontStyle: 'italic'
}
]}
contentStyle={{ fontFamily: 'Inter, sans-serif' }}
/>Local Font Files
<ZenithEditor
customFonts={[
{
fontFamily: 'MyCustomFont',
src: '/fonts/my-custom-font.woff2',
format: 'woff2'
},
{
fontFamily: 'HindiFont',
src: '/fonts/hindi_font.ttf',
format: 'truetype',
unicodeRange: 'U+0900-097F' // Devanagari script
}
]}
contentStyle={{
fontFamily: 'MyCustomFont, sans-serif'
}}
/>Font Loading Configuration
<ZenithEditor
customFonts={[
{
fontFamily: 'CustomFont',
src: '/fonts/custom.woff2',
fontDisplay: 'swap', // or 'block', 'fallback', 'optional'
fontWeight: 'normal',
fontStyle: 'normal'
}
]}
fontLoadOptions={{
autoLoad: true, // Load fonts automatically (default: true)
timeout: 10000, // Timeout in milliseconds (default: 5000)
testString: 'ABC' // Test string for loading detection
}}
onFontsLoaded={(fonts) => {
console.log('Successfully loaded fonts:', fonts);
}}
onFontLoadError={(error) => {
console.error('Font loading failed:', error);
}}
/>Advanced Font Loading with Hooks
For more control over font loading, use the font loading hooks:
import { useFontLoader, ZenithEditor } from 'zenith-editor';
function AdvancedFontExample() {
const {
state,
loadFont,
isFontLoaded,
getLoadedFonts
} = useFontLoader([
{
fontFamily: 'Poppins',
src: 'https://fonts.gstatic.com/s/poppins/v20/pxiEyp8kv8JHgFVrFJDUc1NECPY.woff2'
}
]);
const handleLoadAdditionalFont = async () => {
await loadFont({
fontFamily: 'Montserrat',
src: 'https://fonts.gstatic.com/s/montserrat/v25/JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2'
});
};
return (
<div>
<div className="mb-4">
<p>Loading: {state.isLoading ? 'Yes' : 'No'}</p>
<p>Loaded fonts: {getLoadedFonts().join(', ')}</p>
<button onClick={handleLoadAdditionalFont}>
Load Additional Font
</button>
</div>
<ZenithEditor
contentStyle={{
fontFamily: isFontLoaded('Poppins')
? 'Poppins, sans-serif'
: 'sans-serif'
}}
/>
</div>
);
}Custom Styling
The editor uses Tailwind CSS classes that can be customized:
/* Custom styles for your application */
.zenith-editor-container {
@apply border-2 border-blue-500 rounded-xl;
}
.zenith-toolbar {
@apply bg-blue-50 border-blue-200;
}
.zenith-editor-content .zenith-paragraph {
@apply text-lg leading-relaxed;
}
.zenith-editor-content .zenith-heading {
@apply text-blue-900;
}Custom Toolbar
import { Editor } from '@tiptap/react';
import { ColorPicker, FontSelector } from 'zenith-editor';
interface CustomToolbarProps {
editor: Editor;
onImageUpload?: (file: File) => Promise<string>;
loadedFonts?: string[];
}
function CustomToolbar({ editor, loadedFonts }: CustomToolbarProps) {
return (
<div className="flex gap-2 p-2 border-b">
<button
onClick={() => editor.chain().focus().toggleBold().run()}
className={`px-3 py-1 rounded ${
editor.isActive('bold') ? 'bg-blue-500 text-white' : 'bg-gray-200'
}`}
>
Bold
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
className={`px-3 py-1 rounded ${
editor.isActive('italic') ? 'bg-blue-500 text-white' : 'bg-gray-200'
}`}
>
Italic
</button>
<FontSelector
editor={editor}
loadedFonts={loadedFonts}
onFontSelect={(font) => console.log('Font selected:', font)}
/>
<ColorPicker
editor={editor}
onColorSelect={(color) => console.log('Color selected:', color)}
/>
</div>
);
}
// Use custom toolbar
<ZenithEditor
toolbar={CustomToolbar}
// ... other props
/>;Adding Custom Extensions
import { ZenithEditor } from 'zenith-editor';
import { TextAlign } from '@tiptap/extension-text-align';
import { Color } from '@tiptap/extension-color';
function EditorWithCustomExtensions() {
const customExtensions = [
TextAlign.configure({
types: ['heading', 'paragraph'],
}),
Color.configure({
types: ['textStyle'],
}),
];
return (
<ZenithEditor
extensions={customExtensions}
// ... other props
/>
);
}🔤 Font Utilities
Zenith Editor provides utility functions for working with custom fonts:
FontLoader Class
import { FontLoader } from 'zenith-editor';
const fontLoader = FontLoader.getInstance();
// Check if FontFace API is supported
if (FontLoader.isSupported()) {
// Load a single font
const result = await fontLoader.loadFont({
fontFamily: 'MyFont',
src: '/fonts/myfont.woff2'
});
// Load multiple fonts
const results = await fontLoader.loadFonts([
{ fontFamily: 'Font1', src: '/fonts/font1.woff2' },
{ fontFamily: 'Font2', src: '/fonts/font2.woff2' }
]);
// Check if font is loaded
if (fontLoader.isFontLoaded('MyFont')) {
console.log('Font is ready to use');
}
// Get all loaded fonts
const loadedFonts = fontLoader.getLoadedFonts();
// Remove a font
fontLoader.removeFont('MyFont');
// Clear all fonts
fontLoader.clearAllFonts();
}Standalone Font Loading
import { loadCustomFont, loadCustomFonts } from 'zenith-editor';
// Load a single font
const result = await loadCustomFont({
fontFamily: 'CustomFont',
src: '/fonts/custom.woff2',
fontDisplay: 'swap'
});
if (result.status === 'loaded') {
console.log('Font loaded successfully');
} else {
console.error('Font loading failed:', result.error);
}
// Load multiple fonts
const results = await loadCustomFonts([
{ fontFamily: 'Font1', src: '/fonts/font1.woff2' },
{ fontFamily: 'Font2', src: '/fonts/font2.woff2' }
]);
results.forEach(result => {
console.log(`${result.fontFamily}: ${result.status}`);
});CSS Generation
import { FontLoader } from 'zenith-editor';
// Generate CSS @font-face rule
const cssRule = FontLoader.createCSSFontFace({
fontFamily: 'MyFont',
src: '/fonts/myfont.woff2',
fontWeight: '400',
fontStyle: 'normal'
});
console.log(cssRule);
// Output:
// @font-face {
// font-family: 'MyFont';
// src: url('/fonts/myfont.woff2') format('woff2');
// font-weight: 400;
// font-style: normal;
// font-stretch: normal;
// font-display: swap;
// }🔧 Troubleshooting
Initial Content Not Visible
If you're experiencing issues where the initial content is not visible when the editor loads:
Ensure Content Format: Make sure your initial content is properly formatted HTML:
// ✅ Good <ZenithEditor initialContent="<p>Hello world!</p>" /> // ❌ Avoid <ZenithEditor initialContent="Hello world!" />SSR Issues: For Next.js, always use dynamic imports:
const ZenithEditor = dynamic( () => import('zenith-editor').then((mod) => mod.ZenithEditor), { ssr: false } );Check Content State: Verify your content state is properly initialized:
const [content, setContent] = useState('<p>Initial content</p>'); // Make sure content is not empty or undefinedEditor Mounting: If using refs, ensure the editor is fully mounted before accessing:
useEffect(() => { if (editorRef.current) { // Editor is ready console.log(editorRef.current.getHTML()); } }, []); // Only run once after mount
Font Loading Issues
If custom fonts are not loading:
- Check Font URLs: Ensure font URLs are accessible and CORS is properly configured
- Font Format Support: Use modern formats (woff2, woff) for better browser support
- Loading Timeout: Increase the timeout if you have slow network conditions:
<ZenithEditor fontLoadOptions={{ timeout: 10000 }} onFontLoadError={(error) => console.error('Font failed:', error)} />
Performance Optimization
For better performance with large documents:
- Debounce Updates: Debounce the
onUpdatecallback to avoid excessive re-renders - Limit Content Length: Consider pagination or lazy loading for very large documents
- Optimize Images: Compress images before upload and use appropriate formats
🧪 Testing
The package includes comprehensive tests. To run them:
# In the package directory
pnpm test
# Run with coverage
pnpm test --coverage
# Run in watch mode
pnpm test --watchExample test for your components:
import { render, screen } from '@testing-library/react';
import { ZenithEditor } from 'zenith-editor';
test('renders editor with placeholder', () => {
render(<ZenithEditor placeholder="Type something..." />);
expect(screen.getByText('Type something...')).toBeInTheDocument();
});
test('renders editor with initial content', () => {
render(
<ZenithEditor
initialContent="<p>Hello world!</p>"
data-testid="zenith-editor"
/>
);
expect(screen.getByText('Hello world!')).toBeInTheDocument();
});🏗️ Building from Source
# Clone the repository
git clone https://github.com/zenith-editor/zenith-editor.git
cd zenith-editor
# Install dependencies
pnpm install
# Build the package
pnpm build:package
# Run the demo
pnpm demo📄 License
MIT License. See LICENSE for details.
🤝 Contributing
Contributions are welcome! Please read our Contributing Guide for details.
🐛 Issues
Found a bug or have a feature request? Please open an issue.
🎯 Roadmap
- [ ] Table support
- [ ] Collaborative editing
- [ ] Math equation support
- [ ] Export to PDF/Word
- [ ] Plugin marketplace
- [ ] Mobile app integration
Made with ❤️ by the Yash Jha
