The most surprising thing about Zsh color prompts and output is that the colors aren’t generated by Zsh itself, but by the terminal emulator you’re using, interpreting special escape sequences that Zsh embeds in its output.

Let’s see it in action. Imagine you want a prompt that shows your current directory in blue, followed by a green dollar sign.

PS1="%F{blue}%~%f \$ "

When Zsh processes this, it doesn’t "know" blue. Instead, it outputs the ANSI escape sequence for blue, then your current directory (represented by %~), then the escape sequence to reset colors, and finally the literal dollar sign. Your terminal emulator, like iTerm2, GNOME Terminal, or Windows Terminal, reads these sequences and renders the text accordingly.

Here’s how it breaks down:

  • %F{color}: This is a Zsh prompt escape sequence that tells Zsh to output the ANSI escape code for the specified color.
  • %~: This Zsh sequence expands to your current working directory, with your home directory abbreviated by ~.
  • %f: This Zsh sequence outputs the ANSI escape code to reset the text color to its default.
  • \$ : This is a literal dollar sign followed by a space. The \$ is a Zsh sequence that expands to # if you’re root, and $ otherwise.

So, the PS1="%F{blue}%~%f \$ " prompt string is translated by Zsh into something like this for the terminal:

\e[34m/Users/youruser/projects\e[0m $

The \e[ is the standard start of an ANSI escape sequence, and 34m is the code for blue foreground. 0m resets all attributes.

The power comes from Zsh’s ability to embed these sequences dynamically. You can have different colors based on context. For instance, to make the prompt red if the exit status of the last command was non-zero (indicating an error):

setopt PROMPT_SUBST
PS1="%F{blue}%~%f \$(if [[ \$? -ne 0 ]]; then echo "%F{red}"; fi)%f \$ "

Here, setopt PROMPT_SUBST is crucial. It tells Zsh to evaluate command substitutions and variable expansions before displaying the prompt. The \$(if [[ \$? -ne 0 ]]; then echo "%F{red}"; fi) part checks the exit status ($?). If it’s not zero, it echoes the ANSI code for red. The final %f ensures the color resets afterward, regardless of whether an error occurred.

This system is incredibly flexible. You can use named colors like red, blue, green, yellow, magenta, cyan, white, black, and their bright variants (brightred, etc.). You can also use 256-color codes by specifying them numerically, like %F{208} for a nice orange, or even true-color RGB values with %F{#RRGGBB}.

Beyond the prompt (PS1), you can leverage ANSI escape codes for coloring any output. For example, to color grep output:

alias grep='GREP_COLORS="auto" GREP_COLOR="\e[31m" grep --color=auto'

This alias sets the GREP_COLOR environment variable to the ANSI sequence for red (\e[31m) before running grep. The --color=auto flag tells grep to use these colors if the output is going to a terminal.

The mental model to build is that Zsh is a sophisticated text generator. It doesn’t "do" color; it generates the instructions for your terminal to do color. The prompt escapes (%F, %K, %B, %U, etc.) are just Zsh’s convenient way of inserting those raw ANSI escape sequences.

Most people don’t realize that the Zsh prompt escape sequences like %F{blue} are just syntactic sugar for raw ANSI escape codes like \e[34m. While Zsh provides convenience for common colors, you can embed any valid ANSI escape sequence directly into your prompt string or output using print -P. For example, to create a blinking red text: print -P "\e[5;31mBlinking Red!\e[0m". This means you’re not limited by Zsh’s predefined color names; you have the full power of ANSI control codes at your disposal, as long as your terminal emulator supports them.

The next step is often exploring how to manage these color sequences more robustly, perhaps using functions or external libraries to define and reuse color schemes.

Want structured learning?

Take the full Zsh course →