# \[ANN\] PrettyTables.jl v3.5: One Table Specification, Six Back Ends

**URL:** <https://discourse.julialang.org/t/ann-prettytables-jl-v3-5-one-table-specification-six-back-ends/139129>\
**Category:** Package Announcements\
**Created:** [September 1, 2026, 1:39am UTC](https://discourse.julialang.org/t/ann-prettytables-jl-v3-5-one-table-specification-six-back-ends/139129 "2026-09-01T01:39:18Z")\
**Posts on this page:** 12\
**Page:** 1

<div class="post-metadata">

**Author:** ![Ronis\_BR](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ronis_br/32/50999_2.png) [@Ronis\_BR](https://discourse.julialang.org/u/Ronis_BR)\
**Post date:** [September 1, 2026, 1:39am UTC](https://discourse.julialang.org/t/ann-prettytables-jl-v3-5-one-table-specification-six-back-ends/139129/1 "2026-09-01T01:39:18Z")

</div>

PrettyTables.jl v3.5 adds **backend-agnostic table formats and styles**. Until now, making  
a table beautiful meant configuring backend-specific objects (`TextTableFormat`, `LatexTableStyle`, and so on), so switching back ends required rewriting the whole configuration. The new [`TableFormat`](https://ronisbr.github.io/PrettyTables.jl/dev/man/table_format/), `TableStyle`, and `LineStyle` objects describe the table lines and decorations **once** , and every back end converts them to its native representation.

Combined with the other backend-agnostic features of v3 (`Face`-based highlighters, `StyledStrings` in any cell, merged column labels, row groups, summary rows, and footnotes) the _entire_ presentation below is defined in a single named tuple. Switching the back end is one keyword.

This version is not released yet. I am posting here to check if there is some feedback about the implementation.

## The Specification

```julia
using PrettyTables

# Meridian constellation status: altitude [km], inclination [°], mass [kg], launch year,
# health state, and fraction of the design lifetime consumed.
data = Any[
     705.3 98.2 1850 2019 :operational 0.93
     705.8 98.2 1850 2021 :operational 0.68
     619.6 97.8 2140 2024 :operational 0.27
    1402.1 25.0 480 2018 :degraded 1.42
    1398.7 25.0 480 2020 :operational 0.81
     402.4 51.6 95 2023 :safe_mode 0.88
     549.9 53.0 112 2025 :operational 0.31
     561.2 53.1 118 2025 :operational 0.29
]

satellites = [
    "Meridian-1", "Meridian-2", "Meridian-3",
    "Relay-A", "Relay-B",
    "Pathfinder-1", "Pathfinder-2", "Pathfinder-3",
]

# A single slate gray draws every structural line: the junctions between horizontal and
# vertical lines then always match, so no line shows segments with mixed colors. The lines
# are differentiated by width and style instead.
STRUCTURE = "#64748b" # Every table line (slate gray).
CYAN = "#0284c7" # Title and column label groups.
AMBER = "#d97706" # Row group labels and warnings.
MAGENTA = "#db2777" # Highest mass highlight.
GREEN = "#059669" # Nominal indicators.
CORAL = "#dc2626" # Off-nominal indicators.
GRAY_TEXT = "#64748b" # Units, footnotes, and source notes.

HEALTH_LABELS = Dict(
    :operational => "● Operational",
    :degraded => "◐ Degraded",
    :safe_mode => "○ Safe Mode",
)

# Monospaced font used in the meter column so that the dots and the padded percentages
# align in the back ends with proportional fonts. `Face.font` is converted to the HTML
# `font-family`, the Typst `text-font`, and the Excel font name; the text back end is
# already monospaced and LaTeX ignores the font.
MONO = "DejaVu Sans Mono"

# Render the design lifetime fraction as a five-dot meter followed by the percentage. The
# circle glyphs render as clean typographic symbols in every back end. The percentage is
# padded with no-break spaces (U+00A0) because Typst and HTML collapse consecutive regular
# spaces, which would break the alignment of the column.
function life_meter(f::Number)
    n = clamp(round(Int, min(f, 1) * 5), 0, 5)
    return repeat('●', n) * repeat('○', 5 - n) * "\ua0" * lpad(round(Int, 100f), 3, '\ua0') * "\ua0%"
end

spec = (
    # -- Header and Footer -----------------------------------------------------------------
    title = "Meridian Constellation — Fleet Status",
    subtitle = "Flight dynamics daily report · 2026-08-31",

    footnotes = [
        (:column_label, 2, 6) => "Fraction of the design lifetime consumed.",
        (:row_label, 4, 0) => "Relay-A operates 42 % beyond its design life.",
        (:data, 6, 5) => "Safe mode entered after a star tracker anomaly; recovery in progress.",
    ],
    source_notes = "Source: mean orbital elements from GNSS telemetry, epoch 2026-08-31 12:00 UTC.",

    # -- Layout ----------------------------------------------------------------------------
    show_row_number_column = true,
    row_number_column_label = "#",
    stubhead_label = "Satellite",
    row_labels = satellites,

    row_group_labels = [
        1 => "Sun-Synchronous Imaging",
        4 => "Equatorial Data Relay",
        6 => "Technology Demonstration",
    ],

    column_labels = [
        [MultiColumn(2, "Orbit"), MultiColumn(2, "Spacecraft"), MultiColumn(2, "Mission Status")],
        [
            styled"Altitude {(foreground=#64748b):[km]}",
            styled"Inclination {(foreground=#64748b):[°]}",
            styled"Mass {(foreground=#64748b):[kg]}",
            "Launched",
            "Health",
            "Life Used",
        ],
    ],

    alignment = [:r, :r, :r, :c, :l, :r],

    summary_rows = [
        (data, j) ->
            j == 3 ? "$(sum(data[:, j])) kg" : "",
        (data, j) ->
            j == 1 ? "$(round(sum(data[:, j]) / 8; digits = 1)) km" :
            j == 6 ? "$(round(Int, 100 * sum(data[:, j]) / 8)) %" : "",
    ],
    summary_row_labels = ["Σ Total", "x̄ Mean"],

    # -- Cell Rendering --------------------------------------------------------------------
    formatters = [
        fmt__printf("%.1f", [1, 2]),
        (v, i, j) -> j == 5 ? HEALTH_LABELS[v] : v,
        (v, i, j) -> j == 6 ? life_meter(v) : v,
    ],

    # Highlighters are backend agnostic: a predicate on the data plus a `Face`. They are
    # applied in order and the first match wins.
    highlighters = [
        Highlighter((d, i, j) -> (j == 5) && (d[i, 5] == :operational), Face(; foreground = GREEN)),
        Highlighter((d, i, j) -> (j == 5) && (d[i, 5] == :degraded), Face(; foreground = AMBER, weight = :bold)),
        Highlighter((d, i, j) -> (j == 5) && (d[i, 5] == :safe_mode), Face(; foreground = CORAL, weight = :bold)),
        # No bold here: a synthesized or substituted bold face can change the glyph widths
        # and break the alignment of the meter column. The coral color is the emphasis.
        Highlighter((d, i, j) -> (j == 6) && (d[i, 6] >= 1), Face(; foreground = CORAL, font = MONO)),
        Highlighter((d, i, j) -> (j == 6) && (d[i, 6] >= 0.75), Face(; foreground = AMBER, font = MONO)),
        Highlighter((d, i, j) -> (j == 6), Face(; foreground = GREEN, font = MONO)),
        Highlighter((d, i, j) -> (j == 3) && (d[i, 3] == maximum(d[:, 3])), Face(; foreground = MAGENTA, weight = :bold)),
    ],

    # -- Table Format: Which Lines Are Drawn, and Their Design (NEW in v3.5) ---------------
    table_format = TableFormat(;
        top_line = LineStyle(; width = :thick, color = STRUCTURE),
        header_line = LineStyle(; style = :double, color = STRUCTURE),
        merged_header_cell_line = LineStyle(; color = STRUCTURE),
        middle_line = LineStyle(; color = STRUCTURE),
        bottom_line = LineStyle(; width = :thick, color = STRUCTURE),
        center_line = LineStyle(; color = STRUCTURE),

        horizontal_line_at_merged_column_labels = true,
        horizontal_lines_at_data_rows = :none,
        horizontal_line_before_row_group_label = true,
        horizontal_line_after_row_group_label = true,
        horizontal_line_before_summary_rows = true,

        vertical_line_at_beginning = false,
        vertical_line_after_row_number_column = false,
        vertical_line_after_row_label_column = true,
        vertical_lines_at_data_columns = :none,
        vertical_line_after_data_columns = false,
    ),

    # -- Table Style: How Each Section Is Decorated (NEW in v3.5) --------------------------
    style = TableStyle(;
        title = Face(; weight = :bold, foreground = CYAN),
        subtitle = Face(; slant = :italic, foreground = GRAY_TEXT),
        row_number_label = Face(; weight = :bold, foreground = GRAY_TEXT),
        row_number = Face(; foreground = GRAY_TEXT),
        stubhead_label = Face(; weight = :bold, slant = :italic),
        row_label = Face(; weight = :bold),
        row_group_label = Face(; weight = :bold, foreground = AMBER),
        first_line_merged_column_label = Face(; weight = :bold, foreground = CYAN),
        first_line_column_label = Face(; weight = :bold, foreground = CYAN),
        column_label = Face(; weight = :bold),
        summary_row_label = Face(; weight = :bold, slant = :italic),
        summary_row_cell = Face(; slant = :italic),
        footnote = Face(; foreground = GRAY_TEXT),
        source_note = Face(; slant = :italic, foreground = GRAY_TEXT),
    ),
)

```

## One Specification, Six Back Ends

```julia
# Terminal (text back end):
pretty_table(data; spec...)

# Markdown, LaTeX, Typst, and HTML:
pretty_table(String, data; backend = :markdown, spec...)
pretty_table(String, data; backend = :latex, spec...)
pretty_table(String, data; backend = :typst, spec...)
pretty_table(String, data; backend = :html, spec..., stand_alone = true)

# Excel workbook (the Excel back end activates when XLSX.jl is loaded):
using XLSX
pretty_table(data; backend = :excel, spec..., filename = "showcase.xlsx")

```

**Text back end (terminal):**

 ![Captura de Tela 2026-08-31 às 22.32.56](https://global.discourse-cdn.com/julialang/original/3X/5/d/5d6cc23d105f3771b96e547ae11291714b42bbf4.webp)

**Typst back end (compiled to PDF):**

 ![Captura de Tela 2026-08-31 às 22.33.20](https://global.discourse-cdn.com/julialang/original/3X/e/d/ed2bf21a024138fc87b48321d5b265582b2313b1.png)

**HTML back end:**

 ![Captura de Tela 2026-08-31 às 22.34.03](https://global.discourse-cdn.com/julialang/original/3X/1/8/1823a08dcc320c5e204b174ca3a6af640d1564bc.png)

**Excel back end:**

 ![Captura de Tela 2026-08-31 às 22.34.49](https://global.discourse-cdn.com/julialang/original/3X/8/0/806d8a00eb231a2dd32ba019f71829ba03816913.png)

## Notes

- Every field of `TableFormat` and `TableStyle` defaults to `nothing`, which means “keep the back end default”. The objects are a _sparse override_: you only state what you want to change, and each back end keeps its native defaults for the rest.
- The “Health” and “Life Used” columns show a useful pattern: store machine-readable values in the data (a `Symbol` state, a `Float64` fraction), render them with `formatters` (status glyphs, a dot meter), and color them with value-matching `Highlighter`s. All of it is backend agnostic.
- The conversion is a best effort: aspects a back end cannot express are silently ignored. For example, the text back end maps line designs to Unicode box-drawing characters, Typst has no double stroke (it falls back to solid), and the LaTeX dashed lines require the `arydshln` package. The manual has a [support matrix](https://ronisbr.github.io/PrettyTables.jl/dev/man/table_format/#Back-End-Support).
- The backend-specific options are still available in the native table formats and styles if you need finer control over a single back end.

I am still making some decisions and fine tuning some aspects.

---

<div class="post-metadata">

**Author:** ![vancleve](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/vancleve/32/4183_2.png) [@vancleve](https://discourse.julialang.org/u/vancleve)\
**Post date:** [September 1, 2026, 9:28pm UTC](https://discourse.julialang.org/t/ann-prettytables-jl-v3-5-one-table-specification-six-back-ends/139129/2 "2026-09-01T21:28:43Z")

</div>

Holy hell those are some good looking tables!

---

<div class="post-metadata">

**Author:** ![kristoffer.carlsson](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/kristoffer.carlsson/32/22_2.png) [@kristoffer.carlsson](https://discourse.julialang.org/u/kristoffer.carlsson)\
**Post date:** [September 2, 2026, 8:53am UTC](https://discourse.julialang.org/t/ann-prettytables-jl-v3-5-one-table-specification-six-back-ends/139129/3 "2026-09-02T08:53:53Z")

</div>

Nice, TimerOutputs will be ready for it ([Add formatted timer exports for PrettyTables backends - Pull Request #240 - KristofferC/TimerOutputs.jl - GitHub](https://github.com/KristofferC/TimerOutputs.jl/pull/240))

HTML:

 ![image](https://global.discourse-cdn.com/julialang/original/3X/4/3/439f46ea26b67a1c12939309f7283508422e27c2.png)

Typst:

 ![image](https://global.discourse-cdn.com/julialang/original/3X/8/6/86e229a6f5996210910e7fc43d67acec46711715.png)

Excel:

 ![image](https://global.discourse-cdn.com/julialang/original/3X/5/9/5984a3b33e8edf67f60a32d2692de860f8490bac.png)

---

<div class="post-metadata">

**Author:** ![TimG](https://avatars.discourse-cdn.com/v4/letter/t/82dd89/32.png) [@TimG](https://discourse.julialang.org/u/TimG)\
**Post date:** [September 2, 2026, 9:37am UTC](https://discourse.julialang.org/t/ann-prettytables-jl-v3-5-one-table-specification-six-back-ends/139129/4 "2026-09-02T09:37:06Z")

</div>

`DataBar`s are also possible in Excel, too, using `setConditionalFormat` in XLSX.jl - though not directly through PrettyTables, I don’t think.

---

<div class="post-metadata">

**Author:** ![BLI](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/bli/32/37206_2.png) [@BLI](https://discourse.julialang.org/u/BLI)\
**Post date:** [September 2, 2026, 6:32pm UTC](https://discourse.julialang.org/t/ann-prettytables-jl-v3-5-one-table-specification-six-back-ends/139129/5 "2026-09-02T18:32:42Z")

</div>

Looks really nice. Is there some possibility to specify that floats should be presented in either scientific format (e.g., 1e5 or engineering format (e.g., 100e3)? And to typeset these in LaTeX or whatever?

---

<div class="post-metadata">

**Author:** ![Ronis\_BR](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ronis_br/32/50999_2.png) [@Ronis\_BR](https://discourse.julialang.org/u/Ronis_BR)\
**Post date:** [September 2, 2026, 6:42pm UTC](https://discourse.julialang.org/t/ann-prettytables-jl-v3-5-one-table-specification-six-back-ends/139129/6 "2026-09-02T18:42:35Z")

</div>

Yes! You can use a formatter to change the output of any cell you want.

---

<div class="post-metadata">

**Author:** ![orlando\_mendez](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/orlando_mendez/32/211282_2.png) [@orlando\_mendez](https://discourse.julialang.org/u/orlando_mendez)\
**Post date:** [September 2, 2026, 7:37pm UTC](https://discourse.julialang.org/t/ann-prettytables-jl-v3-5-one-table-specification-six-back-ends/139129/7 "2026-09-02T19:37:20Z")

</div>

HI,  
A newby here, so please bear with me, I added the package and pasted the code above to try and running, however I’m getting an error message:

```julia-auto
ERROR: LoadError: UndefVarError: `@styled_str` not defined in `Main`
Suggestion: check for spelling errors or missing imports.
Hint: a global variable of this name also exists in StyledStrings.StyledMarkup.
    - Also exported by StyledStrings (loaded but not imported in Main).
in expression starting at REPL[17]:28

```

Line 17 if I’m not mistaken is about initializing the satelites list  
` "Meridian-1", "Meridian-2", "Meridian-3",`  
So any help is appreciated. In principle, I’m only interested in displaying the table in the terminal, so the rest of outputs aren’t as important (for now).

Thanks!

---

<div class="post-metadata">

**Author:** ![Ronis\_BR](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ronis_br/32/50999_2.png) [@Ronis\_BR](https://discourse.julialang.org/u/Ronis_BR)\
**Post date:** [September 3, 2026, 12:01am UTC](https://discourse.julialang.org/t/ann-prettytables-jl-v3-5-one-table-specification-six-back-ends/139129/8 "2026-09-03T00:01:41Z")

</div>

Hi @orlando_mendez !

The version is not released yet. So you need to clone the `dev` version. Type in Julia REPL `]dev PrettyTables`. Then, restart Julia and try again 🙂

---

<div class="post-metadata">

**Author:** ![kapple](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/kapple/32/218915_2.png) [@kapple](https://discourse.julialang.org/u/kapple)\
**Post date:** [September 3, 2026, 3:14am UTC](https://discourse.julialang.org/t/ann-prettytables-jl-v3-5-one-table-specification-six-back-ends/139129/9 "2026-09-03T03:14:09Z")

</div>

Any plans for Microsoft Word backend?  
Or do the Excel tables copy decently to docx files?

---

<div class="post-metadata">

**Author:** ![simonsteiger](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/simonsteiger/32/209862_2.png) [@simonsteiger](https://discourse.julialang.org/u/simonsteiger)\
**Post date:** [September 3, 2026, 5:22am UTC](https://discourse.julialang.org/t/ann-prettytables-jl-v3-5-one-table-specification-six-back-ends/139129/10 "2026-09-03T05:22:05Z")

</div>

I also needed a docx backend a while ago and created StyledTables.jl on top of SummaryTables (this started pre-LLM but Claude eventually allowed me to more cleanly package the code I had lying around). And it of course offers much fewer features than PrettyTabes.jl, but it’s so far served my docx-hungry collaborators well.

And super cool new features @Ronis_BR, the tables all look so beautiful!

---

<div class="post-metadata">

**Author:** ![TimG](https://avatars.discourse-cdn.com/v4/letter/t/82dd89/32.png) [@TimG](https://discourse.julialang.org/u/TimG)\
**Post date:** [September 3, 2026, 7:45am UTC](https://discourse.julialang.org/t/ann-prettytables-jl-v3-5-one-table-specification-six-back-ends/139129/11 "2026-09-03T07:45:20Z")

</div>

The Excel version will easily cut and paste into Word. I just used ctrl-c / ctrl-v. It pastes as a Word table, so is fully editable after pasting.

 ![image](https://global.discourse-cdn.com/julialang/original/3X/2/2/22cffb7edf6982b623778617be3688d52a8daf8c.png)

This image clipped directly from Word.

---

<div class="post-metadata">

**Author:** ![jules](https://avatars.discourse-cdn.com/v4/letter/j/41988e/32.png) [@jules](https://discourse.julialang.org/u/jules)\
**Post date:** [September 3, 2026, 11:13am UTC](https://discourse.julialang.org/t/ann-prettytables-jl-v3-5-one-table-specification-six-back-ends/139129/12 "2026-09-03T11:13:29Z")

</div>

> [@simonsteiger](#):
>
> I also needed a docx backend a while ago and created [StyledTables.jl](https://juliaregistries.github.io/General/packages/redirect_to_repo/StyledTables) on top of SummaryTables (this started pre-LLM but Claude eventually allowed me to more cleanly package the code I had lying around). And it of course offers much fewer features than [PrettyTabes.jl](https://juliaregistries.github.io/General/packages/redirect_to_repo/PrettyTabes), but it’s so far served my docx-hungry collaborators well.

I’ll see if I can make a PR to add WriteDocx to the matrix here. It has more features than SummaryTables uses (that’s mostly due to the very conservative styling requirements in the pharma space, which is why SummaryTables is very opinionated and doesn’t do colored cell backgrounds for example).
