Easy Responsive Tables with Markdown in React.
Tags: react, javascript, typescript, gatsby
Published: 2020-08-06 16:34:19

## 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](https://react-bootstrap.github.io/components/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](https://dev-to-uploads.s3.amazonaws.com/i/kb4zpnr3sny0g517ruex.png)

Into this: 

![Responsive table screenshot](https://dev-to-uploads.s3.amazonaws.com/i/ajpthl4fenk26n022ufl.png)

## Tools Used

- React 16+
- [markdown-to-jsx](https://github.com/probablyup/markdown-to-jsx) for rending markdown
- [emotion](https://emotion.sh/docs/introduction) + [twin.macro](https://github.com/ben-rogerson/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
```typescript
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
```typescript
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](https://dev-to-uploads.s3.amazonaws.com/i/efubzd39bz4tb9e4475a.png)
###### Pretty nice so far

<hr />

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:

```typescript
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.

```typescript
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.

```typescript
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](https://dev-to-uploads.s3.amazonaws.com/i/3m64hudst4cpw82j7ydi.png)

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

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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](https://dev-to-uploads.s3.amazonaws.com/i/lr5shg2uwm5wghh67m0j.png)

and on desktop:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/mjer0vwwb14d54xg4jal.png)

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

```typescript
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:

```typescript
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:


```typescript
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](https://dev-to-uploads.s3.amazonaws.com/i/mtayt77ygtnl5nqt04zg.gif)

## 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](https://github.com/jesster2k10) while you're at it!