ggplot2::theme
theme() customizes every non-data visual element in ggplot2. Titles, axis labels, tick marks, panel backgrounds, grid lines, legends, strip text, and plot margins all come from theme settings. The system uses inheritance, parent settings cascade down to child elements unless you override them at a specific level.
Signature
theme(...,
complete = FALSE,
validate = TRUE)
... accepts named arguments for theme elements. complete = TRUE marks the theme as finished, which matters when building reusable theme functions.
Returns: A theme object (a classed list, not a string).
Element functions
Four functions create themeable elements:
| Function | What it styles |
|---|---|
element_text() | Text, titles, axis labels, legend text |
element_line() | Lines, axis lines, grid lines, tick marks |
element_rect() | Rectangles, panel backgrounds, legend boxes |
element_blank() | Nothing, removes the element entirely |
element_text(family = NULL, face = NULL, colour = NULL,
size = NULL, hjust = NULL, vjust = NULL,
angle = NULL, lineheight = NULL, margin = NULL,
padding = NULL, debug = NULL)
element_line(colour = NULL, linewidth = NULL, linetype = NULL, lineend = NULL)
element_rect(fill = NULL, colour = NULL, linewidth = NULL, linetype = NULL)
The four element functions form the vocabulary of the theme system. Every visual property you want to change is an instance of one of these four types, and you pass the corresponding function as the value of a theme argument. The most common starting point for customisation is the axis text and titles, which control how the tick labels and axis names appear across the entire plot.
Axis styling
Axis titles and text
ggplot(mtcars, aes(disp, mpg)) +
geom_point() +
theme(
axis.title = element_text(colour = "navy"),
axis.title.x = element_text(size = 12, face = "bold"),
axis.title.y = element_text(size = 12, face = "bold", angle = 90),
axis.text = element_text(colour = "gray30"),
axis.text.x = element_text(angle = 45, hjust = 1)
)
axis.title.x / axis.title.y target individual axes. Same for axis.text with axis.text.x / axis.text.y. While axis text controls the labels and titles, the physical lines that frame the plotting area — the axis line itself and the small tick marks that mark each labelled position — are styled through separate element functions that accept element_line() arguments for colour, thickness, and line pattern.
Axis lines and ticks
ggplot(mtcars, aes(disp, mpg)) +
geom_point() +
theme(
axis.line = element_line(colour = "black", linewidth = 0.5),
axis.ticks = element_line(colour = "gray40"),
axis.ticks.length = unit(0.1, "inches")
)
The previous example shows how to style axis lines and ticks with specific colours and widths. Sometimes you want the opposite effect — a completely clean look with no visible axis furniture at all. The element_blank() function removes any theme element, which is commonly used to strip away tick marks, axis lines, or panel borders when you are aiming for a minimal chart style.
Remove ticks entirely:
theme(axis.ticks = element_blank())
Removing elements with element_blank() gives you a clean slate, and the same principle applies to every part of the plot. Another major area you will want to customise is the legend, which controls how the colour, size, and shape mappings are explained to the reader. You can reposition the legend around the plot, change the background behind each legend key, and adjust the text size and style of legend titles and labels.
Legend appearance
ggplot(mpg, aes(displ, hwy, colour = class)) +
geom_point() +
theme(
legend.position = "bottom",
legend.direction = "horizontal",
legend.background = element_rect(fill = "white", colour = NA),
legend.key = element_rect(fill = "gray95", colour = NA),
legend.text = element_text(size = 9),
legend.title = element_text(size = 10, face = "bold"),
legend.margin = margin(t = 0.2, unit = "cm")
)
legend.position accepts "none", "left", "right", "top", "bottom", or a numeric vector for placing inside the plot. When you choose to place the legend directly on top of the plotting area rather than in a dedicated margin space, you can specify the exact coordinates as a fraction of the plot width and height, which gives you pixel-perfect control over where the legend box sits relative to your data points.
theme(legend.position.inside = c(0.85, 0.15))
With axis and legend styling under control, the next visual layer to address is the panel — the rectangular area behind your data where the grid lines and background colour live. Changing the panel background from the default grey to white is one of the most common theme adjustments, and you can also add a border around the panel, adjust the major and minor grid lines independently, and even set the background colour of the entire plot area outside the panel.
Panel background and grid
ggplot(mtcars, aes(disp, mpg)) +
geom_point() +
theme(
panel.background = element_rect(fill = "white", colour = NA),
panel.border = element_rect(fill = NA, colour = "gray30"),
panel.grid.major = element_line(colour = "gray80", linewidth = 0.3),
panel.grid.minor = element_line(colour = "gray90", linewidth = 0.15),
panel.ontop = FALSE,
plot.background = element_rect(fill = "#f0f0f0", colour = NA)
)
panel.ontop = TRUE draws grid lines on top of your data, useful when points would otherwise obscure the grid. Beyond the structural elements of axes and panels, the text annotations — plot title, subtitle, and caption — each have their own dedicated theme elements. You can control their font size, face, colour, and horizontal alignment independently, and you can decide whether they align relative to the panel edge or the full plot width.
Plot titles and caption alignment
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
labs(
title = "Engine Size vs Fuel Economy",
subtitle = "1999-2008 vehicles from fueleconomy.gov",
caption = "Data: EPA"
) +
theme(
plot.title = element_text(size = 16, face = "bold", hjust = 0),
plot.subtitle = element_text(size = 11, colour = "gray40", hjust = 0),
plot.caption = element_text(size = 8, colour = "gray50", hjust = 1),
plot.title.position = "panel",
plot.caption.position = "panel",
plot.margin = margin(t = 0.3, r = 0.3, b = 0.3, l = 0.3, unit = "cm")
)
plot.title.position controls whether the title aligns relative to the "panel" (data area) or full "plot" area. When you split a plot into multiple panels with facet_wrap() or facet_grid(), each panel receives a strip label at the top that identifies the facet value. The appearance of these strip labels — their background fill, the text colour and weight, and whether they sit inside or outside the panel boundary — is controlled through the strip.background and strip.text theme elements.
Facet strip styling
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(vars(class), ncol = 3) +
theme(
strip.background = element_rect(fill = "steelblue", colour = NA),
strip.text = element_text(colour = "white", size = 10, face = "bold"),
strip.placement = "outside"
)
strip.placement = "inside" puts facet labels next to the axis rather than at the panel edge. After learning all of these individual theme controls, a common workflow is to start with a minimal baseline and selectively add back only the elements your chart needs. Stripping a plot down to its bare data ink by removing all grid lines, axis ticks, borders, and the grey panel background is a technique you will use repeatedly when preparing publication figures or embedding plots in a styled document.
Stripping a plot down
The quickest way to a minimal plot:
ggplot(mtcars, aes(disp, mpg)) +
geom_point() +
theme(
panel.grid.minor = element_blank(),
panel.grid.major.x = element_blank(),
axis.ticks = element_blank(),
axis.line = element_blank(),
panel.border = element_blank(),
panel.background = element_blank()
)
Stripping the plot element by element gives you total control but requires many lines of code. For everyday use, ggplot2 ships with several complete theme functions that apply a coherent set of theme settings in a single call. These built-in themes range from the default grey background to clean minimal styles, and you can layer additional theme() calls on top of a complete theme to override specific elements without rewriting the entire specification.
Base themes plus overrides
Built-in complete themes:
ggplot(mtcars, aes(disp, mpg)) +
geom_point() +
theme_minimal(base_size = 11) +
theme(panel.grid.minor = element_blank())
ggplot(mtcars, aes(disp, mpg)) +
geom_point() +
theme_classic()
Available: theme_grey() / theme_gray(), theme_bw(), theme_linedraw(), theme_light(), theme_dark(), theme_minimal(), theme_classic(), theme_void(). Picking a base theme and layering overrides is the standard workflow for a single plot, but when you are producing a report or dashboard with dozens of charts, you will want to define your house style once and have it apply automatically to every plot. The theme_update() function lets you change the active theme globally, so all subsequent plots inherit the new settings without any extra code.
Updating the active theme globally
theme_update() changes settings for all plots until you reset:
old <- theme_update(
panel.background = element_rect(fill = "#fafafa"),
text = element_text(family = "Helvetica", size = 10)
)
# ... make plots ...
theme_set(old) # restore previous theme
theme_get() reads the current theme without changing anything.
How inheritance works
Theme elements form a hierarchy. Setting a property high up applies it everywhere below:
text→ all textaxis.title→ all axis titles →axis.title.x→axis.title.x.bottom
Override at the most specific level needed:
theme(
text = element_text(family = "Times"),
axis.title = element_text(colour = "black"),
axis.title.x = element_text(angle = 45)
)
theme() arguments fall into two categories: element functions (element_text(), element_rect(), element_line(), element_blank()) and scalar values for properties like legend.position. Passing the wrong type raises an error: theme(axis.title = "none") fails because axis.title expects an element function. Use element_blank() to remove an element entirely, and element_text(size = 14, face = "bold") to modify text appearance.
Complete themes like theme_minimal(), theme_bw(), and theme_classic() set all non-data elements at once. Layering + theme() after a complete theme overrides only the specified elements, leaving the rest of the complete theme intact. This composability means you can define a house style as a base theme function and override specific elements per chart without duplicating the full theme specification.
See also
- /tutorials/r-data-visualization/customizing-ggplot2-themes/, step-by-step theming walkthrough
- /tutorials/r-data-visualization/ggplot2-basics/ — ggplot2 foundations
- /tutorials/r-data-visualization/ggplot2-facets-and-themes/ — combining facets with theme customization