ggplot2 theme() in R: Customize Every Plot Element

The theme() function in ggplot2 controls every non-data element of a plot: fonts, axes, legends, gridlines, backgrounds, and panels. It accepts named arguments paired with element_text(), element_line(), element_rect(), or element_blank() helpers.

⚡ Quick Answer
theme(plot.title = element_text(size = 16, face = "bold"))      # title styling
theme(axis.text.x = element_text(angle = 45, hjust = 1))         # rotate x labels
theme(legend.position = "top")                                   # move legend
theme(panel.grid.minor = element_blank())                        # remove minor grid
theme(panel.background = element_rect(fill = "white"))           # change background
theme(axis.line = element_line(color = "black"))                 # add axis lines
theme(plot.margin = margin(10, 10, 10, 10))                      # adjust margins

Need explanation? Read on for examples and pitfalls.

📊 Is theme() the right tool?
STARTmodify any non-data element (fonts, colors, gridlines)theme()apply a pre-built look in one linetheme_minimal(), theme_classic(), theme_bw()change the colors of data pointsscale_color_*() or scale_fill_*()change axis ranges or labelsscale_x_*(), scale_y_*(), or labs()save a custom theme for reusewrap theme() calls in a function

What theme() does in ggplot2

theme() styles everything in a ggplot2 plot that is not the data itself. Titles, axis labels, tick marks, legend placement, panel backgrounds, gridlines, and margins all live inside theme().

The function takes one argument per element, and each argument expects a matching element_*() helper that describes how to draw it. For example, axis.title = element_text(size = 14) says "the axis title element should be a text element rendered at size 14." Pass element_blank() to hide an element entirely.

Theme calls layer on top of a base theme (theme_grey() by default). To start from a different base, add theme_minimal() or theme_bw() first, then call theme() to override specific elements.

Key Insight
ggplot2 separates data layers (geoms, stats, scales) from presentation layers (themes). This separation means you can swap themes without touching the chart logic, and a single theme() call can restyle dozens of plots consistently.

Syntax

theme(... = element_*(...)). Each argument names an element, each value describes how to render it.

Run live
Run live, no install needed. Every R block on this page runs in your browser. Click Run, edit the code, re-run instantly. No setup.
RLoad ggplot2 and inspect a default plot
library(ggplot2) p <- ggplot(mtcars, aes(wt, mpg)) + geom_point(color = "steelblue") + labs(title = "MPG vs Weight", subtitle = "mtcars dataset", x = "Weight (1000 lbs)", y = "Miles per gallon") p

  

The default look comes from theme_grey(), ggplot2's built-in starting point.

Seven common theme() patterns

1. Style the title and subtitle

RBigger, bolder title
p + theme( plot.title = element_text(size = 18, face = "bold", color = "navy"), plot.subtitle = element_text(size = 12, color = "grey40") )

  

element_text() takes size, face ("plain", "bold", "italic", "bold.italic"), color, family, angle, hjust, and vjust.

2. Rotate x-axis labels

RAngle x-axis text 45 degrees
ggplot(mtcars, aes(factor(cyl), mpg)) + geom_boxplot() + theme(axis.text.x = element_text(angle = 45, hjust = 1))

  
Tip
Set hjust = 1 when rotating axis labels. Without it, labels collide with the axis. With it, the right edge of each label aligns with the tick.

3. Move and restyle the legend

RLegend on top, no title
ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) + geom_point(size = 3) + theme( legend.position = "top", legend.title = element_blank(), legend.background = element_rect(fill = "grey95") )

  

legend.position accepts "top", "bottom", "left", "right", "none", or a numeric vector like c(0.8, 0.2) for inside-the-plot placement.

4. Remove gridlines

RDrop the minor grid
p + theme( panel.grid.minor = element_blank(), panel.grid.major.x = element_blank() )

  

element_blank() removes an element entirely. Use it for gridlines, axis ticks, or anything you want to hide without affecting layout.

5. Change the panel background

RWhite panel with black border
p + theme( panel.background = element_rect(fill = "white"), panel.border = element_rect(color = "black", fill = NA, linewidth = 0.5) )

  

element_rect() controls filled rectangles. Always set fill = NA on borders, otherwise the rectangle covers the data.

6. Add axis lines

RL-shaped axes only
p + theme( panel.background = element_blank(), axis.line = element_line(color = "black", linewidth = 0.4), panel.grid = element_blank() )

  

element_line() controls line elements: axis lines, gridlines, and tick marks. Arguments include color, linewidth, linetype, and lineend.

7. Adjust plot margins

RMore space around the plot
p + theme(plot.margin = margin(20, 20, 20, 20))

  

margin(top, right, bottom, left) sets outer plot spacing in points. Use it when titles get clipped or plots crowd the page edge.

Warning
theme() calls are order-sensitive. Later calls overwrite earlier ones on the SAME element. Combine all overrides into one theme() call, or place theme() AFTER theme_minimal() so your overrides win.

theme() vs theme_minimal() vs theme_classic() vs theme_bw()

theme() is the low-level customizer; theme_*() functions are pre-built bundles. Apply a theme_*() first for a complete look, then layer theme() on top to fine-tune.

Function Background Gridlines Best for
theme_grey() Grey White major + minor ggplot2 default
theme_minimal() White Light grey, no borders Reports, dashboards
theme_classic() White None, axis lines only Scientific papers
theme_bw() White Light grey, full border Publication figures
theme_light() White Light grey, light border Slides, presentations
theme_void() White None, no axes Maps, sparklines

Combine a base theme with theme() overrides for the cleanest result:

RMinimal base + custom overrides
ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) + geom_point(size = 3) + theme_minimal() + theme( plot.title = element_text(face = "bold"), legend.position = "top", panel.grid.minor = element_blank() ) + labs(title = "Fuel economy by weight")

  

The four element_*() helpers

Every theme() argument expects one of four element_*() helpers, or element_blank() to hide the element. Pick the helper that matches the element type: text, line, or filled rectangle.

Helper Controls Common arguments
element_text() Text elements (titles, labels, ticks) size, face, color, family, angle, hjust, vjust
element_line() Line elements (axes, grids, ticks) color, linewidth, linetype, lineend
element_rect() Filled rectangles (backgrounds, borders) fill, color, linewidth, linetype
element_blank() Hide the element completely none
Note
element_blank() removes an element from rendering, but the layout space MAY still be reserved. For axis titles and ticks, use element_blank() to fully reclaim the space. For panel borders, the gap stays unless you also drop the background.

Build a reusable custom theme

Wrap your theme() overrides in a function for consistent branding across many plots. Call the function on every chart to inherit the same look.

RCustom theme function
theme_brand <- function(base_size = 12) { theme_minimal(base_size = base_size) + theme( plot.title = element_text(face = "bold", size = base_size + 4), plot.subtitle = element_text(color = "grey40"), panel.grid.minor = element_blank(), legend.position = "top", strip.text = element_text(face = "bold") ) } p + theme_brand()

  

Now every plot can call theme_brand() and inherit the same look. Pass base_size to scale everything for slides vs reports.

Common pitfalls

Pitfall 1: Calling theme() before the base theme. Putting theme(plot.title = ...) BEFORE theme_minimal() lets theme_minimal() overwrite your changes. Always put theme() last.

Pitfall 2: Forgetting fill = NA on element_rect() for borders. Without it, the rectangle fills the entire panel and hides your data.

Pitfall 3: Wrong element name. axis.title.x exists; axis.title.X does not. Element names are case-sensitive and lowercase. Use ?theme to see the full list.

Pitfall 4: Using element_blank() when you want transparent. element_blank() removes the element; for a transparent fill, use element_rect(fill = "transparent", color = NA).

Try it yourself

Try it: Apply theme_minimal() to the mtcars scatter plot, then make the title bold and remove all minor gridlines. Save to ex_plot.

RYour turn: minimal plus overrides
ex_plot <- ggplot(mtcars, aes(wt, mpg)) + geom_point() + labs(title = "MPG vs Weight") + # your code here ex_plot

  
Click to reveal solution
RSolution
ex_plot <- ggplot(mtcars, aes(wt, mpg)) + geom_point() + labs(title = "MPG vs Weight") + theme_minimal() + theme( plot.title = element_text(face = "bold"), panel.grid.minor = element_blank() ) ex_plot

  

Explanation: theme_minimal() sets the base look; the subsequent theme() overrides the title face and removes the minor grid. Always place custom theme() after the base.

After mastering theme(), explore these companions:

  • theme_minimal(), theme_classic(), theme_bw(): pre-built theme bundles
  • element_text(), element_line(), element_rect(), element_blank(): element constructors
  • labs(): control plot labels (title, subtitle, caption, axis names)
  • guides(): customize legend guides (colorbar, key labels)
  • margin(): set plot, panel, and text margins
  • ggthemes package: extra theme bundles (theme_economist, theme_wsj, theme_fivethirtyeight)

FAQ

What is the difference between theme() and theme_minimal()?

theme() is the low-level customizer that changes individual elements. theme_minimal() is a complete pre-built theme bundle. You typically apply a base like theme_minimal() first, then layer theme() on top to override specific elements (title, legend position, gridlines).

How do I remove gridlines in ggplot2?

Use theme(panel.grid = element_blank()) to remove ALL gridlines, or target specific ones: panel.grid.major.x, panel.grid.minor.y, etc. Combine with axis.line = element_line() if you want axis lines to remain visible.

Why does my theme() get ignored?

Two common causes: (1) you placed theme() BEFORE a theme_*() call, so the base theme overwrote your changes; (2) the element name is misspelled or capitalized incorrectly. Run ?theme for the full canonical element list.

Can I save a custom theme for reuse?

Yes. Wrap your theme_minimal() + theme(...) block in an R function that returns the combined theme object. Apply it with + theme_brand() to any plot, just like a built-in theme. This is the standard pattern for organizations enforcing brand-consistent figures.

How do I change font in ggplot2 theme?

Use the family argument inside element_text(): theme(text = element_text(family = "Arial")). To use a custom font, register it first with extrafont::font_import() or showtext::font_add_google(), otherwise R falls back to its default sans-serif.