react
markdown-to-jsx
css
html
components

The article focuses solely on the concept of Nested Lists. It does not dive deeply into the technical aspects of markdown-to-jsx; instead, it demonstrates how to create such lists and seamlessly integrate them with a markdown parsing library in an easy and scalable way.

Nested Lists with CSS and markdown-to-jsx

A long time ago, I tried implementing markdown parsing using the markdown-to-jsx library. It was quite tricky to implement nested lists with proper numeration in a straightforward manner, using pure CSS. By "nested lists," I mean the following syntax, written in markdown formatted and converted to plain HTML:

// Markdown format
1. First Item
    - Subitem A
    - Subitem B
        1. Sub-subitem I
        2. Sub-subitem II
    - Subitem C
2. Second Item
    - Subitem A
    - Subitem B
        1. Sub-subitem I
        2. Sub-subitem II
3. Third Item
    - Subitem A
        1. Sub-subitem I
        - Deepest Sub-sub-subitem a
        - Deepest Sub-sub-subitem b
        2. Sub-subitem II
    - Subitem B
// HTML result
<ol>
    <li>First Item
        <ul>
            <li>Subitem A</li>
            <li>Subitem B
                <ol>
                    <li>Sub-subitem I</li>
                    <li>Sub-subitem II</li>
                </ol>
            </li>
            <li>Subitem C</li>
        </ul>
    </li>
    <li>Second Item
        <ul>
            <li>Subitem A</li>
            <li>Subitem B
                <ol>
                    <li>Sub-subitem I</li>
                    <li>Sub-subitem II</li>
                </ol>
            </li>
        </ul>
    </li>
    <li>Third Item
        <ul>
            <li>Subitem A
                <ol>
                    <li>Sub-subitem I
                        <ul>
                            <li>Deepest Sub-sub-subitem a</li>
                            <li>Deepest Sub-sub-subitem b</li>
                        </ul>
                    </li>
                    <li>Sub-subitem II</li>
                </ol>
            </li>
            <li>Subitem B</li>
        </ul>
    </li>
</ol>

The result I expected was as follows:

Transformation of Nested Lists Showcase Nested Lists Rendered

How to implement it? Let's dive into this topic in this article.

Lists Nesting in HTML and CSS

First of all, we need some kind of wrapping element that will style everything inside. It may look as follows:

<div class="nested-lists">
// Any nested lists structure here!
</div>

Then, in CSS, we need to style certain list types - ordered and unordered - with specific markers ("-" for unordered and "numbers" for ordered lists):

.nested-lists ul > li:before {
  // The "-" will be added as a prefix for every unordered list element.
  content: '- ';
  // The element itself does not generate any box
  // but its children's boxes will appear as if they were direct
  // children of the element's parent.
  display: contents;
}

.nested-lists ol {
  // Each time a new "ol" appears, the counter resets.
  counter-reset: numbers;
}

.nested-lists ol > li {
  // When an "li" element occurs directly under "ol", the counter is incremented.
  counter-increment: numbers;
}

.nested-lists ol > li::before {
  // Assigning marker as an incremented number.
  content: counter(numbers) '. ';
}

.nested-lists li ol,
.nested-lists li ul {
  // Every nested list inside "li" will have an increased margin.
  margin-left: 16px;
}

Now let's put it together. Inside our wrapper, add the following content:

<div class="nested-lists">
  <ol>
    <li>
      First Item
      <ul>
        <li>Subitem A</li>
        <li>
          Subitem B
          <ol>
            <li>Sub-subitem I</li>
            <li>Sub-subitem II</li>
          </ol>
        </li>
        <li>Subitem C</li>
      </ul>
    </li>
    <li>
      Second Item
      <ul>
        <li>Subitem A</li>
        <li>
          Subitem B
          <ol>
            <li>Sub-subitem I</li>
            <li>Sub-subitem II</li>
          </ol>
        </li>
      </ul>
    </li>
    <li>
      Third Item
      <ul>
        <li>
          Subitem A
          <ol>
            <li>
              Sub-subitem I
              <ul>
                <li>Deepest Sub-sub-subitem a</li>
                <li>Deepest Sub-sub-subitem b</li>
              </ul>
            </li>
            <li>Sub-subitem II</li>
          </ol>
        </li>
        <li>Subitem B</li>
      </ul>
    </li>
  </ol>
</div>

The following result will be rendered:

Nested Lists Nested Lists Result

Thus, the mechanism is primarily based on CSS selectors. We're able to style lists inside other lists and change their margins. In addition, each occurrence of ol > li increments the counter, but the ol element itself resets the counter.

A really important factor is display: contents. This single line allows us to add margins relative to the current nested level. We don't need to "know that" and calculate; we simply specify all margins, such as margin-left: 16px, and it will magically work!

Adding Nested Lists to markdown-to-jsx

Let's dive into the React ecosystem and craft a special component for that. In a real project, you would map many more HTML nodes to components, but I'll focus only on lists to keep this article concise.

// File: MDNestedLists.tsx
import React from 'react';
import Md, { type MarkdownToJSX } from 'markdown-to-jsx';

// Configuration
const OPTIONS: MarkdownToJSX.Options = {
  overrides: {
    ul: ({ children }) => <ul>{children}</ul>,
    ol: ({ children }) => <ol>{children}</ol>,
    li: ({ children }) => <li>{children}</li>,
  },
};

interface MdNestedListsProps {
  // Every md content is a string.
  children: string;
}

const MdNestedLists = ({ children }: MdNestedListsProps) => {
  return (
    <div className="nested-lists">
      <Md options={OPTIONS}>{children}</Md>
    </div>
  );
};

export default MdNestedLists;

Now, we are able to use it like this:

<MdNestedLists>
  {`
1. First Item
  - Subitem A
  - Subitem B
      1. Sub-subitem I
      2. Sub-subitem II
  - Subitem C
`}
</MdNestedLists>;

And that's all! Usually, this md content will be loaded from an API or statically generated. Passing it directly as a string is rare, but it is sometimes used in real-time editors.

It's important to note that whitespacing matters when passing content to the markdown-to-jsx component. Ensure that your string content, whether loaded from an API or provided by a user, is correctly formatted and does not include unsupported or additional whitespace characters.

Summary

We've learned how to quickly create nested lists and style them without additional JavaScript code that might impact your Lighthouse metrics negatively. It's useful to know some CSS tricks like display: contents or counter-increment. Both of these relatively new, sophisticated features allowed us to achieve this impressive result.

Additionally, with markdown-to-jsx, we can take a string written in md format and render a visually appealing UI using a simple lookup object - [tag]: [component]. This allows for the creation of powerful presentations based on user-generated content in md format.