Installation
Quick Take
Examples
- Find exact text in a HAST tree
- Customise every generated class name
- Lift every labelled column with a wildcard
- Renders markdown tables
- Find a numbered child element by tag name
Purpose
This is a rehype plugin to help display HTML tables on narrow screens.
As with all Unified plugins, it can process both HTML and Markdown, which contains HTML (see examples).

Unlike our competition, this plugin lets you customise what happens to each column, depending on its thead string label (wrapping tags, if any are stripped when calculating the label).
For example, I can “lift” only “Type” column cells.
API
Plugin accepts an Optional Options Object; it goes as the second argument into .use(), for example:
import { rehype } from "rehype";
import rehypeFormat from "rehype-format";
import rehypeResponsiveTables from "rehype-responsive-tables";
let res = rehype()
.data("settings", { fragment: true })
.use(rehypeResponsiveTables, {
tableClassName: "rrt-table",
newTrClassName: "rrt-new-tr",
hideTdClassName: "rrt-del-td",
gapTrClassName: "rrt-gap-tr",
newTrSpanTopClassName: "rrt-new-tr__span-top",
newTrSpanOtherClassName: "rrt-new-tr__span-other",
up: ["Type"],
})
.use(rehypeFormat)
.processSync(input)
.toString();
console.log(res);
The Optional Options Object has the following shape:
Here are all defaults in one place for copying:
tableClassName
Default value: rrt-table
tableClassName is the CSS class (a string) set to the <table> tag. It can contain multiple CSS classes separated by a space. Any existing classes are retained.
newTrClassName
Default value: rrt-new-tr
Let’s say you pass the following table to this plugin:

The plugin will generate extra cells:

CSS class newTrClassName is set onto these new rows (<tr>s):

Now, you can hide this row on the “desktop” layout:
@media (min-width: 451px) {
.rrt-new-tr {
display: none;
}
}
hideTdClassName
Default value: rrt-del-td
Let’s say you pass the following table to this plugin:

The plugin will generate extra cells:

CSS class hideTdClassName is set onto all cells in the first column so that you can hide them on mobile:

You can hide this row on the “mobile” layout:
@media (max-width: 450px) {
.rrt-del-td {
display: none;
}
}
gapTrClassName
Default value: rrt-gap-tr
Let’s say you pass the following table to this plugin:

The plugin will generate extra cells:

CSS class gapTrClassName is set onto this gap <tr>, which gets added under each original <tr>:

You have two options:
- if you want the gap to be visible on mobile, hide its left and right borders there:
/* on mobile, hide outer borders: */
.rrt-gap-tr td:nth-child(2) {
border-left-color: transparent;
border-right-color: transparent;
}
/* then, on desktop, hide the whole TR: */
@media (min-width: 451px) {
.rrt-gap-tr {
display: none;
}
}

- if you don’t want this gap, style it
display: noneeverywhere (on desktop and mobile).
newTrSpanTopClassName and newTrSpanOtherClassName
Default values: rrt-new-tr__span-top and rrt-new-tr__span-other
Let’s say you pass the following table to this plugin:

The plugin will generate extra cells:

But then, you tweak the config and “lift” up the “Type” column via opts.up, for example:
let res = rehype()
.data("settings", { fragment: true })
.use(rehypeResponsiveTables, {
up: "Type", // <---
})
.use(rehypeFormat)
.processSync(input)
.toString();
console.log(res);
“Type” column cell values placed under the label, inside new <tr>, with the help of <br>:

The first column’s label is wrapped with a span with a CSS class newTrSpanTopClassName, and then all labels that follow are wrapped with a span with a CSS class newTrSpanOtherClassName.

@media (max-width: 450px) {
.rrt-new-tr__span-top {
font-size: 1.2em;
display: inline-block;
text-align: center;
width: 100%;
margin-bottom: 0.5rem;
}
.rrt-new-tr__span-other {
font-weight: bold;
}
}
Sticky thead
It’s a good idea to make the table headers sticky. That’s CSS styles:
thead {
position: -webkit-sticky;
position: sticky;
top: 0;
}
I also set a background color to <th> because otherwise, it looks not good when other text overlaps:
th {
background-color: white;
}
up
The Optional Options Object’s up value is an array of zero or more strings — labels of each thead column you want to “lift” up.
If you want to lift all columns to get a result similar to our competition, put the wildcard *:
import { rehype } from "rehype";
import rehypeFormat from "rehype-format";
import rehypeResponsiveTables from "rehype-responsive-tables";
let res = rehype()
.data("settings", { fragment: true })
.use(rehypeResponsiveTables, {
up: ["*"],
})
.use(rehypeFormat)
.processSync(input)
.toString();
console.log(res);
Which results in something like this:

API — the default export
The plugin itself is the default export, so you name it whatever you like at the import site:
import rehypeResponsiveTables from "rehype-responsive-tables";
It’s a unified Plugin operating on a hast Root, so pass it to .use() rather than calling it yourself. Its single argument is the Optional Options Object documented above.
Everything else this package exports is a named export.
API — defaults
You can import defaults:
It's a plain object:
The main function calculates the options to be used by merging the options you passed with these defaults.
API — getNthChildTag()
The function getNthChildTag() is imported like this:
A hast tree helper the plugin uses internally, exported for anyone writing a plugin alongside this one. It returns the n-th child element of a given tag name, skipping text nodes:
| Input argument | Type | Obligatory | Description |
|---|---|---|---|
treeType: Plain object Obligatory: yes | |||
tree | Plain object | yes | A hast node whose children to look through. |
tagNameType: String Obligatory: yes | |||
tagName | String | yes | The tag name to count, for example "td". |
nthType: Natural number or zero Obligatory: yes | |||
nth | Natural number or zero | yes | Zero-based index among the matching children. |
It returns the node, or null when there’s no such child. Counting ignores whitespace text nodes, which is the whole point — tree.children[1] in a formatted document is usually a line break, not the second cell:
import { getNthChildTag } from "rehype-responsive-tables";
const tr = {
type: "element",
tagName: "tr",
children: [
{ type: "element", tagName: "td", children: [{ type: "text", value: "first" }] },
{ type: "text", value: "\n" },
{ type: "element", tagName: "td", children: [{ type: "text", value: "second" }] },
],
};
console.log(getNthChildTag(tr, "td", 1).children[0].value);
// => "second"
console.log(getNthChildTag(tr, "td", 5));
// => null
API — contains()
The function contains() is imported like this:
The other exported hast helper. It walks a tree looking for a text node whose trimmed value equals what you asked for:
| Input argument | Type | Obligatory | Description |
|---|---|---|---|
treeType: Plain object Obligatory: yes | |||
tree | Plain object | yes | A hast node to traverse. |
somethingType: String or array of strings Obligatory: yes | |||
something | String or array of strings | yes | The text to look for. |
Given an array, it returns which element matched — that’s how the plugin resolves a thead label against opts.up. It returns undefined when nothing matched, or when something is empty:
import { contains } from "rehype-responsive-tables";
console.log(contains(tr, "second"));
// => "second"
console.log(contains(tr, ["nope", "first"]));
// => "first"
API — types
This package is written in TypeScript and exports the type Opts, the Optional Options Object documented above:
import type { Opts } from "rehype-responsive-tables";
This package does not export a version constant.