Skip to Content
  • Website
Codsen
  • Home
  • Open Source
  • Articles
  • About

prevOpen Source→rehype-responsive-tablesnext

rehype-responsive-tables2.2.0

Rehype plugin to stack the first column cells above their rows.

Downloads per monthChangelogMIT Licenselibera manifesto
  • the top
  • Installation
  • Quick Take
  • Examples
  • PURPOSE
  • API
  • TABLECLASSNA…
  • NEWTRCLASSNA…
  • HIDETDCLASSN…
  • GAPTRCLASSNA…
  • NEWTRSPANTOP…
  • STICKY THEAD
  • UP
  • API — THE DEFAULT EXPO…
  • API — DEFAULTS
  • API — GETNTHCHILDT…
  • API — CONTAINS()
  • API — TYPES
  • Changelog

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

Idea
Idea

Unlike our competitionopens in a new tab, 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:

Fig 1. Input
Fig 1. Input

The plugin will generate extra cells:

Fig 2. Output
Fig 2. Output

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

Fig 3. CSS class is applied onto these tr tags
Fig 3. CSS class is applied onto these tr tags

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:

Fig 4. Input
Fig 4. Input

The plugin will generate extra cells:

Fig 5. Output
Fig 5. Output

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

Fig 6. CSS class is applied onto this tds
Fig 6. CSS class is applied onto this tds

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:

Fig 7. Input
Fig 7. Input

The plugin will generate extra cells:

Fig 8. Output
Fig 8. Output

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

Fig 9. CSS class is applied to this tr
Fig 9. CSS class is applied to this tr

You have two options:

  1. 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;
  }
}
Fig 10. Gaps removed
Fig 10. Gaps removed
  1. if you don’t want this gap, style it display: none everywhere (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:

Fig 11. Input
Fig 11. Input

The plugin will generate extra cells:

Fig 12. Output
Fig 12. Output

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

Fig 13. Lifted cells
Fig 13. Lifted cells

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.

Fig 14. Spans are wrapping these labels
Fig 14. Spans are wrapping these labels
@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 competitionopens in a new tab, 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:

Fig 15. Wildcard "up", all columns lifted
Fig 15. Wildcard "up", all columns lifted

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 unifiedopens in a new tab 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 argumentTypeObligatoryDescription
tree
Type: Plain object
Obligatory: yes
treePlain objectyesA hast node whose children to look through.
tagName
Type: String
Obligatory: yes
tagNameStringyesThe tag name to count, for example "td".
nth
Type: Natural number or zero
Obligatory: yes
nthNatural number or zeroyesZero-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 argumentTypeObligatoryDescription
tree
Type: Plain object
Obligatory: yes
treePlain objectyesA hast node to traverse.
something
Type: String or array of strings
Obligatory: yes
somethingString or array of stringsyesThe 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.

Changelog

Open Changelog
↑ back to top
prev next

Copyright

All rights reserved © Roy Revelt 2026
All our open source packages are under MIT licenceopens in a new tab

Activities

🐛 See a bug? Raise an issueopens in a new tab
💘 Check out the Indiewebopens in a new tab and Libera manifestoopens in a new tab