Intro
While working on a JAMStack eCommerce Site built with Gatsby, I found myself relying on Markdown Tables quite often to represent different types of structured information.
The Problem
The site I was building was mobile-first meaning responsive design was a priority for us and HTML tables, by default, are infamously un-responsive.
Being quite a common issue, there are dozens of CSS frameworks & libraries that can help you solve this problem. However, they all depend on you being able to control the structure of your markup.
When working with data from a CMS like Contentful, or any other markdown based API, you loose control over the markup. You are no longer able to directly modify the DOM attributes or classes of the HTML.
Neither are you able to use custom components (like a react-bootstrap table) for that specific table in your markdown string.
The Solution
In order to solve this issue, we need to do some direct DOM manipulations using plain ol' vanilla javascript and some CSS classes.
An overview of what you need to do is:
- Create a ref object holding your root object (where your markdown will be rendered)
- Query all the tables in that element
- Query all the rows in each table
- Add a
data-label
attribute to thetd
in eachtr
for the correspondingth
(heading) based on the index of the row. - Use CSS to format the styling.
The end result should turn this:
Into this:
Tools Used
- React 16+
- markdown-to-jsx for rending markdown
- emotion + twin.macro for styling.
The Code
To begin, you want to create a basic Markdown
component you can use throughout your app. All this does is take a markdown string and render it out as HTML using markdown-to-jsx
Markdown.tsx
import React from "react";
import MarkdownToJsx from "markdown-to-jsx";
interface MarkdownProps {
className?: string;
children: React.ReactNode;
}
const Markdown = ({ children }: MarkdownProps) => {
return (
<div>
<MarkdownToJsx>{children}</MarkdownToJsx>
</div>
);
};
export { Markdown };
App.tsx
import * as React from "react";
import { Markdown } from "./markdown";
import "./styles.css";
export default function App() {
return (
<div className="App">
<Markdown>
# Hello World
</Markdown>
</div>
);
}
What we get on screen:
Pretty nice so far
Now, we're going to add some styles for our small screened devices.
The first thing we want to do is hide the table header as we want the header to be rendered inline with the rows.
Doing so with emotion + twin.macro looks like this:
import tw from 'twin.macro'
import { css } from '@emotion/core'
const mobileTableStyles = css`
@media screen and (max-width: 600px) {
table thead {
${tw`hidden`} // the same as display: none;
}
}
`
For those of you who have never used twin.macro, it's a css-in-js wrapper around tailwindcss based on
babel-plugin-macros
. All of the classes in thetw
macro are from the TailwindCSS library.
The next thing we want to do is change the display mode of our table rows to block
and add some spacing to them.
const mobileTableStyles = css`
// ...
table tr {
${tw`block mb-3 pb-1`}
}
`
For our data cells, we also want to change the display mode to block, move the content to the right-hand side of the screen and add some borders between them.
const mobileTableStyles = css`
// ...
td {
${tw`block text-right border-b border-gray-200 pb-1 mb-2`}
}
`
With what we have so far, your table should look something like this:
Now, the next thing we want to do is to show the heading of each row inline with the content of the row purely in CSS and our existing markup.
To do that, we need to use some pseudo classes and a cool CSS function called attr
.
const mobileTableStyles = css`
// ...
td {
${tw`block text-right border-b border-gray-200 pb-1 mb-2`}
&:before {
${tw`float-left uppercase font-bold text-xs`}
content: attr(data-label);
}
}
`
If you save your changes and refresh your browser, you'll notice it looks identical as before. That's because we haven't assigned the data-label
attribute on any of our table rows yet.
Plain Ol' Javascript
The best way to go about this is by using some plain old vanilla javascript.
We need to create a ref to the root element in our Markdown
component so we can query all the table rows in the document and assign the data-label
to the corresponding header for each row.
import React, { useEffect, useRef } from 'react';
import MarkdownToJsx from 'markdown-to-jsx'
import tw from 'twin.macro'
import { css } from '@emotion/core'
const Markdown = ({ children, className }: MarkdownProps) => {
const ref = useRef<HTMLDivElement>();
return (
<div ref={ref}>
<MarkdownToJsx
css={mobileTableStyles}
className={className}
>
{content || children}
</MarkdownToJsx>
</div>
);
}
Now, we want to use our ref
object to extract all the headings and rows from any table in our markdown object.
We can use the useEffect
hook for this.
useEffect(() => {
const current = ref.current;
if (!current) return;
current.querySelectorAll('table')
.forEach(makeTableResponsive)
}, [ref])
Our makeTableResponsive
table responsive function will just parse all the headings into an array of strings and set the data-label
attribute for each row based on its index.
const makeTableResponsive = (table: HTMLTableElement) => {
const headings = Array.from(
table.querySelectorAll('thead > tr > th'),
).map(th => th.textContent);
table.querySelectorAll('tbody > tr')
.forEach((row, rowIdx) => {
row.querySelectorAll('td').forEach((td, index) => {
td.setAttribute('data-label', headings[index]);
});
})
};
And with that we should get the following on mobile:
and on desktop:
Bonus
So now we've successfully accomplished our goal of making a responsive markdown table using React.
We could stop here or go a little step further by adding a nice row counter to improve the visual appearance of the table.
In our makeTableResponsive
we want to add the following code:
row.insertAdjacentHTML(
'beforebegin',
`<tr data-index="${rowIdx}">
<td>${rowIdx + 1}.</td>
</tr>`,
);
The code is pretty self explanatory, but all we want to do is add another row that will act as our visual counter.
We want to apply some more styles to control how this will appear on screen.
Firstly, we want it to be completely hidden on non-mobile screens:
const mobileTableStyles = css`
tr[data-index] {
${tw`hidden`}
}
@media screen and (max-width: 600px) {
tr[data-index] {
${tw`block`}
}
}
`
Now you can customise it however you want, I decided to add a little bit more spacing, align it to the left and make it bold:
const mobileTableStyles = css`
// ....
@media screen and (max-width: 600px) {
tr[data-index] {
${tw`block`}
td {
${tw`text-left border-0 font-bold`}
}
}
}
`
Now, once you save that, you should get our expected end result - a visually appealing, fully responsive markdown table with React and minimal effort.
Conclusion
I hope you found this post useful. This is my first-ever blog post online, so forgive me if it wasn't the best. I'll be sure to keep posting more and more interesting stuff I come across as I work on my personal projects.
To be sure to checkout my GitHub page while you're at it!
Top comments (0)