Skip to main content
Cell merging combines adjacent cells so their content is displayed in a single larger cell. flextable tracks merge state internally using span matrices, so text and formatting applied before or after merging are both preserved in the visible (top-left) cell.

When to merge

  • Grouped rows: merge the category column vertically when consecutive rows share the same value.
  • Spanning headers: merge header cells that describe a group of columns.
  • Summary rows: merge across all columns in a total or note row.

Merge horizontally — consecutive identical cells

merge_h() scans each selected row and merges runs of consecutive cells that display identical text:
library(flextable)

schedule <- data.frame(
  time      = c("9h", "10h", "11h", "14h", "15h", "16h"),
  monday    = c("Math",    "Math",  "French",  "History", "Science", "French"),
  tuesday   = c("English", "Math",  "Art",     "Math",    "Math",    "French"),
  wednesday = c("Science", "Math",  "Science", "English", "English", "French"),
  stringsAsFactors = FALSE
)

ft <- flextable(schedule)
ft <- theme_box(ft)
ft <- merge_h(ft)
ft
Use the i selector to restrict merging to specific rows:
ft <- merge_h(ft, i = 1:3)
The part argument defaults to "body". Pass part = "header" to merge header rows:
ft <- merge_h(ft, part = "header")

Merge vertically — consecutive identical cells

merge_v() scans each selected column and merges runs of consecutive cells that display identical text:
ft <- flextable(mtcars)
ft <- merge_v(ft, j = c("gear", "carb"))
ft

Merging based on a key column

The target argument lets you drive merges in multiple columns using the grouping of a single key column:
data_ex <- data.frame(
  srdr_id   = c("175124", "175124", "172525", "172525"),
  substances = c("alcohol", "alcohol", "alcohol", "alcohol"),
  full_name  = c("TAU", "MI", "TAU", "MI (parent)"),
  stringsAsFactors = FALSE
)

ft <- flextable(data_ex)
ft <- theme_box(ft)
ft <- merge_v(
  ft,
  j      = "srdr_id",
  target = c("srdr_id", "substances")
)
ft
Here j = "srdr_id" determines where row runs start and end, but the merge is also applied to the "substances" column.

Combined column matching

Set combine = TRUE to base the grouping on the concatenated values of all columns in j rather than inspecting each column independently:
ft <- merge_v(ft, j = c("gear", "carb"), combine = TRUE)

Merge a specific rectangle of cells

merge_at() merges an arbitrary contiguous block defined by row and column selectors. All rows and columns must be consecutive:
ft <- flextable(head(mtcars), cwidth = .5)
ft <- merge_at(ft, i = 1:2, j = 1:2)
ft
This is useful when you want to create a single large cell without relying on value matching.

Merge a column range within specific rows

merge_h_range() merges a contiguous range of columns — from j1 to j2 — for every selected row. Unlike merge_h(), it does not require cell values to match:
ft <- flextable(head(mtcars), cwidth = .5)
ft <- theme_box(ft)
ft <- merge_h_range(ft, i = ~ cyl == 6, j1 = "am", j2 = "carb")
ft <- align(ft, i = ~ cyl == 6, align = "center")
ft

Remove all merges

merge_none() resets all span information back to no merging. By default it applies to all parts:
ft <- merge_none(ft)
Pass part to target only one section:
ft <- merge_none(ft, part = "body")

Merging and formatting

Merging affects which cell holds the visible content, but it does not change the underlying data or the styles already applied. After merging:
  • The content and style of the top-left cell in the merged region is displayed.
  • Styles applied after merging via bg(), color(), etc. target cells by their original row/column positions — the merge simply hides the non-primary cells.
  • Use fix_border_issues() after merging if borders look inconsistent; merging can expose shared borders that need reconciliation.
ft <- flextable(head(iris))
ft <- set_header_df(
  ft,
  mapping = data.frame(
    col_keys = c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width", "Species"),
    what     = c("Sepal",  "Sepal",  "Petal",  "Petal",  "Species"),
    measure  = c("Length", "Width", "Length", "Width", "Species"),
    stringsAsFactors = FALSE
  ),
  key = "col_keys"
)
ft <- merge_h(ft, part = "header")
ft <- merge_v(ft, j = "Species", part = "header")
ft <- theme_vanilla(ft)
ft <- fix_border_issues(ft)
ft

Multi-level headers with add_header_row()

add_header_row() creates spanning cells directly when you pass colwidths, so you do not need to call any merge function for those rows:
ft <- flextable(head(airquality))
ft <- add_header_row(
  ft,
  values    = c("Air quality", "Date"),
  colwidths = c(4, 2),
  top       = TRUE
)
# The 'Air quality' label already spans 4 columns —
# no additional merge_h() call needed.
ft <- theme_box(ft)
ft
Use merge_h() and merge_v() on header rows only when you built the header with set_header_df() or add_header() and identical labels need to be collapsed.