The mathematical side of LaTeX’s nicematrix
package, with a Typst API: dotted leaders that stretch between cells, blocks,
rules, fills, exterior label rows and columns, braces and sub-matrices. It
also includes the paths and flood fills of the Typst package
pavemat. You write a normal Typst
matrix: , between cells, ; between rows, and dots.c / dots.v /
dots.down where the dots go.
#import "@preview/clanker-slop-nicematrix:0.1.0": *
$ A = nicemat(delim: "[",
a_11, dots.c, a_(1 n);
dots.v, dots.down, dots.v;
a_(m 1), dots.c, a_(m n)) $
Plain matrices come out exactly like mat (same size, same delimiters,
same cramped cells). Add one show rule at the top of the document so that
matrices in inline equations (and nested ones) get the smaller cells of an
inline mat; mat: true also switches every mat over:
#show: nicemat-setup // inline matrices sized like `mat`
#show: nicemat-setup.with(mat: true) // … and every `mat` gets leaders
Inline: $mat(1, dots.c, 1; dots.v, dots.down, dots.v; 1, dots.c, 1)$.
The tabular half of nicematrix ({NiceTabular}, notes, captions, X/V/S
columns, trees) is out of scope: use table for that.
Usage
Import the package from Typst Universe; everything you need is exported:
#import "@preview/clanker-slop-nicematrix:0.1.0": *
The functions keep short names that work in math: nicemat, nicearray, the
leaders cdots, vdots…, and the markers cell, hline, submatrix,
connect, pave… (see the reference below). Outside a nicemat, the
markers fall back to their plain look (dots, the body of a cell…).
The whole library is a single file,
nicematrix.typ,
without dependencies. You can also copy it into a project and write
#import "nicematrix.typ": *. Its internal helpers all start with _, so
import * only brings in the functions above.
A tour
The full documentation is documentation.pdf
(source: documentation.typ), in the
repository of the package. It has four parts, and every example in it shows
the code that produced the result:
- Getting started: installation, a guided tour, and the conventions.
- User guide: one chapter per feature. It includes the examples of the
pavemat manual, ported to
paveandflood. - Reference: every option and marker, with its nicematrix or pavemat equivalent.
- The nicematrix manual, example by example: the 88 examples of the
mathematical environments, each with the original LaTeX and its page in
the manual. The examples that are not ported are listed with the reason
(
{NiceTabular}, TikZ decorations, LaTeX configuration).
Paths and flood fills (from pavemat)
pave(path) draws a path along the grid lines. The path is a string of
directions: W (up), A (left), S (down), D (right). Lower-case letters
move without drawing, and (key: value) groups change the stroke until the
next ]. flood(from, fill:) fills the cells connected to from without
crossing a path:
$ nicemat(1, 2, 3; 4, 5, 6; 7, 8, 9,
pave("DDSSAAW", stroke: #(dash: "dashed")),
flood((1, 1), fill: #red.transparentize(80%))) $
Dotted leaders
In a nicemat, the symbols dots.h (or ...), dots.c, dots.v,
dots.down and dots.up are not glyphs any more. They are leaders drawn
between the nearest non-empty cells on both sides. Empty cells in between
are crossed, and consecutive dots of the same kind make a single line.
$ nicemat(delim: "[",
0, dots.c, dots.c, 0;
dots.v, , , dots.v;
0, dots.c, dots.c, 0) $
For options, use the functions ldots, cdots, vdots, ddots and
iddots (or dots: (..) for all the leaders; there, fill can be a function
(i, j) => color of the cell where a leader starts):
$ nicemat(1, 2, 3, 4, 5;
1, cdots(span: #3), 5; // like \Hdotsfor{3}
1, 2, 3, 4, 5) $
$ nicemat(delim: "[",
1, #h(1cm), 0; // cells with only #h(..) count as empty
, ddots(above: n "times"), ; // labels: above, below, middle
0, , 1) $
Blocks, rules, fills and corners
cell(body, rowspan:, colspan:) works like table.cell. The positions it
covers are skipped automatically, so you don’t write placeholders for them.
\ breaks a cell or a block into lines. Rules are drawn in the middle of the
gaps, like mat’s augment. They never cross blocks, leaders, empty corners
or the exterior rows. hvlines: #true gives all of them, borders: #false
leaves out the outer ones.
$ nicemat(delim: "[", hvlines: #true, margin: #0.2em,
cell(#text(1.5em)[$A$], rowspan: #3, colspan: #3), 0;
dots.v;
0;
0, dots.c, 0, 0) $
$ nicearray(hvlines: #true, borders: #false,
cell(x + y \ = z, rowspan: #2), a; b; 1, 2) $
$ nicearray(corners: #(top + right), hlines: #true, vlines: #true,
1; 1, 1; 1, 2, 1; 1, 3, 3, 1; 1, 4, 6, 4, 1) $
Rules can also be written as markers among the cells, like table.hline:
$ nicemat(1, 2, 3;
hline(); // between the rows
4, vline(), 5, 6; // after the cell it follows
hline(start: #1, stroke: #(dash: "dashed"));
7, 8, 9;
hline(); hline(); // two markers: a double rule
10, 11, 12) $
Backgrounds use fill, like table: a color, an array cycled over the
columns, or a function (i, j) => color. Use cell(.., fill:) for a block
and region(from, to, ..) for any rectangle of cells, or region(row: 1),
region(cols: (0, 2)) for whole rows and columns.
$ nicemat(margin: #0.25em,
fill: #((i, j) => if calc.even(i + j) { red.lighten(85%) } else { blue.lighten(85%) }),
1, 2, 3; 4, 5, 6; 7, 8, 9) $
$ nicemat(1, 2, 3; 4, 5, 6; 7, 8, 9,
region(row: 1, fill: #yellow.lighten(50%))) $
Exterior rows and columns, braces
first-row, last-row, first-col and last-col put the first/last row or
column outside the delimiters. Leaders, hbrace and vbrace work there too.
$ nicearray(first-row: #true, last-row: #true, first-col: #true, last-col: #true,
hlines: #true, vlines: #true,
, hbrace(p, span: #3), hbrace(q, span: #2);
vbrace(p, span: #3), 1, 1, 134, 1, 1, vbrace(p, span: #3);
1, 1, 134, 1, 1; // the braces cover the first column here
1, 1, 13456, 1, 1;
vbrace(q, span: #2), 1, 1, 134, 1, 1, vbrace(q, span: #2);
1, 1, 134, 1, 1;
, hbrace(p, span: #3), hbrace(q, span: #2)) $
Sub-matrices, braces over cells, free leaders and arrows
These decorations take two corners (row, col) (or whole rows and columns:
rows: (a, b), cols: (a, b)) and can be written anywhere among the
arguments:
$ nicearray(1, 1, 1, x; frac(1, 4), frac(1, 2), frac(1, 4), y; 1, 2, 3, z,
submatrix((0, 0), (2, 2)), // room is made for the delimiters
submatrix((0, 3), (2, 3))) $
$ nicemat(1, 1, 1; 1, a, b; 1, c, d,
submatrix((1, 1), (2, 2), delim: "[", sup: T)) $
$ nicemat(1, 2, 3, 4, 5, 6; 11, 12, 13, 14, 15, 16,
hbrace((0, 0), (1, 2), A), // like \OverBrace
hbrace((0, 0), (1, 5), "all", side: #bottom), // like \UnderBrace
vbrace((0, 0), (1, 5), 2 "rows")) $
$ nicemat(1, 2, 3; 4, 5, 6,
hbrace(n, cols: (0, 1)), // whole columns
vbrace(m, rows: (0, 1))) $
$ nicemat(I, 0, dots.c, 0; 0, I, dots.down, dots.v; dots.v, dots.down, I, 0; 0, dots.c, 0, I,
dotline((1, 1), (2, 2))) $ // like \line in \CodeAfter
$ nicearray(column-gap: #3em, row-gap: #2em,
A, B; C, D,
connect((0, 0), (0, 1), above: f), // an arrow
connect((0, 0), (1, 1), bend: -25)) $ // bent, like TikZ's `bend left`
Stacks of matrices
Matrices inside nicemat-group[..] share their column and delimiter
widths, so their columns line up. This is nicematrix’s {NiceMatrixBlock}
with auto-columns-width.
Your own drawings
background and foreground take content or a function receiving the
geometry of the matrix. This replaces the TikZ nodes, \CodeBefore and
\CodeAfter. Coordinates are relative to the top-left corner of the matrix,
ready for place:
$ nicemat(foreground: #(g => {
let (a, b) = (g.cells.at(0).at(0).ink, g.cells.at(2).at(2).ink)
place(line(start: (a.x + a.width, a.y + a.height), end: (b.x, b.y),
stroke: (paint: red, dash: "dashed")))
}),
1, 0, 0; 0, 1, 0; 0, 0, 1) $
Give matrices a name to draw between them (TikZ’s remember picture):
$ A = nicemat(name: "A", 1, 2; 3, 4) quad B = nicemat(name: "B", a, b; c, d) $
#nicemat-connect(("A", (0, 1)), ("B", (0, 0)), bend: 30deg)
nicemat-cell(name, (i, j)) gives the position of a cell on the page (in a
context). debug: #true shows the index and tile of every cell and the
numbers of the rules ("cells" or "rules" for one of them).
Conventions
- Math mode. Values that are not math need
#:#true,#red,#2pt,#(dash: "dashed"). Small integers (span: 3), coordinates ((0, 2)) and lists of indices (hlines: (1, -1)) work without it. - Aliases used in math need at least two letters.
#let m = nicemat.with(..)can’t be called as$m(..)$, because Typst readsmas a variable; name itmmormymat. - Cells are addressed
(row, col), from 0, as written, including the exterior rows and columns. Negative numbers count from the end.fill,alignandmap-cellsreceive(i, j)the same way. - Rules only exist inside the main block. So
hlines,vlines,augment,hline(y:)/vline(x:)and thestart/endof the rule markers count within it, exactly likemat’saugment. Line 0 is the top/left border,-1the last interior line.truemeans all interior lines, plus the borders when there are no delimiters. - Spans skip the covered positions, as in
table: don’t write empty placeholders under a spanningcell,cdots(span:)or brace. - Rectangles (
region,submatrix, braces): two corners, orrow:,rows: (a, b),col:,cols: (a, b); the other direction is then the whole matrix inside the delimiters. - Defaults for a document:
#let nicemat = nicemat.with(..)or#show math.mat: nicemat.with(..).set math.mat(delim:, align:, gap:, row-gap:, column-gap:)is honoured too.
Reference
nicemat(..rows, ..options) / nicearray (same, delim: none)
| option | default | meaning (nicematrix equivalent) |
|---|---|---|
delim |
from mat |
"(", "[", "{", "|", "‖", any extensible symbol ("↓"…), a pair ("[", ")") or none |
delim-fill |
auto |
color of the delimiters (delimiters/color) |
align |
from mat |
alignment, array (one per column) or (i, j) => alignment (l/c/r, columns-type) |
gap, row-gap, column-gap |
from mat |
a length, or an array with one value per gap, like grid gutters (\\[..], @{..}) |
augment |
none |
as in mat |
hlines, vlines |
none |
true, an index or an array of indices (hlines, vlines) |
hvlines |
false |
true: both hlines and vlines (hvlines) |
borders |
auto |
outer rules of hlines/vlines: true: auto (only without delimiters), true, false (hvlines-except-borders) |
stroke |
0.05em |
stroke of the rules (rules/color, rules/width) |
fill |
none |
color, array (cycled over columns) or (i, j) => color (\rowcolor, \rowcolors, \chessboardcolors, …) |
corners |
none |
true (the four corners) or alignments: top + left, …, top, right, … (corners) |
first-row, last-row, first-col, last-col |
false |
exterior rows and columns |
map-cells |
none |
(i, j, body) => body (code-for-first-row, \RowStyle) |
cell-space |
0pt |
minimum room between ink and row edges; length or (top:, bottom:) (cell-space-limits) |
margin |
auto |
room inside the delimiters; length or dictionary (margin, extra-margin) |
column-width |
auto |
a minimum length, one per column ((auto, 1cm), like w{c}{1cm}), or "equal" (columns-width) |
baseline |
horizon |
top, horizon, bottom, a row index, or (line: k) to put a rule on the axis (baseline) |
small |
auto |
auto: like mat (smaller in inline equations under nicemat-setup, and when nested); true: script cells, tighter gaps and dots (small); false: display cells |
dots |
(:) |
options of the leaders, see below (xdots/..) |
background, foreground |
none |
content or (geometry) => content |
name |
none |
name for nicemat-cell / nicemat-connect (name) |
debug |
false |
true, "cells" or "rules": indices and tiles, rule numbers (\ShowCellNames) |
Leaders: ldots, cdots, vdots, ddots, iddots
The same keys are accepted in nicemat(dots: (..)) for every leader, and by
dotline and connect.
| option | default | meaning |
|---|---|---|
symbols (in dots only) |
true |
turn dots symbols into leaders |
span (leader only) |
1 |
cover several columns (ldots, cdots) or rows (vdots), like \Hdotsfor / \Vdotsfor (even span: 1) |
above, below, middle (leader only) |
none |
labels (^, _, :) |
horizontal-labels |
false |
keep the labels horizontal |
fill |
text color | color of the dots, or (i, j) => color of the cell where the leader starts (none: text color) |
stroke |
auto |
auto: round dots; a stroke: a line (line-style) |
radius, spacing |
0.53pt, 0.45em |
dot size and distance (radius, inter); 70% in small cells |
shorten, shorten-start, shorten-end |
0.3em |
gap at the closed ends; 70% in small cells |
bend (dotline, connect) |
0 |
an angle or a number of degrees: a curve bent left (right when negative), like TikZ’s bend left |
nullify |
false |
leaders take no room (nullify) |
parallel (in dots only) |
true |
draw diagonals parallel to the first (parallelize-diags) |
first (leader only) |
false |
reference diagonal for the parallel ones (draw-first) |
marks, mark-size |
none, 4pt |
arrow tips: "->", "<-", "<->" |
label-fill |
page fill | background of middle labels |
Markers
| marker | options |
|---|---|
cell(body) |
rowspan, colspan, fill, stroke (true, a stroke, or one per side like rect), radius, outset (length or (x:, y:)), align, transparent (rules cross it), empty (\NotEmpty) |
diagbox(lower, upper) |
a cell slashed diagonally (\diagbox); also as the body of a spanning cell; stroke |
hline() / vline() |
start, end (end excluded), stroke, y / x |
hbrace(label, span:) / vbrace(label, span:) |
in exterior rows/columns: fill, shift |
hbrace(from, to, label) / vbrace(from, to, label) |
side (top/bottom, right/left), shorten, shift, fill; also hbrace(label, cols: (a, b)), vbrace(label, rows: (a, b)) |
submatrix(from, to) |
or row:/rows:/col:/cols:; delim, sup, sub, hlines, vlines (relative to the sub-matrix), stroke, slim, xshift, left-xshift, right-xshift, extra-height, fill, reserve, bound |
region(from, to) |
or row:/rows:/col:/cols:; fill, stroke (one per side allowed), radius, outset, fit ("cells"/"content"), above |
dotline(from, to) |
leader options, bend |
connect(from, to) |
an arrow: a dotline with a solid line and marks: "->" |
pave(path) |
a path along the grid lines (pavemat): from (grid point (i, j) counted within the matrix, or a corner top + right), stroke, chars (other letters than WASD) |
flood(from) |
fills the cells connected to from without crossing a path: fill |
Functions
| function | use |
|---|---|
#show: nicemat-setup |
inline and nested matrices sized like mat; .with(mat: true) also draws every mat with nicemat |
nicemat-group[..] |
the matrices inside share their column widths ({NiceMatrixBlock}) |
nicemat-cell(name, (i, j)) |
in a context: none or the cell of a named matrix on the page (page, x, y, width, height, ink, baseline) |
nicemat-connect((name, (i, j)), (name, (i, j))) |
a line between cells of named matrices on one page; options of connect |
Geometry (for background / foreground)
width, height, baseline, axis, main (rect of the main block),
rows (top, baseline, bottom, tile-top, tile-bottom), cols
(left, right, tile-left, tile-right), cells.at(i).at(j) (tile
x, y, width, height, plus ink, baseline, empty, kind,
origin), and leaders (dir, from, to, open, start, end).
Differences with nicematrix and pavemat
- Cells are set in text style, as in LaTeX and in
matinside a display equation. Typst gives no way to detect an inline equation without a show rule: add#show: nicemat-setupto get script-size cells in inline equations and nested matrices, likemat. - Nothing needs a second compilation. Everything is measured in one layout
pass, except
nicemat-group, which converges in two iterations automatically. - By default, sub-matrices and over/under braces make room for themselves
instead of overlapping the surroundings (
reserve: #falserestores the nicematrix behaviour for sub-matrices). - Braces use the math font (
overbrace, stretched}) rather than TikZ decorations. - The paths of pavemat use the
strokeof the matrix: a stroke given to one path is added to it (in pavemat it replaces it), and the default is a solid rule rather than a dashed line. As in pavemat, a matrix with paths or flood fills gets a margin of half a gap around its cells. - The
fillsof pavemat are split between thefilloption (whole matrix),region(a cell or a rectangle) andflood(a region bounded by paths).
Development
The source is at https://github.com/coder56765/clanker-slop-nicematrix.
The library is the single file nicematrix.typ, in sections ordered so that
every name is defined before it is used (utilities, measuring, markers,
parsing, layout, drawing, leaders, rules, decorations, paths, groups,
nicemat, setup, named matrices). tests/run.sh compiles every test. The
tests assert the geometry against mat, the parsing and the leader
extremities, and render tests/out/*.png for visual checks. Build the
documentation from the root of the repository with
typst compile --root . docs/documentation.typ.
License
The package is released under the MIT license (see LICENSE).
The paths and flood fills (the section “Paths and flood fills” of
nicematrix.typ) are adapted from
pavemat (MIT, © 2024–2025
QuadnucYard).
The documentation, in the docs folder of the repository, is not part of the
package, and one of its files is not covered by the MIT license:
docs/nicematrix-latex.json holds the
code examples of the manual of nicematrix (© François Pantigny), which the
documentation prints next to their Typst ports. They are distributed under the
LaTeX Project Public License 1.3c,
like nicematrix itself; the \emph{..} highlighting of the manual was
removed. The same excerpts appear in docs/documentation.pdf.