Every visual choice — the cream background, warm-brown text, croissant point symbols, and custom font — is controlled through {ggplot2}’s theme system. By the end of this reading you’ll know how to build something like this from scratch.
What is a theme?
A theme is a collection of visual settings that control every non-data element of a plot: text sizes and fonts, background colors, grid line weights, legend positions, axis tick lengths, and more. Themes do not change what data is encoded or how it is mapped to aesthetics — they only change how the plot looks.
{ggplot2} ships with eight complete themes that set all elements at once. You can then layer additional theme() calls on top to tweak individual elements.
Complete themes
Let’s build a base plot to use throughout this section:
gapminder_2000s <- gapminder |>filter(year >2000)p <-ggplot(data = gapminder_2000s,mapping =aes(x = gdpPercap, y = lifeExp, color = continent, size = pop)) +geom_point(alpha =0.7) +scale_x_log10(labels =label_currency(scale_cut =cut_short_scale())) +scale_size_continuous(labels =label_comma()) +scale_color_discrete_qualitative(palette ="Dark 3") +facet_wrap(vars(year)) +labs(x ="GDP per capita",y ="Life expectancy",color ="Continent",size ="Population",title ="Wealth and health across continents",subtitle ="2002 and 2007",caption ="Source: The Gapminder Project" )
The {ggthemes} package provides themes modeled after specific publication styles:
p +theme_tufte()
1
theme_tufte() from {ggthemes} strips all non-data ink. No background, no grid lines — maximizing the data-to-ink ratio in the spirit of Edward Tufte.
p +theme_economist() +scale_color_economist()
1
theme_economist() replicates the visual style of The Economist.
2
scale_color_economist() incorporates a corresponding color scale.
Many other extension theme packages exist for specific aesthetics ({tvthemes}, {ggdark}, {firatheme}, etc.). These are fun for personal projects but should be used thoughtfully for professional work — a theme that looks charming in isolation can feel distracting in a report.
Tweaking complete themes
Every complete theme accepts base_size and base_family arguments that scale all text and switch the font uniformly:
p +theme_minimal(base_size =14)
1
base_size = 14 scales all text elements proportionally. The default is 11. Use larger sizes for presentations, smaller for dense reports.
p +theme_minimal(base_family ="Atkinson Hyperlegible")
1
base_family sets the font for all text. The font must be installed on the system (or downloaded with systemfonts::require_font()).
Since {ggplot2} 4.0, complete themes also accept ink, paper, and accent arguments to change the color palette of the whole theme in one call:
p +aes(shape =NULL, color =NULL) +geom_smooth(method ="lm", se =FALSE) +theme_bw(ink ="#BBBBBB",paper ="#333333",accent ="#B31B1B" )
1
ink controls text and axis colors.
2
paper controls background colors.
3
accent controls highlight colors like fitted lines.
`geom_smooth()` using formula = 'y ~ x'
NoteExpressing color values in R
In R, colors can be expressed either by name (e.g., "red", "steelblue") or by hexadecimal code (e.g., "#FF0000" for red). Hex codes are more precise and allow for a wider range of colors, while named colors are more convenient for common hues. You can find many tools online for helping to choose and convert colors.
The anatomy of theme()
The components of the {ggplot2} theme system. Source: {ggplot2 styling}
theme() is the function that lets you modify individual theme elements. It has 147 arguments — one for almost every visual element of the plot. You don’t need to memorize all of them; you learn them as you need them.
The key organization principle: theme elements follow a hierarchy of specificity. More specific names override less specific ones:
axis.text — controls all axis text
axis.text.x — controls x-axis text only
axis.text.x.bottom — controls the bottom x-axis text only
Element types
Each theme argument takes one of five special functions:
Function
Controls
element_text()
Text elements (size, font, color, angle, hjust, vjust, face)
element_line()
Line elements (color, linewidth, linetype)
element_rect()
Rectangle/background elements (fill, color, linewidth)
element_geom()
Default properties of geom layers
element_blank()
Removes the element entirely
Common modifications
Here is a before/after showing several common tweaks applied at once:
p +theme_minimal() +theme(plot.title =element_text(face ="bold", size =rel(1.3)),plot.title.position ="plot",panel.grid.minor =element_blank(),legend.position ="bottom",strip.background =element_rect(fill ="grey90", color =NA),strip.text =element_text(face ="bold") )
1
Bold, larger title.
2
"plot" aligns the title to the full plot width; "panel" (default) aligns to the panel area.
3
Remove minor grid lines — they rarely add information.
4
Move legend to the bottom to free horizontal space.
5
Light grey facet label background.
6
Bold facet label text.
Building a custom theme function
When you need the same set of tweaks across many plots, wrap everything in a function. This is how you maintain a consistent “house style” across a report or presentation.
We can now apply the custom theme to the Gapminder plot:
p +theme_pretty()
And to a plot for an entirely different dataset — a custom theme should work consistently across different chart types and data:
ggplot(data =drop_na(penguins, sex),mapping =aes(x = bill_len, y = body_mass, color =str_to_title(sex))) +geom_point(size =2.5, alpha =0.6) +scale_color_discrete_qualitative(palette ="Dark 3") +scale_y_continuous(labels =label_comma()) +facet_wrap(vars(species)) +labs(x ="Bill length (mm)",y ="Body mass (g)",color ="Sex",title ="Gentoo penguins are the largest",subtitle ="Females are typically smaller than males",caption ="Source: Palmer Station LTER" ) +theme_pretty()
Setting a theme globally
Once you have a custom theme, use theme_set() to apply it to every subsequent plot in the session — no need to add + theme_pretty() to each one:
theme_set(theme_pretty())
Custom fonts
Rendering custom fonts has historically been inconsistent in R because it involves four separate systems: R, the operating system, the text rendering engine, and the graphics device. Modern {ggplot2} workflows have simplified this considerably.
{ragg}: a consistent graphics device
The {ragg} package provides a fast, consistent graphics device that handles system fonts reliably. It is used by default in ggsave() as of {ggplot2} 3.3.4.
In Quarto, use {ragg} via the dev chunk option. Add this to your document’s YAML:
knitr:opts_chunk:dev: ragg_png
Installing fonts
To use a font:
Install it on your system (download from Google Fonts, etc.)
{ragg} and {systemfonts} will detect it automatically
If you cannot install fonts directly (e.g., on a shared server), {systemfonts} provides require_font() to download from Google Fonts at render time:
library(systemfonts)require_font("Roboto Slab")
1
Downloads Roboto Slab from Google Fonts if not already installed, making it available for the current R session.
Summary
A theme controls all non-data visual elements of a {ggplot2} plot
{ggplot2} ships with eight complete themes; {ggthemes} and other packages add more
Adjust all theme text and font at once via base_size, base_family, and the ink/paper/accent arguments to complete themes
theme() with element_text(), element_line(), element_rect(), and element_blank() targets individual elements
Wrap a set of theme calls into a custom theme function for consistent styling across a project
Use {ragg} (dev: ragg_png) as the graphics device to enable reliable custom font rendering