dplyr::bind_cols

bind_cols(..., .name_repair = "unique")
Returns: data.frame · Updated April 18, 2026 · Tidyverse
dplyr data-wrangling combine bind

bind_cols() adds columns from multiple data frames side by side. It matches rows by their position in each data frame, not by any key column. This makes it fast and simple for data that is already aligned, but dangerous for data that might be in different orders.

Where possible, prefer a join by key over bind_cols(). Joins match rows explicitly, so you catch misalignments instead of silently producing garbage.

Syntax

bind_cols(..., .name_repair = "unique")

Parameters

ParameterTypeDefaultDescription
...data framesrequiredData frames to combine. Each can be a data frame, a list of data frames, or a list that could be a data frame. Shorter inputs are recycled to match the longest.
.name_repairstring"unique"How to handle duplicate column names. One of "unique" (make unique), "universal" (R-compatible unique), "check_unique" (error on conflict), or "minimal" (no repair).

Basic Usage

Join two data frames column-wise:

library(dplyr)

df1 <- tibble(name = c("Alice", "Bob", "Charlie"))
df2 <- tibble(score = c(85, 92, 78), grade = c("B", "A", "C"))

bind_cols(df1, df2)

# # A tibble: 3 × 3
#   name    score grade
#   <chr>   <dbl> <chr>
# 1 Alice      85 B
# 2 Bob        92 A
# 3 Charlie    78 C

Rows are matched purely by position. The first row of df1 combines with the first row of df2, and so on.

The Positional Matching Gotcha

This is the most important thing to understand about bind_cols(). It does not check that your rows align by any key. If df1 and df2 are in different orders, you’ll get wrong results with no warning:

df1 <- tibble(name = c("Alice", "Bob", "Charlie"))
df2 <- tibble(score = c(92, 85, 78))  # Bob and Alice swapped!

bind_cols(df1, df2)
# Alice gets Bob's score. No error. No warning.

Always sort your data before binding, or use a join instead:

# Safe: join by name instead
df1 <- tibble(name = c("Alice", "Bob"), score = c(85, 92))
df2 <- tibble(name = c("Bob", "Alice"), grade = c("A", "B"))

df1 |> left_join(df2, by = "name")

Supplying Multiple Data Frames

You can bind any number of data frames in one call:

df1 <- tibble(x = 1:3)
df2 <- tibble(y = 4:6)
df3 <- tibble(z = 7:9)

bind_cols(df1, df2, df3)

# # A tibble: 3 × 3
#       x     y     z
#   <int> <int> <int>
# 1     1     4     7
# 2     2     5     8
# 3     3     6     9

Combining a List of Data Frames

Pass a list of data frames with !!! (splice) or directly:

df_list <- list(
  tibble(a = 1:3),
  tibble(b = 4:6),
  tibble(c = 7:9)
)

bind_cols(!!!df_list)

# # A tibble: 3 × 3
#       a     b     c
#   <int> <int> <int>
# 1     1     4     7
# 2     2     5     8
# 3     3     6     9

Row Size Mismatch

If the data frames have incompatible row counts, bind_cols() throws an error:

df1 <- tibble(x = 1:3)
df2 <- tibble(y = 1:2)

bind_cols(df1, df2)
# Error: Can't recycle `..1` (size 3) to match `..2` (size 2).

Recycling handles simple cases where one data frame is shorter:

df1 <- tibble(x = 1:3)
df2 <- tibble(y = c(1, 2))  # recycled to c(1, 2, 1)

bind_cols(df1, df2)
# # A tibble: 3 × 2
#       x     y
#   <int> <dbl>
# 1     1     1
# 2     2     2
# 3     3     1

This recycling is convenient but risky. A length-1 vector is safe for constants; otherwise, watch out.

Column Name Repair

The .name_repair parameter controls what happens when column names collide:

df1 <- tibble(x = 1:3)
df2 <- tibble(x = 4:6)

bind_cols(df1, df2, .name_repair = "unique")
# # A tibble: 3 × 2
#       x x...1 x...2
#   <int> <int> <int>

bind_cols(df1, df2, .name_repair = "check_unique")
# Error: Names must be unique.

Common Patterns

Adding metadata columns

df <- tibble(name = c("Alice", "Bob"), score = c(85, 92))

bind_cols(df, tibble(
  date = Sys.Date(),
  source = "exam"
))

# # A tibble: 2 × 4
#   name    score date       source
#   <chr>   <dbl> <date>     <chr>
# 1 Alice      85 2026-04-18 exam
# 2 Bob        92 2026-04-18 exam

Building a wide dataset incrementally

part1 <- tibble(id = 1:3, x = c("a", "b", "c"))
part2 <- tibble(y = c(1, 2, 3))
part3 <- tibble(z = c(TRUE, FALSE, TRUE))

bind_cols(part1, part2, part3)

See Also