You could use quarto or RMarkdown to compose a document that can be rendered as HTML or DOCX. Below is an example quarto file that renders to HTML. I tested DOCX too, but the sjPlot::sjt.xtab() tables do not show up in DOCX. Maybe with some tweaking that can be fixed. Tables created with the gt, gtsummary or flextable packages usually work well across different quarto output formats.
---
title: "Multi Output"
format: html
execute:
echo: false
warning: false
---
```{r}
data <- data.frame(
group = sample(LETTERS[1:3], 50, replace = T),
var_1 = sample(c("Yes", "No"), 50, replace = T),
var_2 = sample(c("Low","Mid","High"), 50, replace = T),
var_3 = sample(c("Yes","No","Don't know"), 50, replace = T))
```
## Table 1
```{r}
sjPlot::sjt.xtab(data$group, data$var_1, file="output.html")
```
## Table 2
```{r}
sjPlot::sjt.xtab(data$group, data$var_1, file="output.html")
```

To programmatically create any number of tables (aka looping) you can utilize purrr::map()/purrr::walk() to iterate over the columns in your dataframe. In the example below we first create an html file for each table in a subfolder ("figures"). We then use quarto inlcudes to integrate these into our quarto document. If you were to create your tables with gt or gtsummary this extra step of creating the individual html files for the tables would not be necessary.
---
title: "Multi Output Programmatic"
format: html
execute:
echo: false
warning: false
---
```{r}
library(tidyverse)
```
```{r}
data <- data.frame(
group = sample(LETTERS[1:3], 50, replace = T),
var_1 = sample(c("Yes", "No"), 50, replace = T),
var_2 = sample(c("Low","Mid","High"), 50, replace = T),
var_3 = sample(c("Yes","No","Don't know"), 50, replace = T))
```
## Tables
```{r}
#| include: false
dir.create("figures")
var_names <- names(data)[-1]
map(var_names,
\(x) sjPlot::sjt.xtab(data$group, data[[x]], file = paste0("figures/", x, ".html")) )
```
```{r}
#| output: asis
walk(var_names, \(x) {
cat("##", x, "\n\n")
cat(paste0("{{< include figures/", x, ".html >}}\n\n"))
})
```
