People who know me better will probably know that I am a bit of a typography nerd, which is a slightly less cynical way of saying that I sometimes like to spend time getting some details right that other people might not even notice. This is the story of one of these adventures.

The curious case of the missing ligatures

A few weeks ago, I was putting together a little private website using a font that I really like1, EB Garamond, an modern open-source implementation of a classic Antiqua font dating back to 1592. Because it is such a classic font, I was a little surprised when I noticed that the standard ligatures for letter combinations such as fi, ffi and fl were not working.2 Just in case you have never heard about ligatures, this is what I am talking about:

Without ligature   With ligature
The letter combination "fi" in EB Garamond. Left: Without a ligature, right: with a ligature.

Now, at this point, the most sane reaction probably would have been to say “Huh”, shrug, and move on with my life. But as I already said, I like to get these things right, and perhaps even more importantly, I like to understand what is happening when things are not working, and so I started looking into it a bit deeper.

My first theory was that it might have to do with Adobe Fonts (formerly known as Typekit), which I was using to embed the font.3 And indeed, according to the “Web Projects” tab4, EB Garamond does not support any OpenType features, including common ligatures (liga):

Screenshot from fonts.adobe.com
According to the "Web Projects" tab on Adobe Fonts, EB Garamond does not support any OpenType features, including liga (common ligatures) and dlig (discretionary ligatures).

This is actually pretty weird, not only because it contradicts the Wikipedia article on EB Garamond, which clearly lists liga as an available feature. It is also weird because if you go to the respective page of EB Garamond on Adobe Fonts and start typing something like “fi ffi fl” into the sample text field, you will notice that the preview there does actually give you ligatures. Eh, what? 🤔

As a first idea to fix the issue, I decided to swap out Adobe Fonts for Google Fonts, whose preview feature was also showing me ligatures. Unfortunately, however, that still did not fix the problem on my website. At that point, I started suspecting that maybe the issue was due to the browser’s cache, so I double checked with another browser, and now it got really weird: Neither Firefox, nor Chrome, nor Opera displayed the ligatures, but for some reason, Safari did.5

As a next step, I dropped Google Fonts and switched to the completely manual, @font-face-based way of using the WOFF2 font files from the official EB Garamond GitHub repository. What followed was a long and fruitless series of attempts to fix the problem by playing around with various CSS attributes, such as font-variant-ligatures. Of course, nothing worked, and after a while (I had already spent the better part of a Sunday afternoon on this), I gave up in frustration…

…until the next day, when my curiosity got the better of me again and I started looking into the problem once more. I had just started preparing a minimal working example, when suddenly, the ligatures started working! I started comparing the MWE with my actual website, and finally I realized: In the minimal example, I had specified the document language as <html lang="en"> (i.e., English), whereas the actual website I was building was in German: <html lang="de">. But that could not possibly be the reason, could it?! Well, yes. Yes it could. Changing the language between English and German would switch the ligatures off and on.

“But why on Earth would the document language matter for ligatures?!” is what I though at this point.

Ligatures in different languages

As it turns out there actually is a valid reason. A very obvious reason, retrospectively: Many rules of typography depend on the specific language that is being typeset, and ligatures are no exception here. The most intuitive example I came across is the Turkish language. Turkish, and also a number of other Turkic languages such as Azerbaijani, Gagauz or Kyrgyz, has two different versions of the letter “i”, one with and one without the dot (somestimes also called “tittle”): “i” and “ı”. However, in a ligature with an “f”, the dot would be absorbed into the “f”, thus making it impossible to tell which version of “i/ı” was used.6 Consequently, Turkish texts do not use the “fi” ligature. And indeed: Setting the document language to Turkish disables the “fi” but keeps the ligatures for “ff” and “fl”:

Turkish ligatures
Ligatures in EB Garamond when setting the document language to Turkish.

Okay, so, while this is quite interesting, it does not really explain what I was seeing for my website. German only has one version of the letter “i”, the same as English, so it cannot be the same reason as for Turkish. I started researching typography and ligature rules for German, and realized that there are actually quite a few! The two most important ones seem to be the following:

  1. No ligatures across the word border of a compound noun.

    German is known (and maybe sometimes feared) for being quite partial to concatenating nouns into compounds, which are typically written without a space or hyphen. Ligatures should never span across the border of the individual words, though. For instance, “Schilf|insel”7 (meaning “reed island”) should be written without any ligatures.

    Another example is “Schiff|fahrt” (= seafaring). According to the old German orthography rules, which were in place until the 1990s, the word was spelled with only two “f” (“Schiffahrt”), as having the same letter repeat three times was considered unaesthetical (or at least that is what my 3rd grade German teacher told me). In this case, no ligature would be used. Following the last spelling reform, having three f’s is permitted; however, in this case, only the first two (i.e., the ones in “Schiff”) would be combined into a ligature.

  2. No ligatures for flectional endings and final syllables, except those starting with “i”.

    Okay, this rule is maybe a bit more complicated than the last one, but bear with me. Let’s start with he “flectional endings”. German happens to be one of those languages that heavily conjugates its verbs, meaning the verbs take on different forms depending on who they refer to, the tense, whether you are talking about something real or something hypothetical, and so on. Think “I do” vs. “she does”, but with a much richer variety of patterns.

    Now, In cases where a “ligature-able” (ligable?) combination of letters (e.g., “f” and “t”, although “ft” is sometimes considered a discretionary ligature) appears at the border of the root of the word and its flectional ending, no ligature should be used. For example, “kauf|te” (= he / she / it bought) should be written without a ligature, while “beauftragte” (= he / she / it assigned something or someone) might combine the “ft”.

    Lastly, ligatures should not be used to connect the final syllable of a word. For example, “höf|lich” (= polite) should not use a ligature. An exception to this rule are final syllables that begin with “i”, for example “-ich”, “-ig”, “-in” or “-isch”. The word “häuf|ig” (= frequently), therefore, should use a ligature.

If you find this confusing, I can reassure you: It is confusing.8 In fact, it is so confusing that even professionals do not always seem to get it right. When I first saw these rules, I was like “Wait, really?!” and immediately went to check some actual books to see if they obeyed these rules. My findings were, uh, mixed, as the following example will illustrate. I have taken it from page 10 of Martin Walker’s »Menu surprise« (original title: A Taste for Vengeance), which was published in 2019 by Diogenes; one of the most renowned and reputable publishers of books in German language.9,10

“Correct” example: Correct usage of the ft ligature
The -ten in “kämpften” is a flectional syllable, hence according to the rules above, the F and the T should not be combined. Likewise, the ft in “Mannschaft” does not match any special rules, so a ligature is appropriate.
“Incorrect” example: Incorrect usage of the ft ligature
The -te in “lupfte" is again a flectional syllable, so actually, there should be no ligature here.

A bit later, after I had understood the technical aspects of how ligatures are defined in a font file a bit better (see below), I actually reached out via email to Georg Duffner and Octavio Pardo, the two people that actually designed EB Garamond, and asked them about their motivation for disabling the f-based ligatures in German. Both of them actually replied! 🙂

(Add something about explanations from Georg)

Technical details: the GSUB table

While now definitely would have been a good time to just call it a day, I got curious how all of this works on a technical level. How does a font know if its supposed to use a ligature or not? Where does the language dependency enter? Digging ever deeper into the rabbit hole that I had fallen into, I found that for OpenType fonts, the answer appears to be hidden the Glyph Substitution Table (GSUB) of the font file, which, in general, can be a pretty complicated beast.

To take a closer look, I used ttx, a little tool that is able to extract the GSUB table from a given font file into an XML document by running the following command:

ttx -t GSUB EBGaramond-Regular.ttf

The resulting output is pretty long, more than 3500 lines, with a structure that is not exactly super obvious for someone who is looking at this for the first time. Luckily, the interesting bits already start very early in the document: The GSUB table basically begins with a list of different languages for each of which it defines a feature set. In the case of German, this looks like the following:

<LangSysRecord index="3">
  <LangSysTag value="DEU "/>
  <LangSys>
    <ReqFeatureIndex value="65535"/>
    <!-- FeatureCount=28 -->
    <FeatureIndex index="0" value="0"/>
    <FeatureIndex index="1" value="1"/>
    <FeatureIndex index="2" value="2"/>
    <FeatureIndex index="3" value="3"/>
    <FeatureIndex index="4" value="4"/>
    <FeatureIndex index="5" value="5"/>
    <FeatureIndex index="6" value="6"/>
    <FeatureIndex index="7" value="7"/>
    <FeatureIndex index="8" value="8"/>
    <FeatureIndex index="9" value="9"/>
    <FeatureIndex index="10" value="13"/>
    <FeatureIndex index="11" value="17"/>
    <FeatureIndex index="13" value="18"/>
    <FeatureIndex index="13" value="19"/>
    <FeatureIndex index="14" value="20"/>
    <FeatureIndex index="15" value="21"/>
    <FeatureIndex index="16" value="22"/>
    <FeatureIndex index="17" value="23"/>
    <FeatureIndex index="18" value="24"/>
    <FeatureIndex index="19" value="25"/>
    <FeatureIndex index="20" value="26"/>
    <FeatureIndex index="21" value="27"/>
    <FeatureIndex index="22" value="28"/>
    <FeatureIndex index="23" value="29"/>
    <FeatureIndex index="24" value="30"/>
    <FeatureIndex index="25" value="31"/>
    <FeatureIndex index="26" value="32"/>
    <FeatureIndex index="27" value="33"/>
  </LangSys>
</LangSysRecord>

Each of these FeatureIndex elements is a reference to a rule set that is defined in a later part of the GSUB document. Interestingly, German seems to differ from the default language only in a single feature, namely the one with value="13". If we look up the corresponding FeatureRecord, we get:

<FeatureRecord index="13">
  <FeatureTag value="locl"/>
  <Feature>
    <!-- LookupCount=1 -->
    <LookupListIndex index="0" value="6"/>
  </Feature>
</FeatureRecord>

Again, this is just a reference, this time to the LookupIndex with value="6". If we follow it once more, we finally arrive at the one substitution rule for EB Garamond that is unique to German:

<Lookup index="6">
  <LookupType value="1"/>
  <LookupFlag value="0"/>
  <!-- SubTableCount=1 -->
  <SingleSubst index="0">
    <Substitution in="Adieresis" out="Adieresis.deu"/>
    <Substitution in="Odieresis" out="Odieresis.deu"/>
    <Substitution in="Udieresis" out="Udieresis.deu"/>
    <Substitution in="f" out="f.DEU"/>
  </SingleSubst>
</Lookup>

The rule actually consists of four parts, but only the last one matters for our current case:

<Substitution in="f" out="f.DEU"/>

In more plain language, this means: “Replace the chararacter f with the character f.DEU”. Now what is that about? Why is there a special “German f” in this font? To take a closer look, I brought out FontForge and compared the two glyphs:

f   f.DEU
The standard f and the German f.DEU.

Looks suspiciously similar? Well, yes. Checking out the glyph information for f.DEU reveals that it is a “reference to character f at position 102”. In other words, f.DEU is really just an alias for the normal f.

Now why would you have a rule that replaces all glyphs named f with a glyph named f.DEU that looks exactly the same as the original f? Well, if you go and replace all f character with f.DEU character before you call the rule that replaces all combinations of fi or ff with the corresponding ligature, then you have disabled f-based ligatures! And this is precisely what seems to be happening here. I confirmed this by modifying the original font file to remove the f.DEU substitution, and as a result, the f-based ligatures suddenly started working. Tada, case solved! 🤓

Lessons learned

(What did I learn from this?)

TL;DR: If you are making a website and for some reason the ligatures are not working as expected, maybe check which language you have specified for your document in the <html lang="..."> tag.


  1. That is not to say: I am quite font of it 🙊 ↩︎

  2. In case you are wondering why those letter combinations are not producing ligatures on this blog even though I am a self-proclaimed typography nerd: It is because this website is currently using Merriweather as its main font for text, and Eben Sorkin, the font’s designer, has decided not to add dedicated glyphs for ligatures for now. I am not sure whether I fully agree with his decision, but I currently like the font too much for this to be a dealbreaker. ↩︎

  3. Mostly because I am already paying for Adobe Fonts as part of my Lightroom subscription, and I figured I might as well get some value for my money. ↩︎

  4. For some reason, this really seems like the only place where this type of information is ever exposed on Adobe Fonts, for reasons that are completely beyond me. I probably spent like an hour looking at the source code of the website trying to figure out which requests I would need to send to the Adobe server to get the information about the OpenType features for any given font before I eventually gave up. (My plan at this point was to maybe just ditch EB Garamond and find a font that I like that comes with OpenType support…) I still do not understand why this information is not just accessible through their normal filters…?! ↩︎

  5. I just checked again, and actually Safari is now consistent with the other three browsers. However, I updated my macOS in between figuring out the issue and writing this post, and I suspect that “fixed” it? ↩︎

  6. Despite my best efforts, I did manage to find an example of a word pair where this would actually make a difference or could cause confusion. If you happen to know Turkish and can provide me with such an example, please feel free to reach out! ↩︎

  7. I added the “|” to highlight the word borders here; usually, we would simply write “Schilfinsel”. ↩︎

  8. By the way, those were the rules for Antiqua, the style of typeface that we all use today. However, for 𝕱𝔯𝔞𝔨𝔱𝔲𝔯 (a.k.a. Gothic type, German type, or black letter), the typeface class that was the common standard in Germany and Austria (and pretty much only there) until World War 2, the rules are supposedly even more complicated↩︎

  9. Diogenes is actually a Swiss publishing house based in lovely Zürich, which might be relevant because Swiss German occasionally follows slightly different orthographic rules than German from Germany. Most prominently, Swiss German generally does not use the letter ß (which, historically, evolved as a ligature of the letters S and Z — hence the name Eszett) but writes ss instead. However, I checked, and at least this particular book does use ß, so I assume that it should not matter. Besides, as far as my understanding goes, OpenType features are defined for languages, not countries, so the rules should be the same for Switzerland and Germany in any case? ↩︎

  10. The main reason why I chose this book was because it was the first that I could find on the Diogenes website that had everything I was looking for (“correct” and “incorrect” usage of an ft ligature for a flectional syllable, as well as a “normal” ft) all on one page. ↩︎