Releasing react-headless-markdown-editor 🚀

Recently I had to construct a tiny private CMS in which one of the fields of the content types was a markdown open text. Initially, I thought I could just have it as a textarea since markdown is a pretty simple format to write but I wanted to go the extra mile and build something that would improve the writing experience.

Welcome react-headless-markdown-editor 🥳

Why headless? 🤔

My initial build was constructed and styled on top of Tailwind and followed my ruleset but if I was gonna share this package worldwide, I wanted it to have full styling flexibility and no pre-defined layout or configuration.

That's why I made it headless! You can import the package and layout it however it best suits your project as well as extend it with custom functionality with ease.

Comes with goodies 📦

The package has built-in controls you can easily use out of the box

  • headings (h1, h2 and h3)
  • links
  • quotes
  • image uploads

Nice! So how does it work?


You use your preferred package manager

npm i -S react-headless-markdown-editor
# or
yarn add react-headless-markdown-editor
# or
pnpm i react-headless-markdown-editor


import { Editor } from "react-headless-markdown-editor";

// ...

<Editor id="body" name="body" rows={25}>
  <Editor.H1Control />
  <Editor.H2Control />
  <Editor.H3Control />

// ...

Now this will obviously render our component completely void of styles and that's not good, so let's give it some style! ✨ 💅

I'm gonna use Tailwind to simplify the process.

const controlClx =
  "inline-block p-4 text-blue-600 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-blue-500";

  containerClassName="w-full bg-white rounded-lg border shadow-md dark:bg-gray-800 dark:border-gray-700"
  controlsClassName="flex flex-wrap text-sm font-medium text-center text-gray-500 bg-gray-50 rounded-t-lg border-b border-gray-200 dark:border-gray-700 dark:text-gray-400 dark:bg-gray-800"
  className="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-b-md border border-gray-300 focus:ring-yellow-500 focus:border-yellow-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-yellow-500 dark:focus:border-yellow-500"
  <Editor.H1Control className={controlClx} />
  <Editor.H2Control className={controlClx} />
  <Editor.H3Control className={controlClx} />

This is our demo's styling. Check it out to see how it looks.

Now it's looking a lot better and you can style it however you please.

Advanced Usage

There are a few advanced use cases that you might need some guidance with

Image upload

The package exports a control to help with this out of the box.

<Editor.ImageControl uploader={handleImageUpload} />

All you need to do is provide a function to the uploader prop that returns a string (hopefully the public url to the file).

Custom control

You can also create fully custom controls with whatever behaviour you can imagine.

The package exports the hook useEditorContext that will connect to the Editor and provide you with useful methods like

  • getSelection: Gets current text selection of the textarea
const { getSelection } = useEditorContext();
  • insertIntoCursor: Inserts whatever text you provide it with into the cursor of the textarea
const { insertIntoCursor } = useEditorContext();

// spongebobify("hello world") => "hElLo wOrLd"
function spongebobify(value: string): string {
  let chars = "";
  let uppercase = false;
  for (let i = 0; i <= value.length; i++) {
    const char = value.charAt(i);
    chars += uppercase ? char.toUpperCase() : char.toLowerCase();
    uppercase = !uppercase;
  return chars;