Scientific figures for Typst

Crystal structures,
typeset.

Describe a crystal the way crystallographers do — a space group, Wyckoff positions, lattice parameters — or point at a real .xyz / POSCAR / CIF file. materia expands the symmetry, finds the bonds, and paints a publication-quality 3D figure entirely inside the Typst compiler. No Python. No raster step. Native vector output.

rocksalt.typ
#import "@preview/materia:0.1.0": prototypes, crystal

#crystal(prototypes.rocksalt("Na", "Cl", a: 5.64), width: 8cm)

Two lines — the complete source of Plate IV below.

230
space groups
80
layer groups
2
packages
0
external tools
Ball-and-stick figure of the SrTiO3 perovskite unit cell: large slate-blue strontium spheres at the corners, a gray titanium sphere at the center inside a translucent TiO6 octahedron with copper-colored oxygen atoms, with a legend and an a/b/c axes triad.
Plate I — SrTiO3 perovskite, space group Pmm, with TiO6 coordination octahedra. Drawn by materia from five Wyckoff sites.
New File import & molecular rendering .xyz · extended-xyz · POSCAR · CIF importers, molecule mode, Brillouin zones, band panels, and declarative molecular-orbital diagrams.

§ 01 · From a real file to a figure

Point it at the file you already have.

Structures rarely start life in a document — they come out of VASP runs, database exports, and geometry optimizers. The bundled materia-io parser (Rust, compiled to WASM, shipped inside the package) reads the file and hands materia a clean structure: one import call, one render call, done.

water.xyz
3
water molecule
O   0.000   0.000   0.000
H   0.757   0.586   0.000
H  -0.757   0.586   0.000

becomes

document.typ
#import "@preview/materia:0.1.0": import-xyz, molecule

#molecule(import-xyz(path("water.xyz")))
Rendered water molecule from the xyz file: a red oxygen sphere bonded to two white hydrogen spheres with two-tone bonds, plus an element legend.
Fig. 1a — a plain .xyz is Cartesian atoms with no lattice, so it renders as a molecule: no cell grid, no crystallographic triad.
Rendered silicon crystal cell from an extended-xyz file: tan silicon spheres on a cubic cell with gray cell edges, legend, and axes triad.
Fig. 1b — extended-xyz with a Lattice="…" header is auto-detected and comes back as a periodic cell instead.
POSCAR
Cu fcc conventional cell
1.0
   3.615  0.000  0.000
   0.000  3.615  0.000
   0.000  0.000  3.615
Cu
4
Direct
 0.0 0.0 0.0
 0.0 0.5 0.5
 0.5 0.0 0.5
 0.5 0.5 0.0

becomes

document.typ
#import "@preview/materia:0.1.0": import-poscar, crystal

#crystal(import-poscar(path("POSCAR")))
Rendered face-centered-cubic copper crystal from a VASP POSCAR file: copper-colored spheres connected by thick bonds inside a cubic cell with legend and axes triad.
Fig. 2 — fcc copper straight from a VASP 5 POSCAR/CONTCAR; Direct and Cartesian coordinate modes both work, scale factor included.

Bonds arrive pre-computed by the parser — the same 1.15× covalent-radius rule as hand-built structures, so imported and authored figures look identical.

nacl.cif
data_nacl
_cell_length_a 5.64
_cell_length_b 5.64
_cell_length_c 5.64
loop_
_symmetry_equiv_pos_as_xyz
'x, y, z'
'z, x, y'
'-x, -y, -z'
'x, y+1/2, z+1/2'
# … the ops the file asserts
loop_
_atom_site_label
_atom_site_fract_x
_atom_site_fract_y
_atom_site_fract_z
Na1 0.0 0.0 0.0
Cl1 0.5 0.0 0.0

becomes

document.typ
#import "@preview/materia:0.1.0": import-cif, crystal

#crystal(import-cif(path("nacl.cif")))
Rendered rocksalt NaCl crystal from a CIF file: alternating purple sodium and green chlorine spheres on a cubic lattice with legend and axes triad.
Fig. 3 — an explicit _symmetry_equiv_pos_as_xyz loop (the path most database exports take) is applied directly; a bare spacegroup number falls back to materia’s own symmetry tables.

Files the pragmatic subset can’t honor — partial occupancy, multi-block — are rejected with an error that names the unsupported feature, never silently mis-expanded.

§ 02 · Render modes

One structure, four vocabularies.

Crystallographers, chemists, and textbook authors draw the same atoms differently. A single mode: argument on crystal() and molecule() switches the whole figure.

Six-panel comparison: benzene in ball-and-stick, licorice, and space-filling CPK modes on the top row; NaCl in ball-and-stick and space-filling modes plus benzene with single-color gray bonds on the bottom row.
Plate II — benzene and NaCl across the render modes; lower right, the single-color bond option replacing the default two-tone split.
"ball-and-stick"
The default. Covalent-scale balls, two-tone sticks split at the bond midpoint — the Materials Project look.
"licorice"
Uniform thin sticks with matching end caps; atom size independent of element. The organic chemist’s sketch.
"space-filling" / "cpk"
Spheres at the van der Waals radius (Alvarez 2013 table), no bonds — how tightly the electrons actually pack.
bond-color: luma(110)
Opt out of the two-tone split and draw each bond as a single stick in any color you name.

§ 03 · Perspective camera

Depth, when you want it.

Orthographic projection is the right default for technical figures — parallel lines stay parallel, distances stay comparable. But a large supercell reads better with real foreshortening. Add two keys to the view: dictionary and the camera pulls in; the default stays pixel-identical to earlier releases.

perspective.typ
#crystal(nacl, supercell: (2, 2, 2), view: (
  azimuth: 25deg, elevation: 15deg,
  mode: "perspective", distance: 18,  // Ångström
))
Side-by-side comparison of the same NaCl supercell rendered with the default orthographic camera and with the perspective camera at distance 18, where nearer atoms appear visibly larger.
Plate III — the same NaCl supercell, orthographic (left) and perspective at distance: 18 (right).

§ 05 · The family

Two packages, one scene core.

Every package speaks the same typed-primitive language and renders through the same depth-sorted painter — so figures from different packages share themes, cameras, and composition inside one CeTZ canvas.

scenery 0.1.0

The shared 2D/3D scene core — Typst’s first programmatic 3D API. Typed primitives, named anchors, cameras, occlusion, themes, legends and colorbars.

#import "@preview/scenery:0.1.0":
  build-scene, sphere, seg, camera, render-scene

#let scene = build-scene(
  sphere((0,0,0), 0.6),
  sphere((2,0,0), 0.6, color: rgb("#dd8452")),
  seg((0,0,0), (2,0,0)),
)
#render-scene(scene,
  camera(azimuth: 30deg, elevation: 20deg),
  width: 5cm)

materia · real space 0.1.0

Crystal and molecular figures from space groups, layer groups, Wyckoff positions, or structure files. Symmetry tables are cross-validated against pymatgen.

// Rutile TiO2 from its asymmetric unit
#crystal(structure(
  spacegroup: 136,
  lattice: (a: 4.594, c: 2.959),
  sites: (
    (element: "Ti", wyckoff: "a"),
    (element: "O",  wyckoff: "f", x: 0.305),
  ),
))

materia · reciprocal & electronic

Wigner–Seitz zones, HPKOT-labelled k-paths, band panels, and declarative molecular-orbital diagrams share the same composable scene API.

#import "@preview/materia:0.1.0": bz-figure

#bz-figure((a: 3.61), bravais: "cF",
  path: ("Γ", "X", "W", "K", "Γ", "L"),
  width: 8cm)

§ 06 · How it works

Rust where throughput bites,
Typst where the science lives.

A deliberate boundary: the WASM plugin does throughput-bound text and geometry; Typst keeps the validated crystallography and every styling decision. The prebuilt .wasm ships inside the package — end users never need a Rust toolchain.

File bytes

.xyz · extended-xyz · POSCAR · CIF. Typst reads the file; the plugin never touches disk.

rust · wasm

materia-io

Real parsers with real errors. CIF symmetry-op loops applied directly; bonds auto-detected. Freestanding wasm32 — deterministic, no I/O.

Normalized record

One JSON schema for every format: lattice, Cartesian atoms, optional spacegroup + asymmetric unit, bonds. Golden-file fixtures stay human-reviewable.

typst

materia

Symmetry expansion through pyxtal-generated, pymatgen-cross-validated tables; molecule mode; bond rules; render-mode selection.

typst

scenery → CeTZ

Projection, occlusion, painter’s depth sort, gradient shading, themes — pure vector output on the page.

  • Host-agnostic core. The crates assume nothing about Typst — bytes in, structured geometry out — so the same .wasm could power a standalone web viewer later.
  • Rebuilt in CI. The toolchain is pinned and CI rebuilds each committed .wasm from source, then exercises it through parser and pixel-equivalence tests.
  • No split-brain crystallography. Table-based symmetry expansion stays in Typst, cross-validated against pymatgen; Rust only applies operations a file itself asserts.
  • Pure Typst remains first-class. Geometry rendering defaults to Typst; the WASM engine is an explicit acceleration path with pixel-equivalence gates.

§ 07 · The engine, live

Not a video. The actual WASM,
rotating in your browser.

§ 06 said the same .wasm could power a standalone web viewer. Here it is: this page fetches the byte-identical plugin binary Typst loads, instantiates it in JavaScript, and calls it again on every frame you drag — depth sort, occlusion clipping, BSP face splits, all computed in the engine. The canvas only paints what comes back.

Ball-and-stick figure of the SrTiO3 perovskite unit cell: large slate-blue strontium spheres at the corners, a gray titanium sphere at the center inside a translucent TiO6 octahedron with copper-colored oxygen atoms.
Plate XII — SrTiO3 once more, but alive: the same five Wyckoff sites, re-sorted by the plugin binary on every frame.