# Prospective changes to AnnotatedString and StyledStrings

**URL:** <https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847>\
**Category:** Internals & Design\
**Tags:** stdlib\
**Created:** [January 2, 2026, 7:40pm UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847 "2026-01-02T19:40:32Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![tecosaur](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tecosaur/32/23206_2.png) [@tecosaur](https://discourse.julialang.org/u/tecosaur)\
**Post date:** [January 2, 2026, 7:40pm UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/1 "2026-01-02T19:40:32Z")

</div>

_A request for feedback and testing for a large piece of work I’ve undertaken_

# Background

The `StyledStrings` was first conceptualised in June 2021, PR’d in May 2023, merged in Oct 2023, and shipped in Julia v1.11.

Since then, the initial design has undergone several rounds of refinement. Lilith has been fantastic helping hash out some of the particular semantics, Sukera helped me use Supposition.jl’s PBT to make the `styled"""` markup syntax parsing robust, Kristoffer has identified and implemented a handful of performance wins, and most recently, Cody has been a big help in working out how to work around the Base/stdlib split and thorny invalidations caused by it.

While all of these improvements have been valuable additions, time has also revealed which of the decisions made in the original design/implementation have caused the biggest headaches and what compromises have struck a poor balance in hindsight.

I’ve recently pushed some work that’s been discussed on-and-off with various people to a viable state, to address all of the headaches and shortcomings that have emerged as major annoyances/impediments that I’m (a) aware of, and (b) feel confident to handle. This has taken a while in large part because I’ve been waiting for a few key design decisions to settle and feel “right” in my mind.

I’m now very interested in getting feedback on the proposed changes, and encouraging anybody interested to take the WIP branch for a spin and see what you think in practice/if you can find anything I’ve overlooked.

# The headaches and shortcomings

- Piracy with the dispatch on printing/display of `AnnotatedString` (defined in `Base`, implemented in `StyledStrings`)
- The `AnnotatedString` behaviour
- It not being possible for any other package to define its own annotation type with custom display/printing
- A lack of namespacing
- The `Any` value type in annotations (`AnnotatedString`/`AnnotatedChar`/`AnnotatedIO`)
- No validation of named faces (`styled"{bluue:text}"` is silently unstyled)
- Long prefixes when using custom faces within a package (e.g. `REPL_History_search_unselected`)
- Consistent face name prefixes relying on package authors always following the recommendations (in other words: inconsistent face naming)
- The use of `Face`s directly mixed with the use of `Symbol`s to refer to named faces (and all the `Union{Symbol, Face}` code that results)
- The inability to remap faces, except when displaying (`withfaces`)
- The inability to unset a property of a face in user configuration (only set it to a different value)

In the end, about half of these items are collectively addressed in a suite of tightly connected changes that I’m collectively terming “Face primacy”. More on that later.

Now, onto the solutions/proposed changes.

Oh, by the way, everything here is fully backwards-compatible, up to `Annotated{String,Char,IOBuffer}` gaining an extra type parameter.

# Parameterising the value of annotations

To remove the `value::Any` from the various annotated types, we change it to `value::V where V`. By itself, this wouldn’t be terribly helpful since we’d then have `StyledStrings` using a combination of face names (`Symbol`s) and `Face` values: `value::Symbol`, `value::Face`, and `value::Union{Symbol, Face}`. As well as this seeming likely to make inference more complex, the use of `Symbol` prevents the `StyledStrings` display methods from being based solely on `StyledStrings`-defined types, which is what we need to avoid piracy.

# Hybrid-interned Faces, instead of Symbols

With value-parameterised annotations, it would be simplest and nicest to just have `value::Face`, but eliminating `Symbol`-named `Face`s both makes the Public API much more inconvenient, and makes some detailes of the implementation very tricky (colours are implemented as named `Face`s, without names there is no “red” foreground/background).

The neat solution to this I’ve landed on is naming `Face`s with `Face`s. Specifically, by:

1. Renaming the immutable `Face` struct
2. Creating a mutable wrapper type
3. Interning `Face`s by name (into an `IdDict`)
4. Creating a convenience for accessing/interning faces: `face"<name>"`

This lets us drop the use of `:red` to refer to a particular `Face` (`Symbol => Face`), and instead use `face"red"` (`Face => Face`). Customisations etc. are referenced against that particular `Face`, instead of the name.

This is integrated into the `styled"""` macro, so that `styled"{red:text}"` uses the new interning system.

Note: there’s more going on here than just interning (hence the “hybrid”), as you’ll see in the next section.

**Old Sample**

```julia
importantface = :emphasis

astr = styled"{$importantface:oh hey there!}"

withfaces(:emphasis => :magenta) do
   println(astr)
end

```

**New Sample**

```julia
importantface = face"emphasis"

astr = styled"{$importantface:oh hey there!}" # Unchanged

withfaces(face"emphasis" => face"magenta") do
   println(astr)
end

```

# Namespaces and palettes

Currently, all named faces are put in a shared global dictionary, where packages have to specify the full names set/used. This is done for four reasons:

1. Having full names makes mapping user configuration straightforward
2. I didn’t think of a nice way to do namespacing
3. Nobody suggested a good way of doing namespacing
4. Over in Emacs-land, this is what’s done, and everybody adheres strictly to the conventions (it might help that in Emacs, if you try to define a face without your package prefix, you get a warning)

After talking about the design with Cody, I’ve now got a system for both module-level face namespaces, and named collections of faces — palettes. If you hadn’t guessed already, the module namespaces are just palettes with a predictable name.

The public API for this consists of three new macros:

- `@defpalette!` for creating a palette
- `@registerpalette!` (placed within ` __init___ `) for performing some work at runtime (needed to make user customisation work, along with light/dark theming)
- `@usepalettes!` for bringing in faces from certain palettes (think `using`). Note that this may be used exactly once per module.

Palettes affect how `face"<name>"` looks up the `Face` belonging to `<name>`, letting you skip package prefixes. Instead of `:markdown_heading` you can call `@usepalletes! Markdown` and then do `face"heading"`. Dot-access also works (and allows you to reference different faces with the same name from different modules): `face"Markdown.heading"`.

Use of `@defpalette!`, `@usepalettes!`, or `face""` opts you into a post-Face primacy API, where all faces looked up (by `face""` or `styled""`) must be defined in one of the palettes used.

This all works by having `@defpalette!` store the definitions in a special named tuple accessed by `@usepalettes!` and `face""`. This allows for faces to be looked up at compile (macroexpansion) time instead of runtime. To preserve runtime behaviour (backwards compatibility, the ability to customise faces, etc.) we then intern these faces with `@registerpalette!` during ` __init__ `.

This is the “hybrid” in “hybrid Face interning”. Named `Face`s are created at compile-time, and passed around/referenced between modules at compile time, and then interned at runtime. This removes the need for `addfaces!`.

**Old Sample**

```julia
module MyColours

using StyledStrings

const FACES = (
    :MyColours_gold => Face(foreground = 0xffd700),
    :MyColours_lavender => Face(foreground = 0xe6e6fa),
    :MyColours_salmon => Face(foreground = 0xfa8072),
    # ...
)

__init__ () = foreach(addface!, FACES)

end

# --

module MyTables

using StyledStrings
using MyColours

const FACES = (
    :MyTables_header => Face(inherit = :MyColours_lavender, bold = true),
    :MyTables_row => Face(inherit = :MyColours_salmon),
    :MyTables_rowsep => Face(inherit = :MyColours_gold),
    # ...
)

__init__ () = foreach(addface!, FACES)

function printtable(header::String, rows::Vector{String}, width::Int)
    println(styled"{MyTables_header:$header}\n{MyTables_rowsep:$('-'^width)}")
    for row in rows
        println(styled"{MyTables_row:$row}")
    end
end

end

```

**New Sample**

```julia
module MyColours

using StyledStrings

@defpalette! begin
    gold = Face(foreground = 0xffd700)
    lavender = Face(foreground = 0xe6e6fa)
    salmon = Face(foreground = 0xfa8072)
    # ...
end

__init__ () = @registerpalette!

end

# --

module MyTables

using StyledStrings
using MyColours

@usepalettes! MyColours

@defpalette! begin
    header = Face(inherit = lavender, bold = true)
    row = Face(inherit = salmon)
    rowsep = Face(inherit = gold)
end

__init__ () = @registerpalette!

function printtable(header::String, rows::Vector{String}, width::Int)
    println(styled"{header:$header}\n{rowsep:$('-'^width)}")
    for row in rows
        println(styled"{row:$row}")
    end
end

end

```

# Remapping faces

For backwards compatibility with the hybrid face interning system, we needed the ability to remap faces resolved at compile time to a different face at runtime. After doing this work, it was trivial to introduce a new `withfaces`-like context wrapper that applies to face _construction_ rather than _display_.

For example,

```julia
mapstr = remapfaces(face"red" => face"blue") do
    styled"some {red:important} text"
end

```

produces `styled"some {blue:important} text"`.

# Unsetting faces

In your `faces.toml`, you can now set attributes to `"default"` to unset that attribute.

I’m considering renaming the special value string to `"unset"`.

I’m planning on extending this to families/palettes, e.g. `Markdown = "unset"`.

# Bonus: adaptive colouring and colour blending

Since I’m doing all this writing, I’ll also mention a separate new feature that I’m quite excited for: the ability to define light/dark face variants and also detect terminal colours and blend faces at runtime to adapt to the colour scheme the user sees.

Light/dark face variants can be defined like so:

```julia
@defpalette! begin
    region = Face(background=0x636363)
    region.light = Face(background=0xaaaaaa)
    region.dark = region => Face(background=0x363636)
end

```

The 16 ANSI colours are also read from the current terminal (when possible), making it possible to adjust styling to match the user’s terminal. This lets us break beyond the limits of 16-bit colour without any compromises, letting us do things like add pretty and customisable (and hence accessible) region highlights.

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

 ![image](https://global.discourse-cdn.com/julialang/original/3X/6/9/69389d072a69fab164966737c64e6809c9322c87.png)

_NB: please pretend you see `face"background"` instead of `:background`, these screenshots are a little out of date_

# Status

As of writing, this has all been implemented, and existing tests are passing. There’s a ~3000 loc PR to `StyledStrings` I’d love to get a code review on if anybody’s up for the task, and a much smaller ~300 loc WIP PR to the main Julia repo ([#60527](https://github.com/JuliaLang/julia/pull/60527)).

There are some minor issues blocking CI currently (apparently `depwarn` is an error in Julia CI?), and I’d like to write a bunch of new tests for the new functionality, but it’s all implemented at this point, and seemingly working well 🤞.

* * *

There’s some more I’d write about, but this is already much longer than I anticipated, and it’s much later than I’d like, so I’ll just call it here for now 🙂 I’m very happy to clarify details, and go into depth on aspects of the design (e.g. topologically sorting face dependencies within a palette to allow for order-independent definition).

---

<div class="post-metadata">

**Author:** ![tecosaur](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tecosaur/32/23206_2.png) [@tecosaur](https://discourse.julialang.org/u/tecosaur)\
**Post date:** [January 3, 2026, 6:36pm UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/2 "2026-01-03T18:36:27Z")

</div>

For another angle on this (and in case I can interest anyone in some proofreading), I’ve drafted the relevant changes to the documentation.

> **The draft**
>
> Note: this will look a bit funny because it hasn’t been processed by Documenter. Hence the `@ref`/`@id` links, and example blocks that are supposed to be executed to produce the output content shown in the docs.
> 
> * * *
> 
> # [StyledStrings](@id stdlib-styledstrings)
> 
> ```julia-auto
> CurrentModule = StyledStrings
> DocTestSetup = quote
> using StyledStrings
> end
> 
> ```
> 
> !!! note  
> The API for StyledStrings and AnnotatedStrings is considered experimental and is subject to change between  
> Julia versions.
> 
> ## [Styling](@id stdlib-styledstrings-styling)
> 
> When working with strings, formatting and styling often appear as a secondary  
> concern.
> 
> For instance, when printing to a terminal you might want to sprinkle [ANSI  
> escape  
> sequences](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters)  
> in the output, when outputting HTML styling constructs (`<span style="...">`,  
> etc.) serve a similar purpose, and so on. It is possible to simply insert the  
> raw styling constructs into the string next to the content itself, but it  
> quickly becomes apparent that this is not well suited for anything but the most  
> basic use cases. Not all terminals support the same ANSI codes, the styling  
> constructs need to be painstakingly removed when calculating the width of  
> already-styled content, and that’s before you even get into handling multiple  
> output formats.
> 
> Instead of leaving this headache to be widely experienced downstream, it is  
> tackled head-on by the introduction of a special string type  
> ([`AnnotatedString`](@ref Base.AnnotatedString)). This string type wraps any other  
> `AbstractString` type and allows for formatting information to be applied to regions (e.g.  
> characters 1 through to 7 are bold and red).
> 
> Regions of a string are styled by applying [`Face`](@ref StyledStrings.Face)s  
> (think “typeface”) to them — a structure that holds styling information. As a  
> convenience, faces can be named (e.g. `shadow`) and then referenced instead of  
> giving the [`Face`](@ref StyledStrings.Face) directly.
> 
> Along with these capabilities, we also provide a convenient way for constructing  
> [`AnnotatedString`](@ref Base.AnnotatedString)s, detailed in [Styled String  
> Literals](@ref stdlib-styledstring-literals).
> 
> ```julia-auto
> using StyledStrings
> styled"{yellow:hello} {blue:there}"
> 
> ```
> 
> ## [Annotated Strings](@id man-annotated-strings)
> 
> It is sometimes useful to be able to hold metadata relating to regions of a  
> string. A [`AnnotatedString`](@ref Base.AnnotatedString) wraps another string and  
> allows for regions of it to be annotated with labelled values (`:label => value`).  
> All generic string operations are applied to the underlying string. However,  
> when possible, styling information is preserved. This means you can manipulate a  
> [`AnnotatedString`](@ref Base.AnnotatedString) —taking substrings, padding them,  
> concatenating them with other strings— and the metadata annotations will “come  
> along for the ride”.
> 
> This string type is fundamental to the [StyledStrings stdlib](@ref  
> stdlib-styledstrings), which uses `:face`-labelled annotations to hold styling  
> information, but arbitrary textual metadata can also be held with the string,  
> such as part of speech labels.
> 
> When concatenating a [`AnnotatedString`](@ref Base.AnnotatedString), take care  
> to use [`annotatedstring`](@ref StyledStrings.annotatedstring) instead of  
> `string` if you want to keep the string annotations. The annotations of  
> a [`AnnotatedString`](@ref Base.AnnotatedString) can be accessed and modified  
> via the [`annotations`](@ref StyledStrings.annotations) and [`annotate!`](@ref  
> StyledStrings.annotate!) functions.
> 
> ```jldoctest
> julia> str = AnnotatedString("hello there", [(1:5, :pos, :interjection), (7:11, :pos, :pronoun)])
> "hello there"
> 
> julia> lpad(str, 14)
> " hello there"
> 
> julia> typeof(lpad(str, 7))
> AnnotatedString{String, Symbol}
> 
> julia> str2 = AnnotatedString(" julia", [(2:6, :pos, :noun)])
> " julia"
> 
> julia> str3 = Base.annotatedstring(str, str2)
> "hello there julia"
> 
> julia> Base.annotations(str3)
> 3-element Vector{@NamedTuple{region::UnitRange{Int64}, label::Symbol, value::Symbol}}:
> (region = 1:5, label = :pos, value = :interjection)
> (region = 7:11, label = :pos, value = :pronoun)
> (region = 13:17, label = :pos, value = :noun)
> 
> julia> str1 * str2 == str3 # *-concatenation works
> true
> 
> ```
> 
> Note that here the `AnnotatedString` type parameters depend on both the string  
> being wrapped, as well as the type of the annotation values.
> 
> !!! compat “Julia 1.14”  
> The use of a type parameter for annotation values was introduced with Julia 1.14.
> 
> ## [Faces](@id stdlib-styledstrings-faces)
> 
> Styles are specified through the `Face` type, which is designed to represent a common set of useful typeface attributes across multiple mediums (e.g. Terminals, HTML renderers, and LaTeX/Typst). Suites of faces can be defined in packages, conveniently referenced by name, reused in other packages, and [customised by users](@ref stdlib-styledstrings-face-toml).
> 
> ### The `Face` type
> 
> A [`Face`](@ref StyledStrings.Face) specifies details of a typeface that text can be set in. It  
> covers a set of basic attributes that generalize well across different formats,  
> namely:
> 
> - `font`
> - `height`
> - `weight`
> - `slant`
> - `foreground`
> - `background`
> - `underline`
> - `strikethrough`
> - `inverse`
> - `inherit`
> 
> For details on the particular forms these attributes take, see the  
> [`Face`](@ref StyledStrings.Face) docstring. However, it is worth drawing  
> particular attention to `inherit`, as it allows us to _inherit_ attributes from other  
> [`Face`](@ref StyledStrings.Face)s.
> 
> The attributes that specify color (`foreground`, `background`, and optionally `underline`) may either specify a 24-bit RGB colour (e.g. `#4063d8`) or name another face whose foreground colour is used. In this way a set of named colors can be created by defining faces with foreground colors.
> 
> !!! compat “Julia 1.14”  
> Direct use of `Face`s as colors was introduced with Julia 1.14.
> 
> ```julia-auto
> juliablue = Face(foreground = 0x4063d8) # Hex-style colour
> juliapurple = Face(foreground = (r = 0x95, g = 0x58, b = 0xb2)) # RGB tuple
> Face(foreground = juliablue, underline = juliapurple)
> 
> ```
> 
> Notice that the foreground and underline color are labelled as “unregistered”.  
> While assigning `Face`s to variables is fine for one-off use, it is often more  
> useful to create named faces that you can reuse, and other packages can build on.
> 
> ### [Named faces and `face""`](@id stdlib-styledstrings-named-faces)
> 
> StyledStrings comes with 32 named faces. The `default` face fully specifies all  
> attributes, and represents the expected “default state” of displayed text. The  
> `foreground` and `background` faces give the default foreground and background  
> of the `default` face. For convenience, the faces `bold`, `light`, `italic`,  
> `underline`, `strikethrough`, and `inverse` are defined to save the trouble of  
> frequently creating `Face`s just to set the relevant attribute.
> 
> We then have 16 faces for the 8 standard terminal colors, and their bright  
> variants: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`,  
> `bright_black`/`grey`/`gray`, `bright_red`, `bright_green`, `bright_blue`,  
> `bright_magenta`, `bright_cyan`, and `bright_white`. These `Face`s, together  
> with `foreground` and `background` are special in that they are their own  
> foreground. This is unique to these 18 faces, as their particular color will  
> depend on the terminal they are shown in.
> 
> A small collection of semantic faces are also defined, for common uses. For  
> shadowed text (i.e. dim but there) there is the `shadow` face. To indicate a  
> selected region, there is the `region` face. Similarly for emphasis and  
> highlighting the `emphasis` and `highlight` faces are defined. There is also  
> `code` for code-like text, `key` for keybindings, and `link` for links. For  
> visually indicating the severity of messages, the `error`, `warning` (with the  
> alias: `warn`), `success`, `info`, `note`, and `tip` faces are defined.
> 
> These faces can be easily retrieved using the [`face""`](@ref @face\_str) macro.  
> For instance, `face"blue"` returns the `Face` that defines the colour blue, and  
> `face"emphasis"` returns the `Face` defined for marking emphasised text.
> 
> ```julia-auto
> face"default"
> face"highlight"
> face"tip"
> 
> ```
> 
> !!! compat “Julia 1.14”  
> The `face""` macro and current face naming system was introduced with Julia 1.14.  
> In Julia 1.11 through to 1.13 (and the backwards compatibility package registered in General)  
> faces are named with `Symbol`s, and a global faces dictionary is used. The old API is still  
> supported for backwards compatibility, but it is strongly recommended to support the new  
> system by putting palette definitions behind a version gate if compatibility with older library  
> versions is required.
> 
> ### [Palettes](@id stdlib-styledstrings-face-palettes)
> 
> While the base faces provided are often enough for basic styling, they convey  
> little semantic meaning. Instead of a package say using `face"bold"` or an  
> anonymous `Face` for table headers it would be more appropriate to create a  
> named face for each distinct purpose. This benefits three sets of people:
> 
> - Package authors: as it’s much easier to implement readable code and consistent styling by naming the styles you use
> - The ecosystem: as it allows for style reuse across packages
> - End users: as named faces allow for customisation (more on that later)
> 
> !!! tip “Effective use of named faces”  
> It is _strongly_ recommended that packages should take care to use and introduce  
> semantic faces (like `code` and `table_header`) over direct colors and styles (like `cyan`).
> 
> Sets of named faces are created with the `@defpalette!` macro. Simply  
> wrap a series of `<name> = Face(...)` statements in a `begin ... end` block and  
> declare all the faces you want to create. For example:
> 
> ```julia
> @defpalette! begin
> table_header = Face(weight = :bold, underline = true)
> table_row_even = Face(background = bright_black)
> table_row_odd = Face(background = black)
> table_separator = Face(foreground = yellow)
> end
> 
> ```
> 
> All faces defined by `@defpalette!` are recognised by [`face""`](@ref  
> @face\_str). In our table example, this means that `face"table_header"` will work  
> just as `face"cyan"` does.
> 
> To support face customisation, along with other runtime features, it is  
> necessary that whenever `@defpalette!` is used a call to `@registerpalette!` is  
> put in the module’s ` __init__ ` function.
> 
> ```julia
> function __init__ ()
> @registerpalette!
> end
> 
> ```
> 
> The ability to use a face defined within a module, like `face"table_header"`, is  
> specific to that module. Should `face"table_header"` be put in another module,  
> it will not be found. In order to use faces defined in another module or  
> package, we can invoke `@usepalettes!`. This imports the faces defined  
> by the modules provided as arguments.
> 
> ```julia
> @usepalettes! MyColors
> 
> face"burgundy" # defined in MyColors
> 
> ```
> 
> !!! note “Declare and import faces before using them”  
> Face resolution with `face""` is performed at macro-expansion (compile) time.  
> A consequence of this is that faces must be defined and imported with `@defpalette!`  
> and `@usepalettes!` before any `face""` calls referencing those faces.
> 
> It is also possible to specify a color provided by another module using a  
> qualified name, of the form `face"<module path>.<name>`. In our example,  
> `face"MyColors.burgundy"` could be used if `@usepalettes!` wasn’t called.
> 
> ### [Dynamic face theming](@id stdlib-styledstrings-theming)
> 
> When trying to create well-designed content for the terminal, only being able to assume 8 colors with two shades, but not knowing what those colors are, can be frustratingly limiting. However, reaching outside the 16 shades is fraught. Take picking a highlight colour. For any single color you pick, there’s a user with a terminal theme that makes the resulting content unreadable.
> 
> By hooking into `REPL` initialisation, `StyledStrings` is able to query the terminal state and determine what the actual colors used by the terminal are. This allows for simplistic light/dark detection, as well as more sophisticated color blending.
> 
> Light and dark variants of a face can be embedded in the `@usepalettes!` call that defines the faces, by using `.light` and `.dark` suffixes. For example:
> 
> ```julia
> @defpalette! begin
> table_highlight = Face(background = 0xc2990b) # A muddy yellow. Not great but often legible
> table_highlight.light = Face(background = 0xffda90) # A pale yellow for light themes
> table_highlight.dark = Face(background = 0x876804) # A dull yellow for dark themes
> end
> 
> ```
> 
> Users can customise the light and dark face variants, even if no variants are  
> declared in `@defpalette!`. At runtime, the light and dark variants will  
> automatically be applied when a light/dark terminal theme is detected.
> 
> This helps us avoid the worst case of illegible content, but we can still do better. People are still using odd themes which make it hard to pick shades and hues that are completely reliable, and it’s easy to clash with the colour hues already used in the terminal color theme (for example, if the particular green you pick clashes). To produce the best experience, we can _blend_ the colors already used in the terminal theme to produce the best hue and shade. This is done with three key functions:
> 
> - `recolor` as a hook for when theme information has been collected/updated
> - `blend` for blending colors
> - `setface!` for updating the face style
> 
> Using these tools, we can set `table_highlight` to fit in seamlessly with the existing terminal theme.
> 
> ```julia
> # Must be placed within ` __init__ `
> StyledStrings.recolor() do
> faintyellow = StyledStrings.blend(face"yellow", face"background", 0.7)
> StyledStrings.setface!(face"table_highlight", Face(background = faintyellow))
> end
> 
> ```
> 
> Whether the extra capability afforded by this approach will vary face-by-face. It is most valuable when reaching for colors in-between those offered by the terminal, or based on the particular foreground/background shades or hues.
> 
> ### Applying faces to a `AnnotatedString`
> 
> By convention, the `:face` attributes of a [`AnnotatedString`](@ref  
> Base.AnnotatedString) hold information on the [`Face`](@ref StyledStrings.Face)s  
> that currently apply.
> 
> !!! compat “Julia 1.14”  
> Faces used to be given by either a single `Face`, a `Symbol` naming a face, or a vector  
> of `Face`s/`Symbol`s. As of version 1.14 this is deprecated and only supported for  
> backwards compatibility. This should not be used in any new code.
> 
> The `show(::IO, ::MIME"text/plain", ::AnnotatedString)` and `show(::IO, ::MIME"text/html", ::AnnotatedString)` methods both look at the `:face` attributes  
> and merge them all together when determining the overall styling.
> 
> We can supply `:face` attributes to a `AnnotatedString` during construction, add  
> them to the properties list afterwards, or use the convenient [Styled String  
> literals](@ref stdlib-styledstring-literals).
> 
> ```julia-auto
> str1 = AnnotatedString("blue text", [(1:9, :face, face"blue")])
> str2 = styled"{blue:blue text}"
> str1 == str2
> sprint(print, str1, context = :color => true)
> sprint(show, MIME("text/html"), str1, context = :color => true)
> 
> ```
> 
> ## [Styled String Literals](@id stdlib-styledstring-literals)
> 
> To ease construction of [`AnnotatedString`](@ref Base.AnnotatedString)s with [`Face`](@ref StyledStrings.Face)s applied,  
> the [`styled"..."`](@ref @styled\_str) styled string literal allows for the content and  
> attributes to be easily expressed together via a custom grammar.
> 
> Within a [`styled"..."`](@ref @styled\_str) literal, curly braces are considered  
> special characters and must be escaped in normal usage (`\{`, `\}`). This allows  
> them to be used to express annotations with (nestable) `{annotations...:text}`  
> constructs.
> 
> The `annotations...` component is a comma-separated list of three types of annotations.
> 
> - Face names (resolved using the same process described for `face""`)
> - Inline `Face` expressions `(key=val,...)`
> - `key=value` pairs
> 
> Interpolation is possible everywhere except for inline face _keys_.
> 
> For more information on the grammar, see the extended help of the  
> [`styled"..."`](@ref @styled\_str) docstring.
> 
> As an example, we can demonstrate the list of built-in faces mentioned above like so:
> 
> ```julia-repl
> julia> println(styled"
> The basic font-style attributes are {bold:bold}, {light:light}, {italic:italic},
> {underline:underline}, and {strikethrough:strikethrough}.
> 
> In terms of color, we have named faces for the 16 standard terminal colors:
> {black:■} {red:■} {green:■} {yellow:■} {blue:■} {magenta:■} {cyan:■} {white:■}
> {bright_black:■} {bright_red:■} {bright_green:■} {bright_yellow:■} {bright_blue:■} {bright_magenta:■} {bright_cyan:■} {bright_white:■}
> 
> Since {code:bright_black} is effectively grey, we define two aliases for it:
> {code:grey} and {code:gray} to allow for regional spelling differences.
> 
> To flip the foreground and background colors of some text, you can use the
> {code:inverse} face, for example: {magenta:some {inverse:inverse} text}.
> 
> The intent-based basic faces are {shadow:shadow} (for dim but visible text),
> {region:region} for selections, {emphasis:emphasis}, and {highlight:highlight}.
> As above, {code:code} is used for code-like text.
> 
> Lastly, we have the 'message severity' faces: {error:error}, {warning:warning},
> {success:success}, {info:info}, {note:note}, and {tip:tip}.
> 
> Remember that all these faces (and any user or package-defined ones) can
> arbitrarily nest and overlap, {region,tip:like {bold,italic:so}}.")
> 
> ```
> 
> ```julia-auto
> Documenter doesn't properly represent all the styling above, so I've converted it manually to HTML and LaTeX.
> 
> ```
> 
> ```julia-auto
> <pre>
> The basic font-style attributes are <span style="font-weight: 700;">bold</span>, <span style="font-weight: 300;">light</span>, <span style="font-style: italic;">italic</span>,
> <span style="text-decoration: underline;">underline</span>, and <span style="text-decoration: line-through">strikethrough</span>.
> 
> In terms of color, we have named faces for the 16 standard terminal colors:
> <span style="color: #1c1a23;">■</span> <span style="color: #a51c2c;">■</span> <span style="color: #25a268;">■</span> <span style="color: #e5a509;">■</span> <span style="color: #195eb3;">■</span> <span style="color: #803d9b;">■</span> <span style="color: #0097a7;">■</span> <span style="color: #dddcd9;">■</span>
> <span style="color: #76757a;">■</span> <span style="color: #ed333b;">■</span> <span style="color: #33d079;">■</span> <span style="color: #f6d22c;">■</span> <span style="color: #3583e4;">■</span> <span style="color: #bf60ca;">■</span> <span style="color: #26c6da;">■</span> <span style="color: #f6f5f4;">■</span>
> 
> Since <span style="color: #0097a7;">bright_black</span> is effectively grey, we define two aliases for it:
> <span style="color: #0097a7;">grey</span> and <span style="color: #0097a7;">gray</span> to allow for regional spelling differences.
> 
> To flip the foreground and background colors of some text, you can use the
> <span style="color: #0097a7;">inverse</span> face, for example: <span style="color: #803d9b;">some </span><span style="background-color: #803d9b;">inverse</span><span style="color: #803d9b;"> text</span>.
> 
> The intent-based basic faces are <span style="color: #76757a;">shadow</span> (for dim but visible text),
> <span style="background-color: #3a3a3a;">region</span> for selections, <span style="color: #195eb3;">emphasis</span>, and <span style="background-color: #195eb3;">highlight</span>.
> As above, <span style="color: #0097a7;">code</span> is used for code-like text.
> 
> Lastly, we have the 'message severity' faces: <span style="color: #ed333b;">error</span>, <span style="color: #e5a509;">warning</span>,
> <span style="color: #25a268;">success</span>, <span style="color: #26c6da;">info</span>, <span style="color: #76757a;">note</span>, and <span style="color: #33d079;">tip</span>.
> 
> Remember that all these faces (and any user or package-defined ones) can
> arbitrarily nest and overlap, <span style="color: #33d079;background-color: #3a3a3a;">like <span style="font-weight: 700;font-style: italic;">so</span></span>.</pre>
> 
> ```
> 
> ```julia-auto
> \begingroup
> \ttfamily
> \setlength{\parindent}{0pt}
> \setlength{\parskip}{\baselineskip}
> 
> The basic font-style attributes are {\fontseries{b}\selectfont bold}, {\fontseries{l}\selectfont light}, {\fontshape{it}\selectfont italic},\\
> \underline{underline}, and {strikethrough}.
> 
> In terms of color, we have named faces for the 16 standard terminal colors:\\
> {\color[HTML]{1c1a23}\(\blacksquare\)} {\color[HTML]{a51c2c}\(\blacksquare\)} {\color[HTML]{25a268}\(\blacksquare\)}
> {\color[HTML]{e5a509}\(\blacksquare\)} {\color[HTML]{195eb3}\(\blacksquare\)} {\color[HTML]{803d9b}\(\blacksquare\)}
> {\color[HTML]{0097a7}\(\blacksquare\)} {\color[HTML]{dddcd9}\(\blacksquare\)} \\
> {\color[HTML]{76757a}\(\blacksquare\)} {\color[HTML]{ed333b}\(\blacksquare\)} {\color[HTML]{33d079}\(\blacksquare\)} {\color[HTML]{f6d22c}\(\blacksquare\)} {\color[HTML]{3583e4}\(\blacksquare\)} {\color[HTML]{bf60ca}\(\blacksquare\)} {\color[HTML]{26c6da}\(\blacksquare\)} {\color[HTML]{f6f5f4}\(\blacksquare\)}
> 
> Since {\color[HTML]{0097a7}bright\_black} is effectively grey, we define two aliases for it:\\
> {\color[HTML]{0097a7}grey} and {\color[HTML]{0097a7}gray} to allow for regional spelling differences.
> 
> To flip the foreground and background colors of some text, you can use the\\
> {\color[HTML]{0097a7}inverse} face, for example: {\color[HTML]{803d9b}some \colorbox[HTML]{803d9b}{\color[HTML]{000000}inverse} text}.
> 
> The intent-based basic faces are {\color[HTML]{76757a}shadow} (for dim but visible text),\\
> \colorbox[HTML]{3a3a3a}{region} for selections, {\color[HTML]{195eb3}emphasis}, and \colorbox[HTML]{195eb3}{highlight}.\\
> As above, {\color[HTML]{0097a7}code} is used for code-like text.
> 
> Lastly, we have the 'message severity' faces: {\color[HTML]{ed333b}error}, {\color[HTML]{e5a509}warning},\\
> {\color[HTML]{25a268}success}, {\color[HTML]{26c6da}info}, {\color[HTML]{76757a}note}, and {\color[HTML]{33d079}tip}.
> 
> Remember that all these faces (and any user or package-defined ones) can\\
> arbitrarily nest and overlap, \colorbox[HTML]{3a3a3a}{\color[HTML]{33d079}like
> {\fontseries{b}\fontshape{it}\selectfont so}}.
> \endgroup
> 
> ```
> 
> ## [Customisation](@id stdlib-styledstrings-customisation)
> 
> ### [Face configuration files (`Faces.toml`)](@id stdlib-styledstrings-face-toml)
> 
> It is good for the name faces in the global face dictionary to be customizable.  
> Theming and aesthetics are nice, and it is important for accessibility reasons  
> too. A TOML file can be parsed into a list of [`Face`](@ref StyledStrings.Face) specifications that  
> are merged with the pre-existing entry in the face dictionary.
> 
> A [`Face`](@ref StyledStrings.Face) is represented in TOML like so:
> 
> ```toml
> [facename]
> attribute = "value"
> ...
> 
> [package.facename]
> attribute = "value"
> 
> ```
> 
> For example, if the `shadow` face is too hard to read it can be made brighter  
> like so:
> 
> ```toml
> [shadow]
> foreground = "white"
> 
> ```
> 
> Should the package `MyTables` define a `table_header` face, you could change its  
> colour in the same manner:
> 
> ```toml
> [MyTables]
> table_header.foreground = "blue"
> 
> ```
> 
> Light and dark face variants may be set under the top-level tables `[light]` and `[dark]`. For instance, to set the table header to magenta in light mode, one may use:
> 
> ```toml
> [light.MyTables]
> table_header.foreground = "magenta"
> 
> ```
> 
> On initialization, the `config/faces.toml` file under the first Julia depot (usually `~/.julia`) is loaded.
> 
> ### Face remapping
> 
> One package may construct a styled string without any knowledge of how it is intended to be used. Should you find yourself wanting to substitute particular faces applied by a method, you can wrap the method in `remapfaces` to substitute the faces applied during construction.
> 
> ```julia-auto
> StyledStrings.remapfaces(face"warning" => face"error") do
> styled"you should be {warning:very concerned}"
> end
> 
> ```
> 
> This changes the annotations in the styled strings produced within the `remapfaces` call.
> 
> ### Display-time face rebinding
> 
> While `remapfaces` is applied during styled string construction, it is also possible to change the meaning of each face while they are printed. This is done via `withfaces`.
> 
> ```julia-auto
> withfaces(face"yellow" => face"red", face"green" => face"blue") do
> println(styled"{yellow:red} and {green:blue} mixed make {magenta:purple}")
> end
> 
> ```
> 
> This is best applied when you want to temporarily change how faces appear,  
> without modifying the underlying string data.
> 
> ## [API reference](@id stdlib-styledstrings-api)
> 
> ### Styling and Faces
> 
> ```julia-auto
> StyledStrings.@styled_str
> StyledStrings.styled
> StyledStrings.@face_str
> StyledStrings.@defpalette!
> StyledStrings.@registerpalette!
> StyledStrings.@usepalettes!
> StyledStrings.Face
> StyledStrings.remapfaces
> StyledStrings.withfaces
> StyledStrings.SimpleColor
> StyledStrings.parse(::Type{StyledStrings.SimpleColor}, ::String)
> StyledStrings.tryparse(::Type{StyledStrings.SimpleColor}, ::String)
> StyledStrings.merge(::StyledStrings.Face, ::StyledStrings.Face)
> StyledStrings.recolor
> StyledStrings.blend
> StyledStrings.setface!
> 
> ```
> 
> ### Deprecated API
> 
> ```julia-auto
> StyledStrings.addface!
> 
> ```

---

<div class="post-metadata">

**Author:** ![tecosaur](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tecosaur/32/23206_2.png) [@tecosaur](https://discourse.julialang.org/u/tecosaur)\
**Post date:** [January 4, 2026, 7:28am UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/3 "2026-01-04T07:28:22Z")

</div>

I’ve just put [Face primacy and palettes by tecosaur · Pull Request #131 · JuliaLang/StyledStrings.jl · GitHub](https://github.com/JuliaLang/StyledStrings.jl/pull/131) up.

---

<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:** [January 4, 2026, 8:55am UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/4 "2026-01-04T08:55:36Z")

</div>

I’ve never used StyledStrings but your docs seem very clear to me on cursory first reading.

You don’t mention this but I wonder if it would be possible to specify colours by name based on Colors.jl? This may be more intuitive for users but probably adds overhead.

I terms of proof reading, I noticed two small things. You use colour/color inconsistently. And this sentence probably needs a comma not a full stop : Whether the extra capability afforded by this approach will vary face-by-face. It is most valuable when reaching for colors in-between those offered by the terminal, or based on the particular foreground/background shades or hues.

---

<div class="post-metadata">

**Author:** ![tecosaur](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tecosaur/32/23206_2.png) [@tecosaur](https://discourse.julialang.org/u/tecosaur)\
**Post date:** [January 4, 2026, 11:10am UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/5 "2026-01-04T11:10:25Z")

</div>

> [@TimG](#):
>
> I’ve never used StyledStrings but your docs seem very clear to me on cursory first reading.

That’s great to hear, thanks for taking a look Tim!

> [@TimG](#):
>
> You don’t mention this but I wonder if it would be possible to specify colours by name based on [Colors.jl](https://juliaregistries.github.io/General/packages/redirect_to_repo/Colors)? This may be more intuitive for users but probably adds overhead.

On one level, it already is 😁

```julia-repl

julia> using StyledStrings, Colors
[Info: Precompiling StyledStringsExt [acce9e33-b7be-5bfc-9e27-003c16e7ffe6]

julia> styled"{(fg=$(colorant\"salmon\")):text with a Color.jl provided foreground}"
"text with a Color.jl provided foreground"

```

but what the palette system now lets you do is:

```julia
@usepalettes! MyColors

styled"{salmon:text with a foreground colour provided by MyColors}" 

```

[Colors.jl/src/names\_data.jl at master · JuliaGraphics/Colors.jl · GitHub](https://github.com/JuliaGraphics/Colors.jl/blob/master/src/names_data.jl) could be easily translated to a large `@defpalette!` statement.

> [@TimG](#):
>
> I terms of proof reading, I noticed two small things. You use colour/color inconsistently.

Ah thanks. I’m not an American, so “colours” is native to me, but I need to use “color” for consistency with the rest of the Julia docs. Grrrr.

I’ve also rewritten the sentence you flagged. Thanks for giving that a read!

---

<div class="post-metadata">

**Author:** ![matthias314](https://avatars.discourse-cdn.com/v4/letter/m/a88e4f/32.png) [@matthias314](https://discourse.julialang.org/u/matthias314)\
**Post date:** [February 2, 2026, 2:49am UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/6 "2026-02-02T02:49:40Z")

</div>

> you can now set attributes to `"default"` to unset that attribute

What about unsetting _all attributes_? Suppose I want to redefine some existing face, say `julia_char`, from scratch. Would I have to unset all possible properties separately in my `faces.toml`? Or is this what you mean by “extending this to families/palettes”?

---

<div class="post-metadata">

**Author:** ![tecosaur](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tecosaur/32/23206_2.png) [@tecosaur](https://discourse.julialang.org/u/tecosaur)\
**Post date:** [February 2, 2026, 2:52am UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/7 "2026-02-02T02:52:08Z")

</div>

Unsetting all faces within a namespace, and then overriding them is something I’m interested in doing. The question is how.

Perhaps the best method might actually be to introduce a `.unset` / `.clear` “dummy attribute” that when set in `faces.toml`clears all attributes of that face/all faces in that namespace.

---

<div class="post-metadata">

**Author:** ![matthias314](https://avatars.discourse-cdn.com/v4/letter/m/a88e4f/32.png) [@matthias314](https://discourse.julialang.org/u/matthias314)\
**Post date:** [February 2, 2026, 2:58am UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/8 "2026-02-02T02:58:13Z")

</div>

Something else I’ve [come across today](https://discourse.julialang.org/t/how-to-access-a-face-defined-in-faces-toml/135387): It seems to me that currently there is no way to retrieve the `Face` object associated to a given name. Suppose I want to know what attributes `julia_char` has. How do I do it (without creating an annotated string and examining it)? There is the private function `getface(face::Symbol)`, but it combines `face` with the default attributes.

---

<div class="post-metadata">

**Author:** ![tecosaur](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tecosaur/32/23206_2.png) [@tecosaur](https://discourse.julialang.org/u/tecosaur)\
**Post date:** [February 2, 2026, 2:59am UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/9 "2026-02-02T02:59:58Z")

</div>

In the current version of StyledStrings, there is no good/public way.

In the new version, there’s the `face""` macro. E.g. `face"julia_char"`.

---

<div class="post-metadata">

**Author:** ![matthias314](https://avatars.discourse-cdn.com/v4/letter/m/a88e4f/32.png) [@matthias314](https://discourse.julialang.org/u/matthias314)\
**Post date:** [February 2, 2026, 3:02am UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/10 "2026-02-02T03:02:22Z")

</div>

> [@tecosaur](#):
>
> In the new version, there’s the `face""` macro

Are you also planning a function that one can call it with a variable (of type `Symbol`)?

---

<div class="post-metadata">

**Author:** ![tecosaur](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tecosaur/32/23206_2.png) [@tecosaur](https://discourse.julialang.org/u/tecosaur)\
**Post date:** [February 2, 2026, 3:03am UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/11 "2026-02-02T03:03:07Z")

</div>

Currently no. Is there a use case you have in mind?

---

<div class="post-metadata">

**Author:** ![matthias314](https://avatars.discourse-cdn.com/v4/letter/m/a88e4f/32.png) [@matthias314](https://discourse.julialang.org/u/matthias314)\
**Post date:** [February 2, 2026, 3:06am UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/12 "2026-02-02T03:06:44Z")

</div>

For the [use case I’m having in mind](https://github.com/JuliaLang/julia/pull/60899) a macro would suffice. However, it seems strange to me that there is a dictionary of face definitions that essentially has a `setindex!` method, but no `getindex`.

---

<div class="post-metadata">

**Author:** ![tecosaur](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tecosaur/32/23206_2.png) [@tecosaur](https://discourse.julialang.org/u/tecosaur)\
**Post date:** [February 2, 2026, 3:08am UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/13 "2026-02-02T03:08:41Z")

</div>

Currently it’s a dictionary, but in the new version it’s a bit more complex: you can access `face"julia_char"` during precompilation while the dictionary is empty.

---

<div class="post-metadata">

**Author:** ![matthias314](https://avatars.discourse-cdn.com/v4/letter/m/a88e4f/32.png) [@matthias314](https://discourse.julialang.org/u/matthias314)\
**Post date:** [February 2, 2026, 3:11am UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/14 "2026-02-02T03:11:11Z")

</div>

Here is another point: From reading the existing docs (I’ve only glanced over the new version), it’s unclear to me how faces defined by packages interact with `faces.toml`. The package will probably have some ` __init__ ()` method that defines default values for the faces it uses, and `faces.toml` may contain user preferences. In which order are they read? If the package is loaded at a later point (say, in an interactive session), will the package default overwrite the user preference?

---

<div class="post-metadata">

**Author:** ![tecosaur](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tecosaur/32/23206_2.png) [@tecosaur](https://discourse.julialang.org/u/tecosaur)\
**Post date:** [February 2, 2026, 3:13am UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/15 "2026-02-02T03:13:00Z")

</div>

> [@matthias314](#):
>
> In which order are they read? If the package is loaded at a later point (say, in an interactive session), will the package default overwrite the user preference?

Regardless of the order that `faces.toml` and package face definitions are loaded into the global state, the customisations from `faces.toml` are applied on top of the package-provided face definitions. This is because the modifications are stored in a separate layer.

Thanks for mentioning this isn’t clear, I’ll make an adjustment to the docs.

---

<div class="post-metadata">

**Author:** ![tecosaur](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tecosaur/32/23206_2.png) [@tecosaur](https://discourse.julialang.org/u/tecosaur)\
**Post date:** [February 2, 2026, 7:21am UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/16 "2026-02-02T07:21:05Z")

</div>

> [@tecosaur](#):
>
> Perhaps the best method might actually be to introduce a `.unset` / `.clear` “dummy attribute” that when set in `faces.toml`clears all attributes of that face/all faces in that namespace.

For clarity, the “perhaps” is here because I’m not settled on this, and feedback would be helpful 🙂

---

<div class="post-metadata">

**Author:** ![matthias314](https://avatars.discourse-cdn.com/v4/letter/m/a88e4f/32.png) [@matthias314](https://discourse.julialang.org/u/matthias314)\
**Post date:** [February 2, 2026, 12:33pm UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/17 "2026-02-02T12:33:53Z")

</div>

> [@tecosaur](#):
>
> Is there a use case you have in mind?

Here is a potential use case: Suppose you want to write a “face viewer” that let’s you examine the existing faces. Users would be able to search for faces having certain attributes and so on. For this you need access to the attributes of the existing faces. You must have that functionality in `StyledStrings` anyway. Why not expose it?

There are certainly other use cases that I can’t think of right now. For this reason, I believe that users should be able to read any data they can set unless there is a good reason against it. There are many examples in Julia where this is not the case. For instance, there is no official way to access the fields of a generator `(f(x) for x in iter)` or of a function composition `f ∘ g`. Why am I not allowed to examine these data structures?

---

<div class="post-metadata">

**Author:** ![tecosaur](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tecosaur/32/23206_2.png) [@tecosaur](https://discourse.julialang.org/u/tecosaur)\
**Post date:** [February 2, 2026, 3:58pm UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/18 "2026-02-02T15:58:36Z")

</div>

> [@matthias314](#):
>
> Why not expose it?

I’m trying to be a bit cautions with what’s exposed as a public API. Yes, technically we’ve said that this library’s API is experimental and subject to change, but I want to severely minimise the number of breaking changes.

> [@matthias314](#):
>
> For this reason, I believe that users should be able to read any data they can set unless there is a good reason against it.

From a library perspective, this is already true, FWIW.

> [@matthias314](#):
>
> Suppose you want to write a “face viewer” that let’s you examine the existing faces. Users would be able to search for faces having certain attributes and so on. For this you need access to the attributes of the existing faces.

This is actually something I want to make, I just haven’t worked out when/how yet 🙂

---

<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:** [February 4, 2026, 11:17pm UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/19 "2026-02-04T23:17:03Z")

</div>

I have a use case I’m struggling to figure out.

I want to be able to assign a StyledString to an Excel cell in XLSX.jl.

```julia-auto
sheet["A1"] = styled"The {bold:{italic:quick} {(foreground=#cd853f):brown} fox} jump over the {(foreground=#008000):lazy} dog"

```

Excel (and now XLSX.jl) can support most of the options offered by StyledStrings, However, I cannot figure how to unpack the formatting of a StyledString to convert it to the XML representation Excel needs. I can do it for the obvious, easy cases (like the one above), but not, for example, for arbitrary, user defined faces.

The primary reason for wanting to do this is that I’m preparing an Excel backend for PrettyTables.jl and PrettyTables.jl supports StyledStrings. I could say simply that the Excel backend doesn’t but, on the face of it (no pun intended!), everything necessary is supported. It is just that I can’t figure out how to do it in a fully general way.

Any hints would be gratefully received!

---

<div class="post-metadata">

**Author:** ![tecosaur](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tecosaur/32/23206_2.png) [@tecosaur](https://discourse.julialang.org/u/tecosaur)\
**Post date:** [February 5, 2026, 3:23am UTC](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847/20 "2026-02-05T03:23:52Z")

</div>

For turning a styled string into Excel-style formatted XML, you’ll probably want to implement a similar function to the `show(::IO, ::MIME"text/plain", ::Base.AnnotatedString{>:Face})` (NB: the `{>:Face}` bit is going to be new, you’ll want a `@static if VERSION >= v"1.14-"` around that) or `show(::IO, ::MIME"text/html", ::Base.AnnotatedString{>:Face})`.

> <https://github.com/JuliaLang/StyledStrings.jl/blob/5224fa097f45635933f9d51f6fbdc706d73049ef/src/io.jl#L369-L520>

There are two-three bits of technically private API that you’ll want to use, but that I think are pretty stable, and good candidates for future public API:

1. `Base.eachregion`
2. `StyledStrings.getface`
3. `StyledStrings.rgbcolor` (1.14+ only though)

The rough sketch for a pre-1.14 and post-1.14 compatible function looks like this (with the aforementioned tweak to the method signature):

```julia-auto
function myformatter(io::IO, s::Union{<:AnnotatedString{>:Face}, SubString{<:AnnotatedString{>:Face}}})
    StyledStrings.load_customisations!() # optional
    lastface::Face = getface()
    for (str, styles) in Base.eachregion(s)
        face = StyledStrings.getface(styles)
        my_output_with_change_in_style(io, lastface, face, str)
        lastface = face
    end
end

```

[Next page](https://discourse.julialang.org/t/prospective-changes-to-annotatedstring-and-styledstrings/134847.md?page=2)
