npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tablemark

v4.1.0

Published

Generate markdown tables from a list of objects or JSON data.

Readme

tablemark · npm version license TypeScript GitHub Actions Workflow Status

Generate markdown tables from JSON data.

Render arrays of objects as markdown tables, with configurable fancy output.

  • Rename table headers and transform cell content
  • Align columns to the left, center, or right (all columns or per column)
  • Customize text casing for column headers (using change-case)
  • Auto-detect and handle ANSI styles and Unicode characters
  • Wrap or truncate long cell contents or strip line breaks

Installation

pnpm add tablemark

# or
npm install tablemark

# or
yarn add tablemark

# or
bun add tablemark

Usage

import { tablemark } from "tablemark";
tablemark([
	{ name: "Bob", age: 21, isCool: false },
	{ name: "Sarah", age: 22, isCool: true },
	{ name: "Lee", age: 23, isCool: true }
]);

// | Name  | Age   | Is cool |
// | :---- | :---- | :------ |
// | Bob   | 21    | false   |
// | Sarah | 22    | true    |
// | Lee   | 23    | true    |

... displays as:

| Name | Age | Is cool | | :---- | :---- | :------ | | Bob | 21 | false | | Sarah | 22 | true | | Lee | 23 | true |

API

tablemark (input: InputData, options?: TablemarkOptions): string

Arguments

  • InputData input: the data to table-ify as an array or iterable of objects

    • Note that nested objects are not supported. Use options.toCellText to customize how nested objects and other non-string values are output.
  • TablemarkOptions options:

    | key | type | default | description | | :----------------------: | :------------------------------------------: | :--------------: | -------------------------------------------------------------------------------------------------- | | align | "left" \| "center" \| "right" | "left" | Horizontal alignment to use for all columns. | | columns | Array<string \| ColumnDescriptor> | - | Array of column descriptors. | | countAnsiEscapeCodes | boolean | false | Whether to count ANSI escape codes when calculating string width. | | headerCase | "preserve" \| ... | "sentenceCase" | Casing to use for headers derived from input object keys (read more). | | lineBreakStrategy | "preserve" \| "strip" \| "truncate" | "preserve" | What to do when cell content contains line breaks. | | lineEnding | string | "\n" | String used at end-of-line. | | maxWidth | number | Infinity | Wrap cell text at this length. | | overflowStrategy | "wrap" \| "truncateStart" \| "truncateEnd" | "wrap" | How to handle overflowing text in cells. | | overflowHeaderStrategy | "wrap" \| "truncateStart" \| "truncateEnd" | "wrap" | How to handle overflowing text in header cells. | | padHeaderSeparator | boolean | true | Whether to pad gutters of the header separator (alignment) row. | | toCellText | ({ key, value }) => string | - | Provide a custom cell value transform function. | | toHeaderTitle | ({ key, title }) => string | - | Provide a custom header title transform function. | | unknownKeyStrategy | "ignore" \| "throw" | "ignore" | How to handle unknown keys found in objects. | | textHandlingStrategy | "auto" \| "advanced" | basic | "auto" | Control support for ANSI styles or Unicode characters (read more). | | wrapWithGutters | boolean | false | Add sides (\| <content> \|) to wrapped rows. |

Returns

string: the resulting markdown formatted table

If input is an empty array, an empty string is returned.

Throws

TypeError: when input is not iterable (e.g., an array) TypeError: when an unknown column alignment option is provided RangeError: when config.unknownKeyStrategy === "throw" and an unknown key in an object is encountered

[!NOTE] The keys of the first encountered object are used for the table's headers. By default, any other keys from successive objects will be ignored, excluding those columns from the table. You can customize this behavior to make this raise an error by using config.unknownKeyStrategy.

options.align

Set the horizontal alignment for all columns. Accepts "left", "center", or "right".

tablemark(
	[
		{ name: "Bob", age: 21 },
		{ name: "Sarah", age: 22 }
	],
	{ align: "center" }
);

// | Name  | Age |
// | :---: | :-: |
// | Bob   | 21  |
// | Sarah | 22  |

options.columns

Describe the columns of the table. Each column can be a simple string to rename the column or an object with properties to further customize the column's behavior. The following properties are available and will override behavior specified elsewhere in options:

  • name: Name of the column used as the title in the header row.
  • align: Horizontal alignment of the column content.
  • maxWidth: Maximum content width of this column.
  • overflowHeaderStrategy: How to handle overflowing text in header cells. Defaults to "wrap".
  • overflowStrategy: How to handle overflowing text in this column. Defaults to the root overflowStrategy.
  • textHandlingStrategy: How to handle text in this column. Defaults to the root textHandlingStrategy.
  • width: Fixed display width for the column, overriding both the root- and column-level maxWidth setting.
tablemark(
	[
		{ name: "Bob", age: 21, isCool: false },
		{ name: "Sarah", age: 22, isCool: true },
		{ name: "Lee", age: 23, isCool: true }
	],
	{
		columns: [
			"first name",
			{ name: "how old", align: "center" },
			"are they cool"
		]
	}
);

// | first name | how old | are they cool |
// | :--------- | :-----: | :------------ |
// | Bob        |   21    | false         |
// | Sarah      |   22    | true          |
// | Lee        |   23    | true          |

... displays as:

| first name | how old | are they cool | | :--------- | :-----: | :------------ | | Bob | 21 | false | | Sarah | 22 | true | | Lee | 23 | true |

options.countAnsiEscapeCodes

Control whether to count ANSI escape codes when calculating string width. The default is false, meaning ANSI codes are ignored. Setting this to true is useful when the output is not intended for a terminal, such as when generating a markdown table for an example in a README file.

const data = [
	{ text: "\u001B[31mRed\u001B[0m", note: "Normal text" },
	{ text: "\u001B[32mGreen\u001B[0m", note: "More text" }
];

tablemark(data, { countAnsiEscapeCodes: false });

// | Text  | Note        |
// | :---- | :---------- |
// | Red   | Normal text |
// | Green | More text   |

tablemark(data, { countAnsiEscapeCodes: true });

// | Text           | Note        |
// | :------------- | :---------- |
// | Red   | Normal text |
// | Green | More text   |

options.headerCase

Control the casing of headers derived from input object keys. The default is "sentenceCase". The options are:

  • "preserve": Keep the original case
  • "camelCase": Example: twoWords
  • "capitalCase": Example: Two Words
  • "constantCase": Example: TWO_WORDS
  • "dotCase": Example: two.words
  • "kebabCase": Example: two-words
  • "noCase": Example: two words
  • "pascalCase": Example: TwoWords
  • "pascalSnakeCase": Example: Two_Words
  • "pathCase": Example: two/words
  • "sentenceCase": Example: Two words
  • "snakeCase": Example: two_words
  • "trainCase": Example: Two-Words
tablemark([{ first_name: "Bob", last_name: "Smith" }], {
	headerCase: "constantCase"
});

// | FIRST_NAME | LAST_NAME |
// | :--------- | :-------- |
// | Bob        | Smith     |

options.lineBreakStrategy

Specify how to handle line breaks in cell content. The options are:

  • "preserve" (default): Keep line breaks
  • "strip": Replace line breaks with spaces
  • "truncate": Trim content at the first line break
tablemark([{ note: "Line 1\nLine 2" }], { lineBreakStrategy: "strip" });

// | Note          |
// | :------------ |
// | Line 1 Line 2 |

options.lineEnding

Set the string used at the end of each line. The default is \n (linefeed).

tablemark([{ name: "Bob" }], { lineEnding: "\r\n" });

options.maxWidth

Set options.maxWidth to wrap any content at that length onto a new adjacent line:

tablemark(
	[
		{ star: false, name: "Benjamin" },
		{ star: true, name: "Jet Li" }
	],
	{ maxWidth: 5 }
);

// | Star  | Name  |
// | :---- | :---- |
// | false | Benja |
//           min
// | true  | Jet   |
//           Li

[!NOTE] To output valid GitHub Flavored Markdown a cell must not contain newlines. Consider replacing those with <br /> (e.g., using options.toCellText).

options.overflowHeaderStrategy

Control how overflowing text in header cells is handled. The options are the same as overflowStrategy. The default is "wrap".

options.overflowStrategy

Control how overflowing text in cells is handled. The options are:

  • "wrap": Wrap text to a new line
  • "truncateStart": Trim overflowing content at the start and replace with
  • "truncateEnd" (default): Trim overflowing content at the end and replace with
tablemark([{ desc: "This is a long description" }], {
	maxWidth: 17,
	overflowStrategy: "truncateStart"
});

// | Desc              |
// | :---------------- |
// | …long description |

options.padHeaderSeparator

Exclude padding around the header's dividing lines (which some formatters prefer).

tablemark(
	[
		{ name: "Bob", age: 21, isCool: false },
		{ name: "Sarah", age: 22, isCool: true },
		{ name: "Lee", age: 23, isCool: true }
	],
	{
		columns: [{ align: "left" }, { align: "center" }, { align: "right" }]
	}
);

// | first name | how old | are they cool |
// |:-----------|:-------:|--------------:|
// | Bob        |   21    | false         |
// | Sarah      |   22    | true          |
// | Lee        |   23    | true          |

... displays as:

| first name | how old | are they cool | |:-----------|:-------:|--------------:| | Bob | 21 | false | | Sarah | 22 | true | | Lee | 23 | true |

options.textHandlingStrategy

By default, tablemark attempts to detect and handle text containing characters like emoji, halfwidth and fullwidth characters, and ANSI escape codes (like terminal colors and styles).

Other options, which are only recommended for very specific use cases, are:

  • "auto" (default): Automatically determine the best text handling strategy based on the input data. This is the recommended option for most use cases.
  • "basic": Faster, but lacks support for properly wrapping text containing certain emojis, halfwidth and fullwidth characters, and ANSI styles.
  • "advanced": Forces proper handling of emoji, halfwidth and fullwidth characters, and ANSI styles, but is slow for large datasets.

To illustrate the difference, notice how the first example below doesn't accurately align the | characters of the row containing the CJK and doesn't apply the ANSI styles correctly while the second example does.

tablemark(
	[
		{
			name: "\u001B[4mThis text, containing emoji 👨‍👩‍👧‍👦, ANSI styles and CJK 古, will not wrap or style properly\u001B[0m"
		}
	],
	{
		textHandlingStrategy: "basic",
		maxWidth: 17,
		wrapWithGutters: true
	}
);

// | Name              |
// | :---------------- |
// | This text,        |
// | containing emoji  |
// | 👨‍👩‍👧‍👦, ANSI          |
// | styles and CJK    |
// | 古, will not align |
// | or style          |
// | properly          |

tablemark(
	[
		{
			name: "\u001B[4mThis text, containing emoji 👨‍👩‍👧‍👦, ANSI styles and CJK 古, will wrap and style properly\u001B[0m"
		}
	],
	{
		// Note that this the default value and can be omitted
		textHandlingStrategy: "auto", // or "advanced"
		maxWidth: 17,
		wrapWithGutters: true
	}
);

// | Name              |
// | :---------------- |
// | This text,        |
// | containing emoji  |
// | 👨‍👩‍👧‍👦, ANSI styles   |
// | and CJK 古, will  |
// | align and style   |
// | properly          |

Screenshot of the terminal output of the above demonstrating the difference between basic and advanced text handling strategies

options.toCellText

Transform the contents of a body cell. This function is called with an object containing the key of the column and the value of the cell and should return a string to be used as the cell's content.

const toCellText = ({ key, value }) => {
	if (value === true) {
		return "✔";
	}

	if (!value) {
		if (key === "studying") {
			return "X";
		}

		return "";
	}

	return value;
};

tablemark(
	[
		{ name: "Bob", pet_owner: true, studying: false },
		{ name: "Sarah", pet_owner: false, studying: true },
		{ name: "Sarah", pet_owner: true, studying: true }
	],
	{
		toCellText,
		columns: [{ align: "left" }, { align: "center" }, { align: "center" }]
	}
);

// | Name  | Pet owner | Studying |
// | :---- | :-------: | :------: |
// | Bob   |     ✔︎     |    X     |
// | Sarah |           |    ✔     |
// | Lee   |     ✔     |    ✔     |

TypeScript object key inference

If you define your toCellText function directly within the tablemark function call as shown above, the type of the key property may be automatically constrained to the keys of your input (if known).

tablemark([{ name: "Bob", role: "Admin" }], {
	toCellText: ({ key, value }) => {
		// `key` is of type `name | role`
		return value;
	}
});

If you want to extract that definition elsewhere, use tablemark's ToCellText type, and combine it with the GetDataKeys helper type for strong typing.

import { tablemark, type ToCellText, type GetDataKeys } from "tablemark";

const data = [{ name: "Bob", role: "Admin" }] as const;

const toCellText: ToCellText<GetDataKeys<typeof data>> = ({ key, value }) => {
	// `key` is of type `name | role`
	return value;
};

tablemark(data, {
	toCellText
});

options.toHeaderTitle

Transform your header titles. This function is called with an object containing the key of the column and the title of the header cell and should return a string to be used as the header cell's content.

This transformation is applied after headerCase, so this function will be called with both the original object key as well as the cased title.

const toHeaderTitle = ({ key, title }) => {
	if (key === "name") {
		// Output the title with bold styling, e.g., for command line display
		// (you could use a package like `chalk` to do this)
		return `\u001B[1m${title}\u001B[0m`;
	}

	return title;
};

tablemark(
	[
		{ name: "Bob", pet_owner: true, studying: false },
		{ name: "Sarah", pet_owner: false, studying: true },
		{ name: "Kisha", pet_owner: true, studying: true }
	],
	{
		toHeaderTitle,
		columns: [{ align: "left" }, { align: "center" }, { align: "center" }]
	}
);

TypeScript object key inference

If you define your toHeaderTitle function directly within the tablemark function call as shown above, the type of the key property can be automatically constrained to the keys of your input (if known).

tablemark([{ name: "Bob", role: "Admin" }], {
	toHeaderTitle: ({ key, title }) => {
		// `key` is of type `name | role`
		return title;
	}
});

If you want to extract that definition elsewhere, use tablemark's ToHeaderTitle type, and combine it with the GetDataKeys helper type for strong typing.

import { tablemark, type ToHeaderTitle, type GetDataKeys } from "tablemark";

const data = [{ name: "Bob", role: "Admin" }] as const;

const toHeaderTitle: ToHeaderTitle<GetDataKeys<typeof data>> = ({
	key,
	title
}) => {
	// `key` is of type `name | role`
	return title;
};

tablemark(data, {
	toHeaderTitle
});

options.unknownKeyStrategy

Control how unknown keys in objects are handled:

  • "ignore" (default): ignore unknown keys
  • "throw": throw an error if an unknown key is found
tablemark(
	[
		{ a: 1 },
		{ a: 2, b: 3 } // 'b' is unknown
	],
	{ unknownKeyStrategy: "throw" }
);
// Throws `RangeError`

options.wrapWithGutters

Enable wrapWithGutters to add pipes on all lines:

tablemark(
	[
		{ star: false, name: "Benjamin" },
		{ star: true, name: "Jet Li" }
	],
	{ maxWidth: 5, wrapWithGutters: true }
);

// | Star  | Name  |
// | :---- | :---- |
// | false | Benja |
// |       | min   |
// | true  | Jet   |
// |       | Li    |

See also

And several tools that power tablemark:

  • ansi-regex – regular expression for matching ANSI escape codes
  • change-case – convert strings between camelCase, PascalCase, Capital Case, snake_case and more
  • string-width – get the visual width of a string - the number of columns required to display it
  • wrap-ansi – wordwrap a string with ANSI escape codes

Contributing

Search the issues if you come across any trouble, open a new one if it hasn't been posted, or, if you're able, open a pull request. Contributions of any kind are welcome in this project.

The following people have already contributed their time and effort:

Thank you!

License

MIT © Bo Lingen / haltcase