DEV Community

Cover image for Easy Responsive Tables with Markdown in React.
Jesse Onolememen
Jesse Onolememen

Posted on

Easy Responsive Tables with Markdown in React.

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 the td in each tr for the corresponding th (heading) based on the index of the row.
  • Use CSS to format the styling.

The end result should turn this:

Before responsive table

Into this:

Responsive table screenshot

Tools Used

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:

Alt Text

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 the tw 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:

Alt Text

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:

Completed

and on desktop:

Alt Text

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.

Alt Text

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)