A fluent, type-safe HTML builder for TypeScript. Chain methods to produce well-formed HTML without string concatenation, template literals, or JSX. Ported from DelphiHTMLWriter.
import { createDocument } from 'tshtmlwriter';
const html = createDocument('html5')
.openHead()
.addTitle('Hello World')
.addMetaNamedContent('viewport', 'width=device-width, initial-scale=1')
.closeTag()
.openBody()
.addHeadingText(1, 'Welcome')
.addParagraphText('Built with tshtmlwriter.')
.openUnorderedList()
.addListItem('Fast')
.addListItem('Type-safe')
.addListItem('Zero dependencies')
.closeList()
.closeTag()
.closeDocument()
.toHTML();- Fluent API — every method returns
IHTMLWriterfor chaining - Strict TypeScript — literal union types for headings, input types, format types, and more
- Compile-time safety — context-aware errors prevent invalid nesting (e.g.,
<li>outside a list) - Full HTML5 support — semantic elements, media, forms, tables, definition lists, and more
- Zero dependencies — just TypeScript
- 347 tests across 21 test files
npm install tshtmlwriterbun add tshtmlwriterimport { createDocument } from 'tshtmlwriter';
const page = createDocument('html5')
.openHead()
.addTitle('My Page')
.closeTag()
.openBody()
.addHeadingText(1, 'Hello')
.closeTag()
.closeDocument()
.toHTML();
// <!DOCTYPE html><html><head><title>My Page</title></head><body><h1>Hello</h1></body></html>import { createFragment } from 'tshtmlwriter';
const fragment = createFragment()
.addBoldText('Important: ')
.addText('Read the docs.')
.toHTML();
// <b>Important: </b>Read the docs.import { create } from 'tshtmlwriter';
const nav = create('nav')
.addClass('main-nav')
.openUnorderedList()
.addListItem('Home')
.addListItem('About')
.closeList()
.closeTag()
.toHTML();
// <nav class="main-nav"><ul><li>Home</li><li>About</li></ul></nav>| Function | Description |
|---|---|
createDocument(docType?) |
Start an <html> document with optional DOCTYPE |
create(tagName) |
Start from any arbitrary tag |
createFragment() |
Build HTML without a wrapper element |
| Method | Description |
|---|---|
.addText(text) |
Add text content (closes open bracket first) |
.addRawText(text) |
Add raw text without closing the bracket |
.addTag(name, closeType?, canHaveAttrs?) |
Open an arbitrary child tag |
.closeTag() |
Close the current tag and return to parent |
.toHTML() |
Get the accumulated HTML string |
create('div')
.addId('hero')
.addClass('container')
.addStyle('color: red')
.addDataAttribute('page', 'home')
.addAriaAttribute('label', 'Hero section')
.addRole('banner')Boolean attributes: .addRequired(), .addDisabled(), .addAutofocus(), .addHidden(), .addReadonly(), .addMultiple(), .addNovalidate()
.addBoldText('bold') // <b>bold</b>
.addItalicText('italic') // <i>italic</i>
.addCodeText('const x = 1') // <code>const x = 1</code>
.addStrongText('important') // <strong>important</strong>
.addEmphasisText('note') // <em>note</em>All format types: bold, italic, underline, emphasis, strong, subscript, superscript, pre, cite, abbreviation, address, code, delete, definition, keyboard, quotation, sample, small, variable, insert, mark, bdi, ruby, rt, rp
.addHeadingText(1, 'Title') // <h1>Title</h1>
.addHeadingText(2, 'Subtitle') // <h2>Subtitle</h2>
// Levels 1-6, type-checked as HeadingLevel// Unordered
.openUnorderedList('disc')
.addListItem('First')
.addListItem('Second')
.closeList()
// Ordered
.openOrderedList('upper-roman')
.addListItem('Act I')
.addListItem('Act II')
.closeList()
// Definition list
.openDefinitionList()
.openDefinitionTerm().addText('HTML').closeTag()
.openDefinitionItem().addText('HyperText Markup Language').closeTag()
.closeTag().openTable({ border: 1, cellPadding: 4 })
.openCaption().addText('Results').closeTag()
.openTableRow()
.openTableHeader().addText('Name').closeTag()
.openTableHeader().addText('Score').closeTag()
.closeTag()
.openTableRow()
.addTableData('Alice')
.addTableData('95')
.closeTag()
.closeTable().openForm({ action: '/submit', method: 'post' })
.openFieldSet()
.addLegend('Login')
.openLabel('email').addText('Email').closeTag()
.openInput({ type: 'email', name: 'email' }).addRequired().closeTag()
.openLabel('pass').addText('Password').closeTag()
.openInput({ type: 'password', name: 'pass' }).closeTag()
.openButton().addText('Sign In').closeTag()
.closeTag()
.closeForm().openHeader().addHeadingText(1, 'Site Title').closeTag()
.openNav().addText('...').closeTag()
.openMain()
.openArticle().addParagraphText('Content here').closeTag()
.openAside().addText('Sidebar').closeTag()
.closeTag()
.openFooter().addText('© 2025').closeTag()Also: section, figure, figcaption, details, summary, dialog, template, picture
.openVideo({ src: 'video.mp4', width: 640, height: 480 }).closeTag()
.openAudio('audio.mp3').closeTag()
.addImage('photo.jpg')
.addIFrame('https://example.com', 800, 600)
.addEmbed('doc.pdf', 'application/pdf').addFigure('photo.jpg', 'A nice photo')
// <figure><img src="photo.jpg" /><figcaption>A nice photo</figcaption></figure>
.addDetailsSummary('Click to expand', 'Hidden content here')
// <details><summary>Click to expand</summary>Hidden content here</details>
.addAnchor('https://example.com', 'Visit')
// <a href="https://example.com">Visit</a>
.addComment('TODO: refactor this')
// <!-- TODO: refactor this -->tshtmlwriter enforces valid HTML structure at runtime with descriptive errors:
create('div').addListItem('oops');
// ❌ NotInListError: Must be inside a list tag
create('div').closeTag().closeTag();
// ❌ ClosingClosedTagError: Cannot close a tag that is already closed
createDocument().openBody().closeDocument();
// ❌ DocumentHasOpenTagsError: Cannot close document while tags are still openError checking can be configured via .setErrorLevels().
All types are exported for use in your own code:
import type {
IHTMLWriter,
HeadingLevel,
FormatType,
InputType,
DocType,
BulletShape,
NumberType,
TableOptions,
FormOptions,
} from 'tshtmlwriter';This is a TypeScript port of DelphiHTMLWriter, originally written in Delphi/Object Pascal. The API design preserves the fluent builder pattern of the original while taking full advantage of TypeScript's type system.