Another WYSIWYG Editor: Biting the Bullet and Implementing TipTap

Joe Taylor
12 min readAug 7, 2024

--

After implementing hasty versions of various WYSIWYG options across a few projects that always left me “underwhelmed” I decided to set aside 24 hours to implement a version I can start leveraging and build out further…

What we’re building!

Introduction: Finally Creating my WYSIWYG

Hey there! Today, we’re diving into the wild world of WYSIWYG editors. You know, those magical text boxes that let you format your words without feeling like you’re decoding the Matrix.

The repo is here… but if your curious in Claude 3.5 Sonnet explaininng the code with some jokes sprinkled in… read on!

So, picture this: you’re building a web app, and you think, “Hey, I’ll just slap in a textarea and call it a day!” Oh, sweet summer child. That’s like telling your kids you’re taking them to Disneyland and then driving them to the local playground. Sure, it’s technically a place to have fun, but it’s not exactly the magical kingdom they were expecting!

I’ve been there, folks. I’ve hastily overlooked the complexity of text editors more times than I’ve told my kids that the ice cream truck only plays music when it’s out of ice cream. (Pro tip: They eventually figure that one out.) But let me tell you, a proper WYSIWYG editor is more helpful than a GPS in a corn maze — and trust me, I’ve needed both!

After realizing that a simple editable div from shadcn/ui was about as useful as a screen door on a submarine, I decided it was time to create a comprehensive solution. Thats not entirely fair, the Textarea is helpful for what it does, but I had severely abused it’s relative utility.

shadcn/ui Textarea… sure it works in a pinch

Enter TipTap— the framework built from ProseMirror that’s about to make our WYSIWYG dreams come true faster!

TipTap to the Rescue

So, what’s on the menu for this WYSIWYG feast? We’re talking a smorgasbord of features that’ll make your text shine brighter than my bald spot in the summer sun:

  • Rich text formatting (because plain text is as boring as my jokes, according to my kids)
  • Code blocks with syntax highlighting (for when you want to show off your coding skills and make the rest of us feel inadequate)
  • Task lists (perfect for listing all the dad jokes your family begs you not to tell)
  • And more bells and whistles than a locomotive conductor’s birthday party!

Stick around, and I promise you’ll learn how to implement a WYSIWYG editor that’s so good, your users will think it’s magic — or at least as magical as me making vegetables “disappear” from my plate when the kids aren’t looking. Let’s dive in!

Setting Up the Foundation: TipTap and React Integration

Alright, folks! Now that we’ve decided to embark on this WYSIWYG adventure, it’s time to lay down the foundation. Much like how I prepare to tell my kids a new dad joke — with excitement, anticipation, and the knowledge that eye-rolls are imminent — we’re going to set up TipTap with React and Next.js.

First things first, let’s get our ingredients together. We’ll need TipTap, React, and Next.js — a combination more powerful than my “Hi Hungry, I’m Dad” joke at the dinner table. Here’s how we start cooking:

npm install @tiptap/react @tiptap/starter-kit @tiptap/extension-color @tiptap/extension-text-style

Now, let’s whip up the base of our WYSIWYG editor. It’s like making a sandwich — you need a good foundation, or everything falls apart faster than my kids’ attention span when I start talking about the good old days.

import { EditorProvider } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

const extensions = [
StarterKit,
// More extensions to come, like my endless supply of weather puns
]
const content = '<p>Hello, World! 👋</p>'
const TipTapEditor = () => {
return (
<EditorProvider extensions={extensions} content={content}>
{/* Our editor will go here, like where my dad jokes go to die */}
</EditorProvider>
)
}

This TipTapEditor component is the heart of our operation. It's like the remote control to the TV – everything important happens here, but sometimes it's frustratingly complex and you can't find the button you need.

Now, let’s talk about these extensions. The StarterKit is like the Swiss Army knife of TipTap – it's got all the basic tools you need to get started. But we're not stopping there! Oh no, we're going to add more extensions than I have excuses for being late to family gatherings.

Here’s a beefed-up version of our extensions array:

import { Color } from '@tiptap/extension-color'
import ListItem from '@tiptap/extension-list-item'
import TextStyle from '@tiptap/extension-text-style'

const extensions = [
StarterKit.configure({
bulletList: {
keepMarks: true,
keepAttributes: false, // TODO: Figure out why this is false, like why my jokes fall flat
},
orderedList: {
keepMarks: true,
keepAttributes: false,
},
}),
Color.configure({ types: [TextStyle.name, ListItem.name] }),
TextStyle,
// More extensions to come, like my expanding waistline
]

Each of these extensions adds a new feature to our editor, kind of like how each of my dad jokes adds a new level of embarrassment for my children. The Color extension lets us change text colors, ListItem helps with, well, list items, and TextStyle allows for inline text styling. It's a real party in there!

Now, you might be wondering, “But wait, where’s the actual editor? Where do I type my groundbreaking novel or my list of increasingly desperate dad jokes?” Fear not! In the next section, we’ll dive into creating a toolbar that would make even the most seasoned handyman jealous. We’ll be adding buttons, dropdowns, and more features than you can shake a stick at (which, coincidentally, is my favorite form of exercise).

Stay tuned, folks! The next part is where the magic really happens — or at least where we pretend it does, much like how I pretend to know what I’m doing when I attempt home repairs. Until then, keep your code clean and your puns ready!

Building the Toolbar: A Comprehensive Formatting Suite

Alright, buckle up, buttercup! We’re about to dive into creating a toolbar that’s more loaded than a baked potato at a steakhouse. This toolbar is going to have more buttons than my shirt after Thanksgiving dinner, and it’s going to be just as satisfying to use.

First things first, let’s create our MenuBar component. This bad boy is going to be the control center of our WYSIWYG editor, like the remote control that always seems to disappear into the couch cushions.

const MenuBar = () => {
const { editor } = useCurrentEditor()

if (!editor) {
return null
}
return (
<div className="menu-bar">
{/* We'll stuff this fuller than a turducken */}
</div>
)
}

Now, let’s start adding some buttons. We’ll use the Toggle component from our UI library, which is more versatile than my excuses for forgetting to take out the trash.

import { Toggle } from "../ui/toggle";
import { Bold, Italic, Strikethrough /* ... */ } from "lucide-react";

// Inside MenuBar component
<Toggle
onClick={() => editor.chain().focus().toggleBold().run()}
disabled={!editor.can().chain().focus().toggleBold().run()}
pressed={editor.isActive("bold")}
className="p-[.35rem] m-0 h-fit w-fit"
>
<Bold className="w-5 h-5 flex-none" />
</Toggle>

This Toggle component is like the Swiss Army knife of UI elements. It's got more features than my attempts at multitasking (which usually ends with burnt dinner and unfolded laundry).

We’ll add similar toggles for italic, strikethrough, and other basic formatting options. It’s like giving your text a makeover, but without the awkward “does this font make me look fat?” questions.

Now, let’s spice things up with some dropdowns. We’ll use the Select component for things like text color and highlight color:

import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";

// Inside MenuBar component
<Select
value={editor.getAttributes("textStyle").color || "default"}
onValueChange={(color) => editor.chain().focus().setColor(color).run()}
>
<SelectTrigger className="w-fit pr-4 bg-secondary">
<SelectValue placeholder="Color" />
</SelectTrigger>
<SelectContent>
<SelectItem value="#e11d48">Rose</SelectItem>
<SelectItem value="#7c3aed">Violet</SelectItem>
{/* More colors than my attempts at painting the living room */}
</SelectContent>
</Select>

This color picker is like a box of crayons for grown-ups. Now your users can make their text more colorful than my language when I step on a Lego.

But wait, there’s more! We’re going to add buttons for all sorts of formatting options:

<Toggle
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
pressed={editor.isActive("heading", { level: 1 })}
>
<Heading1 className="w-5 h-5 flex-none" />
</Toggle>

<Toggle
onClick={() => editor.chain().focus().toggleBulletList().run()}
pressed={editor.isActive("bulletList")}
>
<List className="w-5 h-5 flex-none" />
</Toggle>
<Toggle
onClick={() => editor.chain().focus().setTextAlign("left").run()}
pressed={editor.isActive({ textAlign: "left" })}
>
<AlignLeft className="w-5 h-5 flex-none" />
</Toggle>

We’ve got headings, lists, text alignment… this toolbar has more options than I have excuses for why I haven’t fixed that leaky faucet yet.

And let’s not forget about our code block feature. We’ll add a button that’s more exciting than finding an extra fry at the bottom of the bag:

<Toggle
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
pressed={editor.isActive("codeBlock")}
>
<CodeXml className="w-5 h-5 flex-none" />
</Toggle>

Now your users can add code blocks faster than I can come up with a dad joke about programming. (Why do programmers prefer dark mode? Because light attracts bugs!)

Finally, we’ll wrap all these buttons in some fancy Alert components to group them nicely:

<Alert className="flex flex-row p-1 m-0 h-fit w-fit gap-1">
{/* Stuff more buttons in here than a teenager's first suit */}
</Alert>

And there you have it! A toolbar that’s more feature-packed than a Swiss Army knife at a gadget convention. Your users will have more formatting options than I have grey hairs (and trust me, that’s a lot).

In the next section, we’ll dive into some of the more advanced features of our editor. It’ll be more exciting than watching paint dry… which, coincidentally, is what I told my kids we were doing last weekend for “fun”. Stay tuned!

Advanced Features: Code Blocks, Syntax Highlighting, and More

Alright, folks, strap in! We’re about to take this WYSIWYG editor from 0 to 60 faster than I can embarrass my kids in public (which, let me tell you, is pretty darn fast). We’re diving into the advanced features that’ll make your editor cooler than the other side of the pillow.

First up, let’s talk about code blocks with syntax highlighting. This feature is more impressive than my ability to fall asleep during any movie my family wants to watch.

To get started, we need to invite some new friends to our coding party:

npm install @tiptap/extension-code-block-lowlight lowlight

Now, let’s set up our code block extension:

import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
import { createLowlight, all } from 'lowlight'

const lowlight = createLowlight(all)
const extensions = [
// ... other extensions
CodeBlockLowlight.extend({
addNodeView() {
return ReactNodeViewRenderer(CodeBlockComponent)
},
}).configure({ lowlight }),
]

This setup is like adding a turbo booster to your family minivan. Suddenly, your boring old text editor can handle code with more style than I handle a dad joke punchline.

Now, let’s create our CodeBlockComponent. This bad boy is going to be more functional than my attempts at DIY home improvement:

const CodeBlockComponent = ({ node, updateAttributes, extension }: any) => {
const [copied, setCopied] = useState(false);

const selectLanguage = (language: string) => {
updateAttributes({ language });
};
const copyCode = () => {
navigator.clipboard.writeText(node.textContent);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<NodeViewWrapper className="relative">
<pre className="rounded-md">
<NodeViewContent as="code" />
</pre>
<div className="absolute top-2 right-2 flex items-center space-x-2">
<Select
value={node.attrs.language || "auto"}
onValueChange={selectLanguage}
>
<SelectTrigger className="w-fit pr-4 bg-secondary">
<SelectValue placeholder="Language" />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">Auto</SelectItem>
{extension.options.lowlight.listLanguages().map((lang: string) => (
<SelectItem key={lang} value={lang}>{lang}</SelectItem>
))}
</SelectContent>
</Select>
<Button variant="secondary" onClick={copyCode}>
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</NodeViewWrapper>
);
};

This component is like the Swiss Army knife of code blocks. It’s got language selection smoother than my attempts at dancing at weddings, and a copy button more useful than my “Pull my finger” joke at family gatherings.

But wait, there’s more! Let’s talk about task lists. These are perfect for when you want to make a to-do list of all the dad jokes you haven’t told yet:

import TaskList from '@tiptap/extension-task-list'
import TaskItem from '@tiptap/extension-task-item'

const extensions = [
// ... other extensions
TaskList,
TaskItem.configure({
nested: true,
}),
]

With this setup, your users can create task lists faster than I can come up with excuses for why I haven’t mowed the lawn.

Now, let’s add some pizzazz with text alignment. This feature is more versatile than my collection of Hawaiian shirts:

import TextAlign from '@tiptap/extension-text-align'

const extensions = [
// ... other extensions
TextAlign.configure({
types: ['heading', 'paragraph'],
}),

Your users can now align their text left, right, or center, giving them more control than I have over my dad bod.

Last but not least, let’s talk about link insertion. This feature is more clickable than those ads promising to reveal Hollywood’s best-kept secrets:

const setLink = useCallback(() => {
const url = window.prompt('URL')
if (url) {
editor.chain().focus().setLink({ href: url }).run()
} else {
editor.chain().focus().unsetLink().run()
}
}, [editor])

// In your toolbar
<Button onClick={setLink} disabled={!editor.can().setLink()}>
<Link2 className="w-5 h-5" />
</Button>

Now your users can insert links faster than I can insert myself into my kids’ conversations with their friends (much to their eternal embarrassment).

And there you have it, folks! We’ve added more features to this editor than I have grey hairs (and trust me, that’s a lot). Your WYSIWYG editor is now equipped with code blocks that would make any developer jealous, task lists to organize your life (or procrastinate more efficiently), text alignment to satisfy your inner designer, and link insertion to connect your content to the vast world of the internet.

In our next and final section, we’ll wrap things up and look towards the future. We’ll discuss potential improvements that could make this editor even more powerful than my dad-joke game (if that’s even possible). Stay tuned!

Conclusion: Reflections and Future Improvements

Well, folks, we’ve reached the end of our WYSIWYG adventure. It’s been a wild ride, hasn’t it? We’ve created an editor more feature-packed than my toolbox (and probably more useful, too). Let’s take a moment to reflect on our achievements, much like I reflect on my glory days of high school sports… except our editor actually has a future!

In just 24 hours, we’ve managed to build a text editor that would make even the most seasoned web developer say, “Wow, that’s more impressive than a dad’s ability to embarrass his kids in public!” Let’s recap our key achievements:

  1. We set up a solid foundation with TipTap and React, creating a base as sturdy as my dad jokes are cringeworthy.
  2. We built a toolbar with more buttons than a NASA control panel, giving users more formatting options than I have excuses for being late to family dinners.
  3. We implemented advanced features like code blocks with syntax highlighting, task lists, and text alignment, making our editor more versatile than my collection of “World’s Best Dad” mugs.

Our editor is now capable of handling everything from simple notes to complex documents with code snippets. It’s so feature-rich, it makes a Swiss Army knife look like a plastic spoon!

But as my kids always say when I try to be “hip and cool” — “Dad, you’re not done embarrassing us yet?” And they’re right (about the editor, not about me being embarrassing… I hope). There’s always room for improvement!

Here’s a sneak peek at some future enhancements we could add:

  1. AI Features: Imagine an AI assistant that could help with writing, much like how I “help” my kids with their homework. (Spoiler: They usually end up teaching me.)
  2. Image Support: Let users add images to their documents. It’ll be easier than adding vegetables to my kids’ diets!
  3. Table Support: Because sometimes you need to organize information in rows and columns, like my ever-growing list of dad jokes categorized by levels of eye-rolling they induce.
  4. Collaborative Editing: Allow multiple users to edit the same document simultaneously. It’ll be like when the whole family tries to decide on a restaurant, but hopefully with less arguing.
  5. Dark Mode: Because sometimes you need to write in the dark, like when you’re jotting down dad joke ideas at 3 AM.
  6. Mobile Optimization: Make the editor responsive for mobile devices. It should adapt faster than I do to new technology (which, admittedly, isn’t saying much).
  7. Undo/Redo History: A feature more useful than my ability to remember where I left my car keys.
  8. Export Options: Allow users to export their masterpieces in various formats. It’ll be like sharing my dad jokes, but people might actually want to save these.

Remember, implementing a WYSIWYG editor is a journey, not a destination. It’s like parenting — just when you think you’ve got it figured out, something new comes along to challenge you. (Usually in the form of a new social media platform I need to embarrass my kids on.)

So, I encourage you all to take this editor, experiment with it, and make it your own. Add features, remove ones you don’t need, or completely revamp it. The possibilities are as endless as my supply of dad jokes!

And speaking of experiments, why not try building your own WYSIWYG editor? It’s a great way to learn about text manipulation, React components, and how to create user interfaces that don’t make people want to pull their hair out (unlike my dance moves at family weddings).

In conclusion, remember that every great project starts with a single line of code, much like every great dad joke starts with a single groan. So get out there, start coding, and may your editors be bug-free and your jokes be groan-worthy!

Now, if you’ll excuse me, I have some urgent dad business to attend to. I heard someone say they’re hungry, and I can’t miss an opportunity like that!

--

--