This guide covers per-cell styling, which allows you to apply different formatting to individual cells based on data values.

TipThe Styling Hierarchy

tabviz applies styles in order of specificity:

  1. Theme defaults (base colors, fonts)
  2. Row styles (row_bold, row_color) - apply to entire rows
  3. Cell styles (col_*(bold = ...)) - override for specific cells

More specific styles override less specific ones. This lets you set row-wide defaults and override individual cells.

Row vs Cell Styling

tabviz offers two levels of styling:

Level Parameters Scope Use When
Row row_bold, row_color, row_indent, row_badge Entire row Headers, summaries, highlighting rows
Cell bold, color, bg, badge, icon on columns Specific cells Conditional formatting, significance

Cell styling takes precedence over row styling when both are specified.

Semantic Row Styling

For quick conditional formatting, use semantic styling classes that apply theme-appropriate styles:

Parameter Effect Use For
row_emphasis Bold text, darker color Key results, primary endpoints
row_muted Lighter color, reduced prominence Secondary results, supporting data
row_accent Theme accent color Highlighted findings, special rows
Code
# Add semantic styling columns to your data
data <- data |>
  mutate(
    is_primary = endpoint == "Primary",
    is_secondary = grepl("Secondary", endpoint)
  )

tabviz(data, ...,
  row_emphasis = "is_primary",
  row_muted = "is_secondary"
)

These semantic styles use theme colors automatically, so they adapt when switching themes.

Cell Style Parameters

Every col_*() function accepts these styling parameters:

Parameter Type Description
bold Column name or formula Bold text when TRUE
italic Column name or formula Italic text when TRUE
color Column name or formula Text color
bg Column name or formula Background color
badge Column name or formula Badge label
icon Column name or formula Emoji/unicode icon
emphasis Column name or formula Theme-aware emphasis (bold + foreground)
muted Column name or formula Theme-aware muted styling
accent Column name or formula Theme-aware accent styling

Each parameter accepts either:

  • A column name (string): The values in that column control styling per row
  • A formula expression (~ ...): Evaluated on the fly to compute styling

Formula Expressions for Cell Styling

Instead of pre-computing style columns, you can use formulas that reference your data. For cell-level styling, use .x to refer to the column’s own values:

Code
# Data without any pre-computed style columns
pval_demo <- data.frame(
  study = c("Trial A", "Trial B", "Trial C", "Trial D"),
  hr = c(0.72, 0.85, 0.91, 0.68),
  lower = c(0.55, 0.70, 0.75, 0.52),
  upper = c(0.95, 1.03, 1.10, 0.89),
  pval = c(0.001, 0.1, 0.3, 0.02)
)

tabviz(pval_demo,
  label = "study",
  columns = list(
    col_pvalue("pval",
      # .x refers to the pval column values
      bold = ~ .x < 0.05,
      color = ~ ifelse(.x < 0.05, "#16a34a", "#71717a")
    ),
    viz_forest(point = "hr", lower = "lower", upper = "upper",
               scale = "log", null_value = 1)
  )
)

The .x Pronoun

When using formulas in col_*() functions:

  • .x refers to the current column’s values
  • You can also reference other columns by name
Code
# .x is the column's own values
col_pvalue("pval", bold = ~ .x < 0.05)

# You can also reference other columns
col_numeric("hr", color = ~ ifelse(pval < 0.05, "green", "gray"))

When to Use Formulas vs Columns

Approach Best For
Formulas (~ .x < 0.05) Simple conditions, self-referential logic, one-off styling
Column names ("is_sig") Complex logic computed separately, reused across plots
TipFormula Advantage

Formulas keep styling logic close to where it’s used, making your code more readable:

# Without formulas - need to pre-compute
data <- data |> mutate(pval_bold = pval < 0.05)
col_pvalue("pval", bold = "pval_bold")

# With formulas - logic inline
col_pvalue("pval", bold = ~ .x < 0.05)

Basic Example

Code
# Data with styling columns
data <- data.frame(
  study = c("ACCORD", "ADVANCE", "SPRINT", "ONTARGET"),
  hr = c(0.65, 1.15, 0.82, 0.98),
  lower = c(0.50, 0.95, 0.68, 0.85),
  upper = c(0.85, 1.40, 0.99, 1.14),
  pval = c(0.002, 0.18, 0.04, 0.62),
  # Styling columns (computed from data)
  sig_bold = c(TRUE, FALSE, TRUE, FALSE),          # Bold if p < 0.05
  dir_color = c("#22c55e", "#ef4444", "#22c55e", "#71717a")  # Green/red/gray
)

tabviz(data,
  label = "study",
  columns = list(
    col_numeric("hr", "HR", color = "dir_color", bold = "sig_bold"),
    col_pvalue("pval", bold = "sig_bold"),
    viz_forest(point = "hr", lower = "lower", upper = "upper",
               scale = "log", null_value = 1)
  )
)

Conditional Formatting Patterns

Color by Effect Direction

Code
styled <- data.frame(
  study = c("Trial A", "Trial B", "Trial C", "Trial D"),
  hr = c(0.72, 1.25, 0.88, 1.05),
  lower = c(0.58, 1.02, 0.74, 0.92),
  upper = c(0.89, 1.53, 1.05, 1.20)
) |>
  mutate(
    # Determine direction and significance
    effect_color = case_when(
      upper < 1 ~ "#16a34a",  # Significant benefit (green)
      lower > 1 ~ "#dc2626",  # Significant harm (red)
      TRUE ~ "#71717a"        # Non-significant (gray)
    )
  )

tabviz(styled,
  label = "study",
  columns = list(
    col_interval(point = "hr", lower = "lower", upper = "upper",
                 color = "effect_color"),
    viz_forest(point = "hr", lower = "lower", upper = "upper",
               scale = "log", null_value = 1)
  )
)

Highlight Significant Results

Code
trials <- data.frame(
  study = c("EMPA-REG", "CANVAS", "DECLARE", "CREDENCE", "DAPA-HF"),
  hr = c(0.62, 0.86, 0.93, 0.70, 0.74),
  lower = c(0.49, 0.75, 0.84, 0.59, 0.65),
  upper = c(0.77, 0.97, 1.03, 0.82, 0.85),
  pval = c(0.0001, 0.02, 0.17, 0.0001, 0.0001)
) |>
  mutate(
    is_sig = pval < 0.05,
    sig_bg = if_else(is_sig, "#fef3c7", NA_character_),  # Yellow highlight
    sig_badge = if_else(pval < 0.001, "***", if_else(pval < 0.01, "**", if_else(pval < 0.05, "*", NA_character_)))
  )

tabviz(trials,
  label = "study",
  columns = list(
    col_numeric("hr", "HR", bold = "is_sig", bg = "sig_bg"),
    col_pvalue("pval", badge = "sig_badge"),
    viz_forest(point = "hr", lower = "lower", upper = "upper",
               scale = "log", null_value = 1)
  )
)

Traffic Light Styling

Code
outcomes <- data.frame(
  endpoint = c("Primary", "CV Death", "HF Hosp", "All-cause Death"),
  hr = c(0.74, 0.82, 0.65, 0.88),
  lower = c(0.66, 0.72, 0.55, 0.76),
  upper = c(0.83, 0.94, 0.77, 1.02)
) |>
  mutate(
    status = case_when(
      upper < 1 ~ "benefit",
      lower > 1 ~ "harm",
      TRUE ~ "neutral"
    ),
    icon = case_when(
      status == "benefit" ~ "\u2705",   # Green check
      status == "harm" ~ "\u274C",      # Red X
      TRUE ~ "\u2796"                   # Dash
    ),
    text_color = case_when(
      status == "benefit" ~ "#16a34a",
      status == "harm" ~ "#dc2626",
      TRUE ~ "#525252"
    )
  )

tabviz(outcomes,
  label = "endpoint",
  columns = list(
    col_text("icon", " ", width = 30),
    col_numeric("hr", "HR", color = "text_color"),
    col_interval(point = "hr", lower = "lower", upper = "upper",
                 color = "text_color"),
    viz_forest(point = "hr", lower = "lower", upper = "upper",
               scale = "log", null_value = 1)
  )
)

Combining Row and Cell Styling

Row styling applies to the entire row, while cell styling allows overrides for specific columns:

Code
combined <- data.frame(
  label = c("Primary Endpoints", "  CV death or HF hosp", "  CV death",
            "Secondary Endpoints", "  All-cause mortality"),
  hr = c(NA, 0.74, 0.82, NA, 0.88),
  lower = c(NA, 0.66, 0.72, NA, 0.76),
  upper = c(NA, 0.83, 0.94, NA, 1.02),
  # Row styling
  rtype = c("header", "data", "data", "header", "data"),
  rbold = c(TRUE, TRUE, FALSE, TRUE, FALSE),
  rcolor = c("#2563eb", NA, NA, "#2563eb", NA),
  rbadge = c(NA, "Primary", NA, NA, NA),
  # Cell styling (overrides for specific cells)
  hr_color = c(NA, "#16a34a", "#16a34a", NA, "#71717a")
)

tabviz(combined,
  label = "label",
  row_type = "rtype", row_bold = "rbold", row_color = "rcolor", row_badge = "rbadge",
  columns = list(
    col_numeric("hr", "HR", color = "hr_color"),
    col_interval(point = "hr", lower = "lower", upper = "upper"),
    viz_forest(point = "hr", lower = "lower", upper = "upper",
               scale = "log", null_value = 1)
  )
)

Using set_column_style()

For fluent-style workflows, use set_column_style() after creating a WebSpec:

Code
# Create spec then add styling
tabviz(data,
  label = "study",
  columns = list(
    col_numeric("hr", "HR"),
    col_interval(point = "hr", lower = "lower", upper = "upper"),
    viz_forest(point = "hr", lower = "lower", upper = "upper",
               scale = "log", null_value = 1)
  ),
  .spec_only = TRUE
) |>
  set_column_style("hr", bold = "is_significant", color = "direction_color") |>
  set_column_style("_interval", badge = "sig_stars") |>
  tabviz()

dplyr Workflow for Style Columns

A typical workflow computes style columns with dplyr before plotting:

Code
meta_results <- data.frame(
  study = c("SHIFT", "PARADIGM-HF", "DAPA-HF", "EMPEROR-Reduced"),
  drug_class = c("Ivabradine", "ARNI", "SGLT2i", "SGLT2i"),
  hr = c(0.82, 0.80, 0.74, 0.75),
  lower = c(0.75, 0.73, 0.65, 0.65),
  upper = c(0.90, 0.87, 0.85, 0.86),
  pval = c(0.0001, 0.0001, 0.0001, 0.0001)
)

styled_results <- meta_results |>
  mutate(
    # Significance styling
    is_sig = pval < 0.05,
    sig_stars = case_when(
      pval < 0.001 ~ "***",
      pval < 0.01 ~ "**",
      pval < 0.05 ~ "*",
      TRUE ~ NA_character_
    ),
    # Effect direction
    effect_color = case_when(
      upper < 1 ~ "#16a34a",
      lower > 1 ~ "#dc2626",
      TRUE ~ "#71717a"
    ),
    # Drug class styling
    class_color = case_when(
      drug_class == "SGLT2i" ~ "#2563eb",
      drug_class == "ARNI" ~ "#7c3aed",
      TRUE ~ "#525252"
    )
  )

tabviz(styled_results,
  label = "study",
  columns = list(
    col_text("drug_class", "Class", color = "class_color"),
    col_numeric("hr", "HR", bold = "is_sig", color = "effect_color"),
    col_interval(point = "hr", lower = "lower", upper = "upper",
                 color = "effect_color"),
    col_pvalue("pval", badge = "sig_stars"),
    viz_forest(point = "hr", lower = "lower", upper = "upper",
               scale = "log", null_value = 1)
  )
)

Style Column Naming Conventions

Keep your data organized by using consistent naming:

Style Type Suggested Suffix Example
Bold _bold hr_bold, is_bold
Color _color effect_color, pval_color
Background _bg sig_bg, row_bg
Badge _badge sig_badge, status_badge
Icon _icon status_icon

This makes it clear which columns are data vs styling, and helps when debugging formatting issues.