Based on Tiptap editor, a modern editor component library for Next.js and React for Wizlit
- Built with Next.js 15.3 and React 19
- TypeScript support
- Storybook documentation
- Fully customizable
- Modern and clean design
First, install the required dependencies:
npm install -D tailwindcss @tailwindcss/postcss postcss @tailwindcss/typography tailwindcss-animate
npm install @fontsource/inter cal-sans
npm install @wizlit/editorCreate a tailwind.config.js file in your project root:
const defaultTheme = require('tailwindcss/defaultTheme')
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ['class'],
content: [
'./src/**/*.{ts,tsx}',
],
safelist: ['ProseMirror'],
theme: {
extend: {
fontFamily: {
sans: ['Inter', ...defaultTheme.fontFamily.sans],
},
},
},
plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],
}Create a postcss.config.mjs file in your project root:
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;In your src/app/layout.tsx file, import the required fonts:
import 'cal-sans'
import '@fontsource/inter/100.css'
import '@fontsource/inter/200.css'
import '@fontsource/inter/300.css'
import '@fontsource/inter/400.css'
import '@fontsource/inter/500.css'
import '@fontsource/inter/600.css'
import '@fontsource/inter/700.css'Add the following to your globals.css file:
@tailwind base;
@tailwind components;
@tailwind utilities;Here's an example of how to use the editor in your component:
import { BlockEditor } from "@wizlit/editor";
import '@wizlit/editor/dist/styles/index.css'
import { useState } from 'react';
import { UploadListDialog, UploadListDialogProps } from '@wizlit/editor';
function MyComponent() {
const [content, setContent] = useState('<p>Initial content</p>');
const [uploadDialogProps, setUploadDialogProps] = useState<UploadListDialogProps>({} as UploadListDialogProps);
const handleContentChange = (newContent: string, isChanged: { isChanged: boolean, isStrictChanged: boolean }, stats: EditorStats) => {
console.log('Content changed:', newContent);
console.log('Change status:', isChanged);
console.log('Stats:', stats);
setContent(newContent);
};
return (
<div className="flex flex-col gap-4 p-4 h-screen relative">
<div className="flex justify-end">
<button
onClick={() => setContent('<p>New content after button click! π</p>')}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors"
>
Change Content
</button>
</div>
<BlockEditor
content={content}
onChange={handleContentChange}
className="pr-8 pl-20 py-16 lg:pl-8 lg:pr-8"
readOnly={false}
showDebug={true}
maxCharacters={1000}
altCharacterCounter={true}
maxEmbeddings={8}
image={{
maxSize: 10 * 1024 * 1024, // 10MB
disableBuiltInUploadDialog: true,
getUploadDialogProps: props => {
setUploadDialogProps(props)
},
onUploadImage: async (file: File) => {
console.log('Image upload is disabled in the demo... Please implement the API.uploadImage method in your project.')
return `https://example.image`
},
convertSrc: (src: string) => {
if (src.startsWith('https://picsum.photos/')) {
return 'https://encrypted-tbn0.gstatic.com'
}
return "EXTERNAL_IMAGE"
},
onClick: (url: string, event: MouseEvent) => {
console.log(url)
},
}}
link={{
disableDefaultAction: true,
onClick: (url: string) => {
if (url.startsWith('http')) {
console.log(url)
}
},
}}
/>
<UploadListDialog
{...uploadDialogProps}
className="fixed bottom-4 left-4"
/>
</div>
);
}The BlockEditor component accepts the following props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
content |
string |
No | '' |
Initial HTML content for the editor |
onChange |
(content: string, isChanged: { isChanged: boolean, isStrictChanged: boolean }, stats: EditorStats) => void |
No | - | Callback function called when editor content changes, includes change status and statistics |
className |
string |
No | '' |
Additional CSS classes to apply to the editor |
readOnly |
boolean |
No | false |
Whether the editor is in read-only mode |
showDebug |
boolean | { altCharacterCounter?: boolean } |
No | false |
Whether to show debug information including character count, word count, and editor state |
maxCharacters |
number |
No | - | Maximum number of characters allowed in the editor |
altCharacterCounter |
boolean |
No | false |
Use alternative character counting method |
maxEmbeddings |
number |
No | 3 |
Maximum number of images allowed in the editor |
image |
object |
No | - | Image-related configuration |
image.maxSize |
number |
No | 5 * 1024 * 1024 (5MB) |
Maximum file size in bytes for image uploads |
image.disableBuiltInUploadDialog |
boolean |
No | false |
Whether to disable the built-in upload dialog |
image.convertSrc |
(src: string) => string |
No | - | Function to convert image source URLs |
image.onUploadImage |
(file: File) => Promise<string> |
No | - | Function to handle image uploads. Must return a Promise that resolves to the image URL |
image.getUploadDialogProps |
(props: UploadListDialogProps) => void |
No | - | Callback to get upload dialog props for custom implementation |
image.onClick |
(url: string, event: MouseEvent) => void |
No | - | Function called when an image is clicked |
link |
object |
No | - | Link-related configuration |
link.disableDefaultAction |
boolean | ((url: string) => boolean) |
No | false |
When set to true or returns true from function, prevents default link behavior (opening in new tab). Use with onClick to handle link clicks |
link.onClick |
(url: string) => void |
No | - | Function called when a link is clicked |
key |
string |
No | - | Unique key for multiple editor instances |
The EditorStats interface returned in the onChange callback includes:
| Property | Type | Description |
|---|---|---|
characters |
number |
Number of characters in the editor |
words |
number |
Number of words in the editor |
percentage |
number |
Completion percentage if maxCharacters is set |
embedCount |
number |
Number of embedded elements |
embedList |
string[] |
List of embedded element URLs |
plainText |
string |
Plain text content of the editor |
The UploadListDialogProps interface includes:
| Property | Type | Description |
|---|---|---|
items |
UploadListItem[] |
List of upload items |
onRetry |
(id: string) => void |
Function to retry failed uploads |
className |
string |
Additional CSS classes for the dialog |
pillClassName |
string |
Additional CSS classes for the pill component |
dialogClassName |
string |
Additional CSS classes for the dialog container |
For detailed development instructions, please refer to DEVELOPMENT.md.
MIT