#6634·textual

ANSI themes: text-opacity rasterizes ANSI colors to the Monokai fallback (HeaderTitle turns neon on blur)

Author: oswaldschCreated Jul 7, 2026Updated Aug 24, 2026

Have you checked closed issues? (https://github.com/Textualize/textual/issues?q=is%3Aissue+is%3Aclosed) Yes

Have you checked against the most recent version of Textual? (https://pypi.org/search/?q=textual) Yes

The bug

Under an ANSI theme (ansi-dark / ansi-light), applying text-opacity < 100% to an ANSI-colored region renders it with hardcoded RGB from Rich's Monokai DEFAULT_TERMINAL_THEME instead of the terminal's own ANSI palette.

This happens out of the box with the built-in HeaderTitle. App.DEFAULT_CSS fades it on blur:

css
&:blur HeaderTitle {
    text-opacity: 50%;
}

So a header with background: ansi_green renders in the terminal's green while focused, but the moment the window loses focus (AppBlur), the title flips to a fixed #98e024 (neon lime) that ignores the terminal and matches no other ansi_green element in the app. text-opacity is a blend against the background, which needs concrete RGB; ANSI colors are symbolic, so they get resolved through Monokai (green = #98e024) to do the math.

Complete working example (runs without modification, no terminal or interaction needed, it drives AppBlur itself and prints the rendered styles):

python
import asyncio

from textual import events
from textual.app import App, ComposeResult
from textual.widgets import Header


class Repro(App):
    CSS = "Header { background: ansi_green; color: ansi_black; }"

    def on_mount(self) -> None:
        self.theme = "ansi-dark"

    def compose(self) -> ComposeResult:
        yield Header()


def title_style(app):
    for segment in app.screen._compositor.render_strips()[0]:
        if segment.text.strip() and segment.text.strip() != "⭘":
            return segment.style


async def main():
    app = Repro()
    async with app.run_test() as pilot:
        await pilot.pause()
        print("focused:", title_style(app))
        app.post_message(events.AppBlur())
        await pilot.pause()
        print("blurred:", title_style(app))


asyncio.run(main())

Output:

focused: color(0) on color(2)
blurred: #597d1f on #98e024

Focused, the title is symbolic ANSI (color(0) on color(2)) so the terminal picks the shade. Blurred, it is rasterized to Monokai RGB (#98e024).

Expected: under an ANSI theme the title stays in the terminal's palette on blur (either full ANSI green, or a genuinely dimmed ANSI color) rather than switching to a fixed off-palette RGB.

Actual: it renders as #98e024, Rich's Monokai green.

This is not header-specific, any text-opacity / opacity blend on an ANSI-colored widget under an ANSI theme hits it. The header is just the first place users see it, because the default blur rule triggers it on every focus change.

Workaround, for anyone who lands here, re-pin the title to full opacity:

css
Screen:blur HeaderTitle {
    text-opacity: 100%;
}
textual diagnose

Textual Diagnostics

Versions

Name Value
Textual 8.2.8
Rich 15.0.0

Python

Name Value
Version 3.14.6
Implementation CPython
Compiler GCC 16.1.1 20260430

Operating System

Name Value
System Linux
Release 7.1.2-arch3-1

Terminal

Name Value
Terminal Application Kitty
TERM xterm-kitty
COLORTERM truecolor