When you build a dashboard or a SaaS app, at some point you need a real text editor — not a textarea, not a fake markdown box.
You need:
- headings
- lists
- formatting
- clean HTML output
- full control over UI
That’s where Tiptap fits perfectly.
This blog shows how I’m using Tiptap Editor in a Next.js + React dashboard, with a custom toolbar, Tailwind styling, and HTML output ready to save in a database.
This is not a demo.
This is real dashboard code.
The Page Where Editor Is Used
This is a Next.js client component inside a dashboard layout.
The editor is used for building resume-like structured content.
"use client";
import DashboardLayout from "@/components/(user-dashboard)/layout/DashboardLayout";
import DiagonalDivider from "@/components/(user-dashboard)/components/DiagonalDivider";
import { useState } from "react";
import TipTapEditorMinimal from "@/components/(editors)/RichTextEditor/TipTapEditorMinimal";
import PrimaryButton from "@/components/Common/ui/PrimaryButton";
import { FaFile } from "react-icons/fa";
import { FileChartColumn } from "lucide-react";
export default function ResumesPage() {
const [resumeHtml, setResumeHtml] = useState("");
const [resumeJson, setResumeJson] = useState(null);
const handleResumeChange = (html: string, json: any) => {
setResumeHtml(html);
setResumeJson(json);
// Save to your Database via API route
};
const [post, setPost] = useState("");
const onChange = (content: string) => {
setPost(content);
};
const initialTemplate = `
<h2>Professional Summary</h2>
<p>Full-stack developer with expertise in MERN stack, Next.js, and Tailwind CSS.</p>
<h2>Experience</h2>
<ul>
<li>Software Engineer at XYZ Corp (2023-Present)</li>
</ul>
`;
return (
<>
<DashboardLayout>
<div className="mx-auto max-w-6xl">
<div className="rounded-xl border border-gray-300 backdrop-blur-sm dark:border-gray-800">
<div className="relative rounded-xl bg-white p-6 dark:bg-gray-900">
<div className="bg-primary absolute top-8 left-0 h-10 w-[4px] -translate-x-1/2 rounded-full" />
<div className="flex items-center gap-4">
<FileChartColumn className="text-primary h-10 w-10" />
<div>
<h1 className="text-3xl font-bold">Resumes</h1>
<p className="text-gray-600">
Track all your resume download activities
</p>
</div>
</div>
</div>
</div>
</div>
<div className="mx-auto mt-10 mb-10 max-w-6xl">
<div className="rounded-xl border border-gray-300 backdrop-blur-sm dark:border-gray-800">
<div className="relative rounded-xl bg-white p-6 dark:bg-gray-900">
<h1 className="mb-6 text-3xl font-bold">
Resume Builder
</h1>
<TipTapEditorMinimal content={post} onChange={onChange} />
<div className="mt-6 rounded-lg border p-4">
<p>{resumeHtml.length} characters</p>
<PrimaryButton
className="mt-4"
onClick={() => {
console.log("Save resume:", {
html: resumeHtml,
json: resumeJson,
});
}}
>
Save Resume
</PrimaryButton>
</div>
</div>
</div>
</div>
</DashboardLayout>
</>
);
}
Minimal Tiptap Editor Component
This component handles:
- editor setup
- extensions
- HTML output
- Tailwind styling
"use client";
import { EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import React from "react";
import TextAlign from "@tiptap/extension-text-align";
import Highlight from "@tiptap/extension-highlight";
import MenuBar from "./MenuBar";
interface RichTextEditorProps {
content: string;
onChange: (content: string) => void;
}
export default function TipTapEditorMinimal({
content,
onChange,
}: RichTextEditorProps) {
const editor = useEditor({
extensions: [
StarterKit.configure({
bulletList: {
HTMLAttributes: {
class: "list-disc ml-4 my-2",
},
},
orderedList: {
HTMLAttributes: {
class: "list-decimal ml-4 my-2",
},
},
}),
TextAlign.configure({
types: ["heading", "paragraph"],
}),
Highlight,
],
content: content,
editorProps: {
attributes: {
class:
"h-[230px] overflow-y-auto p-4 border rounded-b-md outline-none prose",
},
},
onUpdate: ({ editor }) => {
onChange(editor.getHTML());
},
immediatelyRender: false,
});
return (
<div>
{editor && <MenuBar editor={editor} />}
{editor && <EditorContent editor={editor} />}
</div>
);
}
Custom Toolbar (MenuBar)
This toolbar is fully custom — no default Tiptap UI.
It supports:
- headings
- bold / italic / strike
- alignment
- lists
- highlight
import React, { useEffect, useState } from "react";
import {
AlignCenter,
AlignLeft,
AlignRight,
Bold,
Heading1,
Heading2,
Heading3,
Highlighter,
Italic,
List,
ListOrdered,
Strikethrough,
} from "lucide-react";
import { Editor } from "@tiptap/react";
import { Toggle } from "../../Common/ui/Toggle";
export default function MenuBar({ editor }: { editor: Editor | null }) {
const [, forceUpdate] = useState({});
useEffect(() => {
if (!editor) return;
const updateHandler = () => forceUpdate({});
editor.on("selectionUpdate", updateHandler);
editor.on("transaction", updateHandler);
return () => {
editor.off("selectionUpdate", updateHandler);
editor.off("transaction", updateHandler);
};
}, [editor]);
if (!editor) return null;
return (
<div className="flex flex-wrap gap-1 border rounded-t-md p-2">
<Toggle onPressedChange={() => editor.chain().focus().toggleBold().run()}>
<Bold />
</Toggle>
<Toggle onPressedChange={() => editor.chain().focus().toggleItalic().run()}>
<Italic />
</Toggle>
<Toggle onPressedChange={() => editor.chain().focus().toggleStrike().run()}>
<Strikethrough />
</Toggle>
<Toggle onPressedChange={() => editor.chain().focus().toggleBulletList().run()}>
<List />
</Toggle>
<Toggle onPressedChange={() => editor.chain().focus().toggleOrderedList().run()}>
<ListOrdered />
</Toggle>
<Toggle onPressedChange={() => editor.chain().focus().toggleHighlight().run()}>
<Highlighter />
</Toggle>
</div>
);
}
Toggle Component (Radix + Tailwind)
"use client";
import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { cva } from "class-variance-authority";
import { cn } from "@/utils/editorUtils";
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md transition",
{
variants: {
size: {
default: "h-9 w-9",
},
},
defaultVariants: {
size: "default",
},
}
);
function Toggle({ className, ...props }: React.ComponentProps<typeof TogglePrimitive.Root>) {
return (
<TogglePrimitive.Root
className={cn(toggleVariants(), className)}
{...props}
/>
);
}
export { Toggle };
Utility Function
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: any[]) {
return twMerge(clsx(inputs));
}
Final Thoughts
Tiptap gives you freedom.
No forced UI.
No opinionated styles.
No hacks.
You control:
- markup
- toolbar
- output
- performance
If you’re building:
- a dashboard
- a SaaS editor
- a content builder
This setup works cleanly and reliably in real projects.
No magic.
Just solid React code.
Top comments (0)