Another possibility is to use mutate_all() instead of replace_na():
df1 %>%
rownames_to_column() %>%
left_join(df2 %>% rownames_to_column()) %>%
left_join(df3 %>% rownames_to_column()) %>%
select(-rowname) %>%
mutate_all(~ if_else(is.na(.), "", .))
# uses anonymous function syntax with '.' as variable: ~ f(.)
and if df1 and df2 are already tibbles, the analogue would be:
df1 %>% mutate(rowname = row_number()) %>%
# since tibbles don't allow row names
left_join(df2 %>% mutate(rowname = row_number())) %>%
left_join(df3 %>% mutate(rowname = row_number())) %>%
select(-rowname) %>%
mutate_if(is.character, ~ if_else(is.na(.), "", .))
# since tibbles require uniform column data types, "" can only appear
# in character columns