Customizing ggplot2 Themes
Every ggplot2 plot has two distinct parts: the data mappings (aesthetics) and everything else. The “everything else” is the theme system. Axis lines, tick marks, panel backgrounds, grid lines, plot titles, subtitles, captions, legend positioning, and facet labels all fall under theme control. Understanding themes means you can make any plot look exactly the way you want it to look.
Built-in Themes
ggplot2 ships with eight complete themes. Each one sets every non-data visual element at once, giving your plots a consistent style with a single function call.
| Theme | Best for |
|---|---|
theme_grey() | Default. Grey background, white panel, light grey grid |
theme_bw() | Black and white. White panel with black border. Printer-friendly |
theme_minimal() | No background, no axis borders. Cleanest option |
theme_classic() | No grid lines. Plain black axes. Good for editorial use |
theme_light() | Like minimal but with light grey grid lines |
theme_dark() | Dark background. Grid lines are also dark |
theme_linedraw() | Only horizontal grid lines, black axes |
theme_void() | Completely empty. Only the geometry remains |
library(ggplot2)
p <- ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) +
geom_point(size = 3) +
labs(
title = "Fuel Economy vs. Weight",
subtitle = "By number of cylinders",
color = "Cylinders"
)
# Apply different themes — each returns a ggplot object you can further modify
p + theme_minimal()
p + theme_classic()
p + theme_bw()
theme_grey() is the default, applied automatically if you don’t specify one. theme_gray() is identical — both names work.
How the Theme System Works
The theme system is a cascading override. You start with a complete built-in theme, then selectively override individual elements using theme(). Each element expects a specific type: element_text(), element_line(), element_rect(), or element_blank().
This matters because passing the wrong element type causes an error or silent failure. For example, panel.grid.major expects a line element, so you must pass element_line() or element_blank(), not element_text().
# Start from a base theme, then override specifics
p + theme_minimal() +
theme(
axis.text = element_text(size = 12),
panel.grid.major = element_line(color = "gray90")
)
Tweaking Individual Elements with theme()
The theme() function accepts hundreds of named arguments. The naming follows a dot-separated hierarchy that groups elements logically.
Text Elements
p + theme(
plot.title = element_text(size = 16, face = "bold", hjust = 0),
plot.subtitle = element_text(size = 12, color = "gray40", hjust = 0),
plot.caption = element_text(size = 9, color = "gray50", hjust = 1),
axis.title.x = element_text(size = 12, face = "bold"),
axis.text.x = element_text(color = "darkred", angle = 45)
)
element_text() arguments include family (font name like "serif" or "sans"), face ("bold", "italic", "bold.italic"), size (in points), color, hjust and vjust (0 to 1 for alignment), angle (degrees), and margin (using margin()).
Line Elements
p + theme(
panel.grid.major = element_line(color = "gray80", size = 0.5, linetype = "dashed"),
panel.grid.minor = element_blank(),
axis.line = element_line(color = "black", size = 0.8),
axis.ticks = element_line(color = "black")
)
element_line() accepts color, size (in mm), linetype ("solid", "dashed", "dotted", etc.), and lineend ("round", "butt", "square"). Use element_blank() to remove a line element entirely.
Rectangle Elements
p + theme(
panel.background = element_rect(fill = "white", color = NA),
plot.background = element_rect(fill = "#F0F0F0", color = NA),
panel.border = element_rect(color = "gray30", fill = NA, size = 1)
)
element_rect() controls filled boxes: backgrounds, borders, legend boxes. Use fill for background color and color for the border. Setting color = NA removes the border.
Legend Customization
Legends are controlled through several theme elements. legend.position accepts "none", "left", "right", "top", "bottom", or a numeric vector c(x, y) for precise placement inside the plot area.
# Move legend to bottom, horizontal
p + theme(legend.position = "bottom")
# Place legend inside the plot
p + theme(legend.position = c(0.8, 0.2))
# Remove legend entirely
p + theme(legend.position = "none")
# Style legend text and title
p + theme(
legend.title = element_text(size = 11, face = "bold"),
legend.text = element_text(size = 10),
legend.margin = margin(t = 5, r = 5, b = 5, l = 5)
)
For multiple legends arranged horizontally:
p + theme(
legend.box = "horizontal",
legend.box.just = "center",
legend.position = "bottom"
)
Styling Facet Labels
Facet labels (the headers showing facet variable values) are controlled with strip.text and strip.background.
p + theme(
strip.text = element_text(size = 12, face = "bold", color = "white"),
strip.background = element_rect(fill = "steelblue", color = NA)
)
# Style x and y strips independently
p + theme(
strip.text.x = element_text(size = 11, face = "bold.italic"),
strip.text.y = element_text(size = 11, face = "bold.italic", angle = 0)
)
Controlling Spacing
Two elements handle the whitespace around your plot. panel.spacing controls the gap between panels in faceted plots. plot.margin adds outer padding around the entire plot.
# Space between panels in facet_wrap or facet_grid
p + facet_wrap(~ cyl) +
theme(
panel.spacing = unit(0.5, "cm"),
panel.spacing.x = unit(1, "cm"),
panel.spacing.y = unit(0.3, "cm")
)
# Outer margins around the full plot (top, right, bottom, left)
p + theme(plot.margin = margin(t = 1, r = 1.5, b = 0.5, l = 0.5, unit = "cm"))
Building a Reusable Custom Theme
When you find yourself applying the same theme settings across multiple plots, wrap them in a custom theme function. This makes your plots consistent and reduces repetition.
The key detail here is %+replace%. This operator replaces the base theme entirely, unlike + which would layer on top and cause unexpected behavior.
theme_publication <- function(base_size = 11, base_family = "sans") {
theme_minimal(base_size = base_size, base_family = base_family) %+replace%
theme(
# Text styling
plot.title = element_text(
size = 16,
face = "bold",
hjust = 0,
margin = margin(b = 8)
),
plot.subtitle = element_text(
size = 12,
color = "gray40",
hjust = 0,
margin = margin(b = 12)
),
plot.caption = element_text(
size = 9,
color = "gray50",
hjust = 1
),
axis.title = element_text(size = 11, face = "bold"),
axis.text = element_text(size = 10, color = "gray30"),
# Grid
panel.grid.major = element_line(color = "gray90", size = 0.4),
panel.grid.minor = element_blank(),
# Legend
legend.position = "bottom",
legend.box = "horizontal",
legend.title = element_text(size = 10, face = "bold"),
legend.text = element_text(size = 9),
# Spacing
plot.margin = margin(t = 0.5, r = 0.5, b = 0.5, l = 0.5, unit = "cm"),
axis.ticks.length = unit(0.2, "cm")
)
}
# Apply it to any plot
ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) +
geom_point(size = 3) +
labs(title = "Fuel Economy vs. Weight", color = "Cylinders") +
theme_publication()
The %+replace% operator is what makes this work correctly. If you use + instead, elements from the base theme that you didn’t override would still apply, potentially creating duplicate or conflicting settings.
Comparing Built-in Themes Side by Side
When choosing a theme, it helps to see them side by side. Patchwork or gridExtra::grid.arrange() let you compare multiple themes on the same data.
library(ggplot2)
library(patchwork)
base <- ggplot(diamonds[1:500,], aes(carat, price, color = cut)) +
geom_point(alpha = 0.6) +
labs(
title = "Diamond Price vs. Carat",
subtitle = "Sample of 500 diamonds",
color = "Cut"
)
minimal <- base + theme_minimal() + labs(title = "theme_minimal()")
classic <- base + theme_classic() + labs(title = "theme_classic()")
bw <- base + theme_bw() + labs(title = "theme_bw()")
linedraw <- base + theme_linedraw() + labs(title = "theme_linedraw()")
(minimal | classic) / (bw | linedraw)
This kind of comparison helps you decide which base theme to start from before applying your customizations.
See Also
- Introduction to ggplot2 — The basics of building plots with layers, aesthetics, and geoms.
- Customizing ggplot2 Charts — Fine-tune chart appearance with scales, coordinate systems, and annotations.
- ggplot2 Facets and Themes — Layout multiple panels and apply consistent styling across figures.
Written
- File: sites/rguides/src/content/tutorials/customizing-ggplot2-themes.md
- Words: ~1150
- Read time: 5 min
- Topics covered: built-in themes, theme() function, element_text, element_line, element_rect, legend customization, facet labels, spacing, custom theme functions, %+replace% operator
- Verified via: ggplot2 documentation, R documentation (rdocumentation.org)
- Unverified items: none