Skip to content

This text expects a basic understanding of fonts and typography. If you feel like you could use a brush-up you can consult this light introduction to the subject.

systemfonts is designed to give R a modern text rendering stack. That’s unfortunately impossible without coordination with the graphics device, which means that to use all these features you need a supported graphics device. There are currently two options:

  • The ragg package provides graphics devices for rendering raster graphics in a variety of formats (PNG, JPEG, TIFF) and uses systemfonts and textshaping extensively.
  • The svglite package provides a graphic device for rendering vector graphics to SVG using systemfonts and textshaping for text.

You might notice there’s currently a big hole in this workflow: PDFs. This is something we plan to work on in the future.

A systemfonts based workflow

With all that said, how do you actually use systemfonts to use custom fonts in your plots? First, you’ll need to use ragg or svglite.

Using ragg

While there is no way to unilaterally make ragg::agg_png() the default everywhere, it’s possible to get close:

  • Positron: recent versions automatically use ragg for the plot pane if it’s installed.

  • RStudio IDE: set “AGG” as the backend under Global Options > General > Graphics.

  • ggplot2::ggsave(): ragg will be automatically used for raster output if installed.

  • R Markdown and Quarto: you need to set the dev option to "ragg_png". You can either do this with code:

    #| include: false
    knitr::opts_chunk$set(dev = "ragg_png")

    Or in Quarto, you can set it in the yaml metadata:

    ---
    title: "My Document"
    format: html
    knitr:
      opts_chunk:
        dev: "ragg_png"
    ---

If you want to use a font installed on your computer, you’re done!

grid::grid.text(
  "Spectral 🎉",
  gp = grid::gpar(fontfamily = "Spectral", fontface = 2, fontsize = 30)
)

Or, if using ggplot2

library(ggplot2)
ggplot(na.omit(penguins)) +
  geom_point(aes(x = bill_len, y = body_mass, colour = species)) +
  labs(x = "Bill Length", y = "Body Mass", colour = "Species") +
  theme_minimal(base_family = "Spectral")

If the results don’t look as you expect, you can use various systemfonts helpers to diagnose the problem:

systemfonts::match_fonts("Spectral", weight = "bold")
#> # A tibble: 1 × 3
#>   path                             index features  
#>   <chr>                            <int> <list>    
#> 1 /tmp/RtmpKF1NQQ/Spectral-700.ttf     0 <font_ftr>
systemfonts::font_fallback("🎉", family = "Spectral", weight = "bold")
#>                                                path index
#> 1 /usr/share/fonts/truetype/noto/NotoColorEmoji.ttf     0

If you want to see all the fonts that are available for use, you can use systemfonts::system_fonts()

systemfonts::system_fonts()

Extra font styles

As we discussed above, the R interface only allows you to select between four styles: plain, italic, bold, and bold-italic. If you want to use a thin font, you have no way of communicating this wish to the device. To overcome this, systemfonts provides register_variant() which allows you to register a font with a new typeface name. For example, to use the light font from the Spectral typeface you can register it as follows:

systemfonts::register_variant(
  name = "Spectral Light",
  family = "Spectral",
  weight = "light"
)

Now you can use Spectral Light where you would otherwise specify the typeface:

grid::grid.text(
  "Light weight is soo classy",
  gp = grid::gpar(fontfamily = "Spectral Light", fontsize = 30)
)

register_variant() also allows you to turn on font features otherwise hidden away:

systemfonts::register_variant(
  name = "Spectral Small Caps",
  family = "Spectral",
  features = systemfonts::font_feature(
    letters = "small_caps"
  )
)
grid::grid.text(
  "All caps — Small caps",
  gp = grid::gpar(fontfamily = "Spectral Small Caps", fontsize = 30)
)

Fonts from other places

Historically, systemfonts primary role was to access the font installed on your computer, the system fonts. But what if you’re using a computer where you don’t have the rights to install new fonts, or you don’t want the hassle of installing a font just to use it for a single plot? That’s the problem solved by systemfonts::add_font() which makes it easy to use a font based on a path. But in many cases you don’t even need that as systemfont now scans ./fonts and ~/fonts and adds any font files it find. This means that you can put personal fonts in a fonts folder in your home directory, and project fonts in a fonts directory at the root of the project. This is a great way to ensure that specific fonts are available when you deploy some code to a server.

And you don’t even need to leave R to populate these folders. systemfonts::get_from_google_fonts() will download and install a google font in ~/fonts:

systemfonts::get_from_google_fonts("Barrio")

grid::grid.text(
  "A new font a day keeps Tufte away",
  gp = grid::gpar(fontfamily = "Barrio", fontsize = 30)
)

And if you want to make sure this code works for anyone using your code (regardless of whether or not they already have the font installed), you can use systemfonts::require_font(). If the font isn’t already installed, this function download it from one of the repositories it knows about. If it can’t find it it will either throw an error (the default) or remap the name to another font so that plotting will still succeed.

systemfonts::require_font("Rubik Distressed")
#> Trying Google Fonts... Found! Downloading font to /tmp/RtmpKF1NQQ

grid::grid.text(
  "There are no bad fonts\nonly bad text",
  gp = grid::gpar(fontfamily = "Rubik Distressed", fontsize = 30)
)

By default, require_font() places new fonts in a temporary folder so it doesn’t pollute your carefully curated collection of fonts.

Font embedding in SVG

Fonts work a little differently in vector formats like SVG. These formats include the raw text and only render the font when you open the file. This makes for small, accessible files with crisp text at every level of zoom. But it comes with a price: since the text is rendered when it’s opened, it relies on the font in use being available on the viewer’s computer. This obviously puts you at the mercy of their font selection, so if you want consistent outputs you’ll need to embed the font.

In SVG, you can embed fonts using an @import statement in the stylesheet, and can point to a web resource so the SVG doesn’t need to contain the entire font. systemfonts provides facilities to generate URLs for import statements and can provide them in a variety of formats:

systemfonts::fonts_as_import("Barrio")
#> [1] "https://fonts.bunny.net/css2?family=Barrio&display=swap"
systemfonts::fonts_as_import("Rubik Distressed", type = "link")
#> [1] "<link rel=\"stylesheet\" href=\"https://fonts.bunny.net/css2?family=Rubik+Distressed&display=swap\"/>"

Further, if the font is not available from a given online repository, it can embed the font data directly into the URL:

substr(systemfonts::fonts_as_import("Arial", repositories = NULL), 1, 200)
#> Warning in systemfonts::fonts_as_import("Arial", repositories = NULL):
#> No import found for Arial
#> character(0)

svglite uses this feature to allow seamless font embedding with the web_fonts argument. It can take a URL as returned by fonts_as_import() or just the name of the typeface and the URL will automatically be resolved. Look at line 6 in the SVG generated below

svg <- svglite::svgstring(web_fonts = "Barrio")
grid::grid.text("Example", gp = grid::gpar(fontfamily = "Barrio"))
invisible(dev.off())
svg()
#> <?xml version='1.0' encoding='UTF-8' ?>
#> <svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'>
#> <g class='svglite'>
#> <defs>
#>   <style type='text/css'><![CDATA[
#>     @import url('https://fonts.bunny.net/css2?family=Barrio&display=swap');
#>     .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle {
#>       fill: none;
#>       stroke: #000000;
#>       stroke-linecap: round;
#>       stroke-linejoin: round;
#>       stroke-miterlimit: 10.00;
#>     }
#>     .svglite text {
#>       white-space: pre;
#>     }
#>     .svglite g.glyphgroup path {
#>       fill: inherit;
#>       stroke: none;
#>     }
#>   ]]></style>
#> </defs>
#> <rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/>
#> <defs>
#>   <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='>
#>     <rect x='0.00' y='0.00' width='720.00' height='576.00' />
#>   </clipPath>
#> </defs>
#> <g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'>
#> <text x='360.00' y='292.32' text-anchor='middle' style='font-size: 12.00px; font-family: "Barrio";' textLength='48.12px' lengthAdjust='spacingAndGlyphs'>Example</text>
#> </g>
#> </g>
#> </svg>

Want more?

This text has mainly focused on how to use the fonts you desire from within R. R has other limitations when it comes to text rendering specifically how to render text that consists of a mix of fonts. This has been solved by marquee and the curious soul can continue there in order to up their skills in rendering text with R.