How to Make HTML Table Labels in Graphviz

All Graphviz tools and guides

To draw a table inside a Graphviz node, set shape=plain and give the node an HTML-like label: label=< <TABLE> ... </TABLE> >. Use <TR> for rows and <TD> for cells, COLSPAN for a header that spans the columns, and CELLBORDER/CELLPADDING for the grid. Escape &, < and > in cell text as &amp;, &lt; and &gt;.

Build a table with a header and data rows

digraph Order {
  node [shape=plain];
  order [label=<
    <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="6">
      <TR><TD COLSPAN="2" BGCOLOR="lightblue"><B>Order</B></TD></TR>
      <TR><TD ALIGN="LEFT">id</TD><TD ALIGN="LEFT">UUID</TD></TR>
      <TR><TD ALIGN="LEFT">items</TD><TD ALIGN="LEFT">List&lt;LineItem&gt;</TD></TR>
      <TR><TD ALIGN="LEFT">total</TD><TD ALIGN="LEFT">price &amp; tax, in cents</TD></TR>
    </TABLE>
  >];
}

Run the header-and-rows example in the Graphviz editor

An Order table node with a light blue bold header spanning two columns, then id UUID, items with a generic list type, and total price and tax in cents rows with a one-pixel grid

  • The label is delimited by < and > instead of quotes. That outer pair is what makes it an HTML-like label.
  • The header <TD> has COLSPAN="2" so it covers both columns, BGCOLOR for the fill, and <B> for bold text.
  • ALIGN="LEFT" left-aligns text inside each data cell; the column is still as wide as its longest value.
  • List&lt;LineItem&gt; and price &amp; tax render as List<LineItem> and price & tax.

Choose borders, cell spacing and padding

The same three rows with three different <TABLE> settings:

digraph Borders {
  node [shape=plain];
  defaults [label=<
    <TABLE>
      <TR><TD><B>defaults</B></TD></TR>
      <TR><TD>id</TD></TR>
      <TR><TD>email</TD></TR>
    </TABLE>
  >];
  grid [label=<
    <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="6">
      <TR><TD><B>grid</B></TD></TR>
      <TR><TD>id</TD></TR>
      <TR><TD>email</TD></TR>
    </TABLE>
  >];
  header_rule [label=<
    <TABLE BORDER="1" CELLBORDER="0" CELLSPACING="0" CELLPADDING="6">
      <TR><TD BORDER="1" SIDES="B"><B>header rule</B></TD></TR>
      <TR><TD>id</TD></TR>
      <TR><TD>email</TD></TR>
    </TABLE>
  >];
}

Run the border comparison in the Graphviz editor

Three tables side by side: defaults draws a double outline with a gap around every cell, grid draws a single-line grid, and header rule draws an outer box with one line under the header

AttributeWhereWhat it controls
BORDER<TABLE> or <TD>Outline width of the table or one cell.
CELLBORDER<TABLE>Default border width for every cell. It falls back to the table's BORDER.
CELLSPACING<TABLE>Gap between cells, and between cells and the table border.
CELLPADDING<TABLE> or <TD>Space between a cell's border and its text.
SIDES<TD>Which sides of a cell's border to draw, using L, T, R and B.

A bare <TABLE> draws a table border and cell borders with a gap between them, which looks like double lines (defaults). BORDER="0" CELLBORDER="1" CELLSPACING="0" gives a single-line grid (grid), the usual choice for entity and record tables. For a lighter table, turn cell borders off and draw only a rule under the header with BORDER="1" SIDES="B" on that cell (header rule). CELLPADDING="6" gives the text room in the last two tables.

Escape text inside cells

An HTML-like label must be well-formed XML. A bare & fails:

digraph Order {
  node [shape=plain];
  order [label=<
    <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="6">
      <TR><TD><B>Order</B></TD></TR>
      <TR><TD>price & tax</TD></TR>
    </TABLE>
  >];
}

Open the broken example in the Graphviz editor

Graphviz 9.0.0 reports not well-formed (invalid token) in line 4 and does not draw the graph. Replace the cell text with price &amp; tax to fix it. A bare < is a syntax error too (the message says missing '>'?), and so is a bare >, because it closes the label early. Write them as &lt; and &gt;. A double quote does not need escaping inside an HTML-like label. A missing </TR> or other mismatched tag also fails, with mismatched tag.

Use shape=plain, not shape=box

digraph Shapes {
  plain_node [shape=plain, label=<
    <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="6">
      <TR><TD><B>shape=plain</B></TD></TR>
      <TR><TD>id</TD></TR>
    </TABLE>
  >];
  box_node [shape=box, label=<
    <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="6">
      <TR><TD><B>shape=box</B></TD></TR>
      <TR><TD>id</TD></TR>
    </TABLE>
  >];
}

Run the shape comparison in the Graphviz editor

Two identical one-column tables: the shape=plain node shows only the table, while the shape=box node draws a second rectangle around it

With shape=box, Graphviz draws the node's own rectangle around the table and adds margin, so the table gets a second outline. shape=plain has no outline, no margin and no minimum size, so the node is exactly the table. shape=none also hides the outline, but it keeps the default margin and minimum node size, so a small table takes extra space in the layout.

Next steps

These examples were rendered with this site's Graphviz 9.0.0 dot engine, which accepts upper- or lowercase tag names. To connect edges to a particular cell, add PORT="name" to its <TD> and follow connecting Graphviz edges to node sides and table ports. For a quick bold line or line break without a table, see multiline Graphviz labels. The database ERD example shows a complete schema built from table labels, and the Graphviz cheatsheet covers general DOT syntax.

Reference: Graphviz HTML-like labels.