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:
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:
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.