Create PowerPoint Presentations from R with officer

Generate .pptx slides — titles, bullet text, tables, and plots — from R

Programming

Learn to create PowerPoint presentations from R with the officer package — add slides with titles, bullet text, tables via flextable, and ggplot2 charts, and generate a .pptx deck programmatically. The modern replacement for the retired ReporteRs.

Author
Published

July 8, 2026

Modified

July 9, 2026

TipWhat you’ll learn
  • Build a PowerPoint deck entirely in R with officerread_pptx()add_slide()ph_with()print().
  • Fill each slide with the four things that matter: a title, bullet text, a table (via flextable), and a ggplot2 chart.
  • Keep charts editable in PowerPoint (true vector) with rvg::dml(), or embed them as a crisp image.
  • Start from a corporate template so every deck lands on-brand — fonts, colours, logo.
  • Why officer is the modern replacement for the retired ReporteRs package.

You have the analysis in R and the audience lives in PowerPoint. Re-drawing every chart by hand into .pptx is the slow, error-prone part — and the moment the data changes, you do it all again. officer closes that gap: it writes real Microsoft PowerPoint files programmatically, so a single R script produces a finished deck you can re-run whenever the numbers move.

If you learned this workflow years ago with ReporteRs, that package is gone (it depended on Java and was archived from CRAN). officer is its spiritual successor — no Java, pure R, actively maintained, and it pairs naturally with R Markdown / Quarto Office reporting via officedown. This post is the practical, copy-paste recipe.

What officer produces

Here is a single slide built from R — a title placeholder and a ggplot2 chart dropped into the content placeholder. The R below is exactly what generated it; the only thing hidden is the step that rasterises the finished .pptx to an image so we can show it inline.

library(officer)
library(ggplot2)

# A self-contained chart: mean tooth length by dose, grouped by supplement
tg <- aggregate(len ~ dose + supp, data = ToothGrowth, FUN = mean)
tg$dose <- factor(tg$dose)

p <- ggplot(tg, aes(x = dose, y = len, fill = supp)) +
  geom_col(position = "dodge") +
  scale_fill_viridis_d(end = 0.85) +
  labs(x = "Dose (mg/day)", y = "Mean length", fill = "Supplement") +
  theme_minimal(base_size = 13)

# Build the slide: start a deck, add a slide, fill the placeholders
doc <- read_pptx()
doc <- add_slide(doc, layout = "Title and Content", master = "Office Theme")
doc <- ph_with(doc, value = "Mean tooth length by dose",
               location = ph_location_type(type = "title"))
doc <- ph_with(doc, value = p,
               location = ph_location_type(type = "body"))

print(doc, target = "cover.pptx")   # write the real .pptx file

A PowerPoint slide generated from R with officer: the title 'Mean tooth length by dose' above a grouped bar chart coloured with the viridis palette.

That is a genuine .pptx slide — open it in PowerPoint and the title is editable text, the chart is a real picture on the slide. Everything below breaks that pattern into its pieces.

From ReporteRs to officer: what changed

If you are searching for ReporteRs and hitting dead links, here is the one-line map. The whole doc <- pptx(); addSlide(); addTitle(); writeDoc() API is retired. The officer equivalents are:

ReporteRs (dead) officer (use this)
pptx() read_pptx()
addSlide(doc, layout) add_slide(doc, layout, master)
addTitle() / addParagraph() ph_with(doc, value, location = ...)
addFlexTable(FlexTable(x)) ph_with(doc, flextable(x), ...)
addPlot(doc, fun) ph_with(doc, gg_or_dml, ...)
writeDoc(doc, file) print(doc, target = file)

The mental model is simpler than the old one: you don’t call a different function per content type — you call ph_with() and hand it whatever you want (text, a bullet list, a flextable, a plot), telling it where to land with a ph_location_*() helper.

Install and load officer

You need three packages: officer for the document, flextable for tables, and rvg for editable vector charts.

install.packages(c("officer", "flextable", "rvg"))
library(officer)
library(flextable)
library(rvg)

Build a deck in four steps

Every officer PowerPoint script is the same four moves. Start a deck from the default template with read_pptx(), add a slide with add_slide() (naming a layout and its master), fill the slide’s placeholders with ph_with(), and write the file with print(). Here is the smallest complete deck:

library(officer)

doc <- read_pptx()                                    # 1. start from the default template
doc <- add_slide(doc, layout = "Title and Content",   # 2. add a slide
                 master = "Office Theme")
doc <- ph_with(doc, value = "Hello from R",           # 3. fill the title placeholder
               location = ph_location_type(type = "title"))
doc <- ph_with(doc, value = "Built entirely with officer",
               location = ph_location_type(type = "body"))
print(doc, target = "hello.pptx")                     # 4. write the .pptx

Two ideas do the heavy lifting. A layout is a slide template (“Title Slide”, “Title and Content”, “Two Content”…); it decides which placeholders exist. A placeholder locationph_location_type("title"), ph_location_type("body") — tells ph_with() which box to drop content into. Match the layout to the content you have, and the placeholders line themselves up.

See which layouts your template offers

Layouts come from the template, so list them before you write. layout_summary() returns every layout/master pair you can pass to add_slide():

layout_summary(read_pptx())
             layout       master
1       Title Slide Office Theme
2 Title and Content Office Theme
3    Section Header Office Theme
4       Two Content Office Theme
5        Comparison Office Theme
6        Title Only Office Theme
7             Blank Office Theme

The default Office theme ships the familiar set — a Title Slide for the cover, Title and Content for a single chart or table, Two Content for a side-by-side, and Section Header to break up a long deck. Use only names that appear in this list; a typo silently fails to place your content.

Add a title slide

A cover slide uses the Title Slide layout, which exposes two special placeholders: ctrTitle (the big centred title) and subTitle. Address them by type just like the body:

library(officer)

doc <- read_pptx()
doc <- add_slide(doc, layout = "Title Slide", master = "Office Theme")
doc <- ph_with(doc, value = "Quarterly Results",
               location = ph_location_type(type = "ctrTitle"))
doc <- ph_with(doc, value = "Generated from R with officer",
               location = ph_location_type(type = "subTitle"))
print(doc, target = "title.pptx")

Add bullet text (ordered and unordered lists)

Plain text goes in as a character string. For multi-level bullets, wrap your items in unordered_list() and give each a level with level_list — level 1 is a top bullet, level 2 an indent. This replaces the old addParagraph(..., par.properties = ...) dance:

library(officer)

bullets <- unordered_list(
  str_list = c("Revenue up 12% YoY",
               "Driven by the EMEA region",
               "APAC flat",
               "New product line launches Q3"),
  level_list = c(1, 2, 2, 1)
)

doc <- read_pptx()
doc <- add_slide(doc, layout = "Title and Content", master = "Office Theme")
doc <- ph_with(doc, value = "Highlights", location = ph_location_type(type = "title"))
doc <- ph_with(doc, value = bullets, location = ph_location_type(type = "body"))
print(doc, target = "bullets.pptx")

The two indented items sit under “Revenue up 12% YoY” as sub-bullets; the last item returns to the top level. Swap unordered_list() for a plain block_list() of fpar() paragraphs when you need numbered lists or mixed formatting.

Add a table with flextable

flextable is the companion package for tables — it builds a styled table object that ph_with() drops straight onto a slide. Create the table, theme it, then place it in the content placeholder:

library(officer)
library(flextable)

ft <- flextable(head(iris, 5))
ft <- theme_vanilla(ft)         # clean, presentation-ready styling
ft <- autofit(ft)               # size columns to their content

doc <- read_pptx()
doc <- add_slide(doc, layout = "Title and Content", master = "Office Theme")
doc <- ph_with(doc, value = "First rows of iris", location = ph_location_type(type = "title"))
doc <- ph_with(doc, value = ft, location = ph_location_type(type = "body"))
print(doc, target = "table.pptx")

flextable gives you full control — bg() for zebra striping, bold() for headers, color() for conditional formatting — and the same table object also renders to Word, HTML, and PDF, so one table definition serves every output.

Add a ggplot2 chart — image or editable vector

You have two ways to put a plot on a slide, and the choice matters. Hand ph_with() the ggplot object directly and officer embeds it as a high-resolution image — simple and pixel-perfect. Wrap it in rvg::dml() instead and the chart becomes native, editable PowerPoint vector graphics — your colleague can click a bar and recolour it, nudge a label, or restyle it to the house template.

library(officer)
library(rvg)
library(ggplot2)

tg <- aggregate(len ~ dose + supp, data = ToothGrowth, FUN = mean)
tg$dose <- factor(tg$dose)
p <- ggplot(tg, aes(x = dose, y = len, fill = supp)) +
  geom_col(position = "dodge") +
  scale_fill_viridis_d(end = 0.85) +
  labs(x = "Dose (mg/day)", y = "Mean length", fill = "Supplement") +
  theme_minimal(base_size = 13)

doc <- read_pptx()

# Slide 1 — plot embedded as an image (simplest)
doc <- add_slide(doc, layout = "Title and Content", master = "Office Theme")
doc <- ph_with(doc, value = "Chart as an image", location = ph_location_type(type = "title"))
doc <- ph_with(doc, value = p, location = ph_location_type(type = "body"))

# Slide 2 — same plot as editable vector graphics
doc <- add_slide(doc, layout = "Title and Content", master = "Office Theme")
doc <- ph_with(doc, value = "Chart as editable vector", location = ph_location_type(type = "title"))
doc <- ph_with(doc, value = dml(ggobj = p), location = ph_location_type(type = "body"))

print(doc, target = "charts.pptx")

This is exactly the problem the old ReporteRs tutorials were chasing — keeping beautiful R graphs beautiful in PowerPoint. rvg::dml() is the answer: no blurry screenshot, no re-drawing, a vector chart that stays crisp at any zoom and editable in the meeting.

Put it together: a complete deck

Because read_pptx()add_slide()ph_with() returns the document each time, you chain slides in one pipeline. Here is a four-slide deck — cover, bullets, table, chart — from a single script:

library(officer)
library(flextable)
library(rvg)
library(ggplot2)

tg <- aggregate(len ~ dose + supp, data = ToothGrowth, FUN = mean)
tg$dose <- factor(tg$dose)
p <- ggplot(tg, aes(x = dose, y = len, fill = supp)) +
  geom_col(position = "dodge") +
  scale_fill_viridis_d(end = 0.85) +
  labs(x = "Dose (mg/day)", y = "Mean length", fill = "Supplement") +
  theme_minimal(base_size = 13)

doc <- read_pptx()

# 1. Title slide
doc <- add_slide(doc, layout = "Title Slide", master = "Office Theme")
doc <- ph_with(doc, "ToothGrowth Report", ph_location_type(type = "ctrTitle"))
doc <- ph_with(doc, "Generated from R with officer", ph_location_type(type = "subTitle"))

# 2. Bullet highlights
doc <- add_slide(doc, layout = "Title and Content", master = "Office Theme")
doc <- ph_with(doc, "Highlights", ph_location_type(type = "title"))
doc <- ph_with(doc,
  unordered_list(str_list = c("Higher dose → longer teeth",
                              "Orange juice leads at low doses"),
                 level_list = c(1, 1)),
  ph_location_type(type = "body"))

# 3. Data table
doc <- add_slide(doc, layout = "Title and Content", master = "Office Theme")
doc <- ph_with(doc, "Group means", ph_location_type(type = "title"))
doc <- ph_with(doc, autofit(theme_vanilla(flextable(tg))), ph_location_type(type = "body"))

# 4. Chart (editable vector)
doc <- add_slide(doc, layout = "Title and Content", master = "Office Theme")
doc <- ph_with(doc, "Mean length by dose", ph_location_type(type = "title"))
doc <- ph_with(doc, dml(ggobj = p), ph_location_type(type = "body"))

print(doc, target = "report.pptx")

Re-run that script tomorrow with new data and you get a fresh, correct deck in seconds — the reason to generate slides from code in the first place.

Start from a corporate template

Most teams need decks on a house template — set fonts, colours, and a logo. Point read_pptx() at an existing .pptx and officer inherits its layouts, masters, and styling; every slide you add lands on-brand:

doc <- read_pptx(path = "company-template.pptx")
layout_summary(doc)          # inspect the template's own layout names first

doc <- add_slide(doc, layout = "Title and Content", master = "Office Theme")
doc <- ph_with(doc, value = "On-brand slide", location = ph_location_type(type = "title"))
print(doc, target = "branded.pptx")

Always run layout_summary() on a template before you write — corporate templates rename their layouts and masters, and add_slide() needs the exact names.

Frequently asked questions

Yes. ReporteRs was archived (it relied on Java and is no longer maintained). officer, by the same author, is its modern successor — pure R, no Java, actively developed. The API changed: use read_pptx() instead of pptx(), add_slide() instead of addSlide(), ph_with() for all content, and print(doc, target = ...) instead of writeDoc().

Wrap the plot in rvg::dml(): ph_with(doc, value = dml(ggobj = p), location = ph_location_type(type = "body")). That embeds the chart as native PowerPoint DrawingML (vector) rather than a flat image, so it stays crisp at any zoom and can be edited in PowerPoint. Passing the ggplot object directly instead (value = p) embeds a high-resolution image — simpler, but not editable.

Yes — pass it to read_pptx(path = "template.pptx"). officer inherits the template’s slide layouts, masters, fonts, colours, and logo. Run layout_summary() first to see the exact layout and master names the template defines, then reference those names in add_slide().

Build the table with the flextable package, then place it with ph_with(): ph_with(doc, value = flextable(my_data), location = ph_location_type(type = "body")). Style it first with helpers like theme_vanilla(), autofit(), bg(), and bold(). The same flextable object also renders to Word, HTML, and PDF.

Whatever the template provides. Call layout_summary(read_pptx()) to list them. The default Office theme includes “Title Slide”, “Title and Content”, “Two Content”, “Section Header”, “Comparison”, “Title Only”, and “Blank”, among others. The layout you pick determines which placeholders exist on the slide.

Going deeper in /learn

This post is the focused recipe. For the full picture of moving data and results in and out of R — CSV, Excel, RDS, Parquet, and formatted Office reports — see the lesson:

Citation

BibTeX citation:
@online{kassambara2026,
  author = {Kassambara, Alboukadel},
  title = {Create {PowerPoint} {Presentations} from {R} with Officer},
  date = {2026-07-08},
  url = {https://www.datanovia.com/blog/powerpoint-in-r-officer},
  langid = {en}
}
For attribution, please cite this work as:
Kassambara, Alboukadel. 2026. “Create PowerPoint Presentations from R with Officer.” July 8. https://www.datanovia.com/blog/powerpoint-in-r-officer.