Mimicking LaTeX theorem environments with CSS

8 August 2021

LaTeX supports “theorem environments,” like this:

(Image)

These aren’t hard to mimic with :before and :after:

article {
  counter-reset: definition theorem lemma example proposition corollary;
}

.definition:before {
  counter-increment: definition;
  content: "Definition " counter(definition) ".";
  font-weight: bold;
}

.theorem:before {
  counter-increment: theorem;
  content: "Theorem " counter(theorem) ".";
  font-weight: bold;
}

.lemma:before {
  counter-increment: lemma;
  content: "Lemma " counter(lemma) ".";
  font-weight: bold;
}

.example:before {
  counter-increment: example;
  content: "Example " counter(example) ".";
  font-weight: bold;
}

.proposition:before {
  counter-increment: proposition;
  content: "Proposition " counter(proposition) ".";
  font-weight: bold;
}

.corollary:before {
  counter-increment: corollary;
  content: "Corollary " counter(corollary) ".";
  font-weight: bold;
}


p:first-child {
  display: inline;
}

One small problem:

(Why inlining is necessary)

So we just inline the first paragraph.

.definition p:first-child,
.theorem p:first-child,
.lemma p:first-child,
.example p:first-child,
.proposition p:first-child,
.corollary p:first-child {
	display: inline;
}

This is almost perfect. You may notice the proof environment is missing.

Proofs, the easy way

Proofs are just like any other theorem environment, except they have an end-of-proof sign U+25FB on the far right of the last line. So you can just do:


.proof {
	overflow: auto; /* Encompass floated children. */
}

.proof:before {
	content: "Proof. "
	font-style: italic;
}

.proof:after {
	content: "\25FB";
	float: right;
}

Except this doesn’t quite work:

(Image)

Inlining fixes this, sort of:

.proof > p:last-child:not(:nth-child(2)) {
  display: inline;
}

.proof > p:last-child:nth-child(2) {
  margin-block-end: 0;
}

The only corner case is when a proof contains exactly two paragraphs; they can’t both be inlined, or they’ll run together. I chose to sacrifice the end-of-proof symbol’s placement, rather than the :before. CSS cannot style an element by its children yet, so this is as far as the logic can go.

Handling KaTeX

Of course, a LaTeX renderer makes things more complicated, but not by much. The first problem we have is that under overflow: auto, proofs that contain display math will overflow their containers slightly. This is really annoying, especially on mobile. There are a few ways of fixing it, none of which I fully understand. I went with display: table, which I’m sure is bad practice, but it was the best out of what I tried.

.proof {
  display: table;
  table-layout: fixed; /* Prevent container from stretching */
  width: 100%;
}

@media screen and (min-width: 900px) {
  .proof {
    width: calc(var(--content-proportion) * 100%);
  }
}

Another problem we have is rendering the end-of-proof symbol properly in proofs that end with display math. The usual LaTeX fix is \qedhere, but we obviously don’t have that available to us.

Or do we?

The definition of \qedhere in the amsthm package is as follows:

%    Prints the current QED symbol and replaces the top entry on the
%    stack with a null entry.
%    \begin{macrocode}
\newcommand{\qedhere}{%
  \begingroup \let\mathqed\math@qedhere
    \let\qed@elt\setQED@elt \QED@stack\relax\relax \endgroup
}
%    \end{macrocode}

There’s a stack involved. And the code goes on for pages after this. So no, the LaTeX way of placing the end-of-proof is not going to be emulated with KaTeX macros. We could possibly fake it with a macro for \hfill \qedsymbol, but \hfill isn’t implemented yet..

A satisfying solution to this requires either JavaScript or a deeper understanding of the structure KaTeX generates than I possess. I chose laziness rather than satisfaction:

.proof > p:last-child > span:last-child .katex-display {
  margin: 1em 0 calc(1.6em * -1.1); /* Line-height times -1.1 */
}

It’s not perfect — especially when the last line of display math contains something like a \sum — but it’s good enough for me.

The complete code is here:

.definition:before {
  counter-increment: definition;
  content: "Definition " counter(definition) ".";
  font-weight: bold;
}

.theorem:before {
  counter-increment: theorem;
  content: "Theorem " counter(theorem) ".";
  font-weight: bold;
}

.lemma:before {
  counter-increment: lemma;
  content: "Lemma " counter(lemma) ".";
  font-weight: bold;
}

.example:before {
  counter-increment: example;
  content: "Example " counter(example) ".";
  font-weight: bold;
}

.proposition:before {
  counter-increment: proposition;
  content: "Proposition " counter(proposition) ".";
  font-weight: bold;
}

.corollary:before {
  counter-increment: corollary;
  content: "Corollary " counter(corollary) ".";
  font-weight: bold;
}

.proof {
  display: table;
  width: 100%;
}

.corollary p:first-child,
.theorem p:first-child,
.definition p:first-child,
.proof p:first-child,
.lemma p:first-child,
.example p:first-child {
  display: inline;
}

.corollary,
.theorem,
.definition,
.proof,
.lemma,
.example {
  margin-block: 1rem;
}

.proof:before {
  content: "Proof. ";
  font-style: italic;
}

.proof:after {
 	content: "\25FB";
  float: right;
}

.proof > p:last-child:not(:nth-child(2)) {
  display: inline;
}

.proof > p:last-child:nth-child(2) {
  display: inline-block;
  margin-block-end: 0;
}

.proof > p:last-child > span:last-child .katex-display {
  margin: 1em 0 calc(-1.6em * 1.1);
}