ANSI escapes

What ANSI escape sequences are, how terminal control codes work, and practical usage with examples.

topic: Linux

release date: 2026-03-13

introduction

ANSI escape sequences are short byte sequences that instruct a terminal emulator to perform actions such as moving the cursor, changing colors, clearing parts of the screen, or switching modes. They are lightweight control instructions built on top of ASCII and were standardized from the original VT100 and ANSI X3.64 families of control codes. Modern terminal emulators implement most of these sequences and a number of private extensions.

In practice you will see escape sequences in scripts, command output, programs using ncurses, and when manipulating terminals directly from shell. Correct use of escape codes allows creating full-screen text UIs, colored logs, progress indicators, and robust cleanup on exit.

Note - always test escapes on your target terminal emulator. Behavior can vary between terminals, tmux, remote shells, and virtual consoles.

sequence entries

An escape sequence usually starts with the Escape character (decimal 27) followed by a sequence of bytes that indicate the class and arguments. Common ways to represent Escape in source are:

  • Ctrl-Key - ^[
  • octal - \033
  • hex - \x1B
  • unicode - \u001b
  • decimal - 27

The most common family is the CSI sequences. CSI is the Control Sequence Introducer. It can be formed by ESC [ or a single-byte CSI code. CSI sequences accept optional numeric parameters separated by semicolons and end with a single letter or symbol that defines the action.

Arguments are separated by semicolons. Missing arguments are interpreted as defaults (often 1 or 0 depending on the command).

basic example

# set bold and red foreground
printf '\x1b[1;31mHello\x1b[0m\n'

This sequence uses CSI [1;31m to set attributes then resets with [0m.

general/basic codes

Before CSI sequences we often rely on basic ASCII control characters for simple behavior. These are important because they are widely portable and often easier to reason about when writing scripts.

Sequence Name Description
\e[0m Reset Reset all text attributes and return to terminal default style.
\e[1m Bold Enable bold or increased intensity text.
\e[2m Dim Enable faint or reduced intensity text.
\e[3m Italic Enable italic text style. Not supported by every terminal.
\e[4m Underline Enable underline text decoration.
\e[5m Slow blink Enable slow blinking text.
\e[6m Rapid blink Enable rapid blinking text.
\e[7m Inverse Swap foreground and background colors.
\e[8m Hidden Hide text. Characters exist but are not visible.
\e[9m Strikethrough Draw a line through the text.
\e[21m Reset bold Disable bold attribute.
\e[22m Reset bold and dim Disable both bold and faint text modes.
\e[23m Reset italic Disable italic text.
\e[24m Reset underline Disable underline.
\e[25m Reset blink Disable blinking text.
\e[27m Reset inverse Restore normal foreground and background colors.
\e[28m Reset hidden Make hidden text visible again.
\e[29m Reset strikethrough Disable strikethrough decoration.
\a BEL Terminal bell sound.
\b Backspace Move cursor one character backwards.
\t Horizontal tab Move cursor to the next tab stop.
\n New line Move cursor to next line.
\r Carriage return Move cursor to the beginning of the line.
\v Vertical tab Move cursor vertically to the next line tab stop.
\f Form feed Advance to the next page or clear the screen on some terminals.
Note - some languages or shells may not support the shorthand \e. Prefer explicit octal or hex escapes when portability matters.

moving the cursor

Cursor movement sequences let you position output precisely. These are the building blocks for progress bars, interactive prompts, and full-screen text UIs.

ESC Code SequenceDescription
ESC[HMove cursor to home position (1,1 in many terminals)
ESC[{line};{col}HMove cursor to line, column
ESC[#AMove cursor up # lines
ESC[#BMove cursor down # lines
ESC[#CMove cursor right # columns
ESC[#DMove cursor left # columns
ESC[6nRequest cursor position - terminal replies with ESC[row;colR
ESC[sSave cursor position
ESC[uRestore saved cursor position
ESC MMove cursor up one line, scrolling if needed
When using saved positions be careful with nested saves. Matching restores is your responsibility.

example - simple progress spinner

spinner() {
	local i=0
	local chars='|/-\'
	while true; do
		printf '\r%c' "${chars:i%4:1}"
		i=$((i+1))
		sleep 0.1
	done
}
# run spinner in background, PID stored so we can kill it later
spinner & sp=$!
sleep 2
kill "$sp"
printf '\r \r'  # clear spinner

In the spinner we use carriage return and printing to the same line instead of full cursor moves for simplicity.

line/terminal erasing

Erase sequences let you clear parts of the screen or the current line. They do not implicitly move the cursor unless documented.

ESC SequenceDescription
ESC[J / ESC[0JErase from cursor to end of screen
ESC[1JErase from cursor to start of screen
ESC[2JErase entire screen
ESC[K / ESC[0KErase from cursor to end of line
ESC[1KErase from start of line to cursor (inclusive)
ESC[2KErase entire line
Note - erasing does not reposition the cursor. Use \r to return to the start of line when needed.

example - progress with line erase

for i in {1..40}; do
	printf '\r%s' "progress $i/40"
	sleep 0.05
done
printf '\r\x1b[2KDone\n'

basic 8-16 colors

Most terminals support a basic palette of 8 colors and often an extended set of 16 colors which include a bright variant. Colors are controlled by SGR parameters within CSI sequences. Multiple attributes can be combined with semicolons.

Color nameforegroundbackground
Black3040
Red3141
Green3242
Yellow3343
Blue3444
Magenta3545
Cyan3646
White3747

Bright colors use 90 to 97 for foregrounds and 100 to 107 for backgrounds. Alternatively, some terminals map bold to bright colors when available.

examples

# bold red text
printf '\x1b[1;31mBold red\x1b[0m\n'

# bright green foreground (if supported)
printf '\x1b[92mBright green\x1b[0m\n'

# dim white on red background
printf '\x1b[2;37;41mDim white on red\x1b[0m\n'

256 colors

256-color mode extends the palette via indexed colors. Use SGR parameters 38;5;{ID}m for foreground and 48;5;{ID}m for background. IDs range 0 to 255.

IDs 0-7 correspond to the standard 8 colors, 8-15 are bright variants, 16-231 are a 6x6x6 color cube, and 232-255 are grayscale ramp.

Below is an illustration of the 256 color palette for reference.

256 color palette
The screenshot is from one of my scripts - 256colors.
When choosing 256 colors, test both dark and light terminal themes. Perceived color depends on terminal palette and gamma.

example

# orange-ish from 256 palette (example ID 208)
printf '\x1b[38;5;208mOrange text\x1b[0m\n'

# background with ID 236 (dark gray)
printf '\x1b[48;5;236m  \x1b[0m\n'

RGB true colors

Truecolor mode allows precise 24-bit colors using the sequences 38;2;r;g;b for foreground and 48;2;r;g;b for background. Most modern terminal emulators support truecolor.

example

# bright orange via RGB
printf '\x1b[38;2;255;165;0mTruecolor orange\x1b[0m\n'

# subtle background
printf '\x1b[48;2;20;20;20m   \x1b[0m\n'
Truecolor support varies. If compatibility is required fall back to 256 colors or palette mapping.

screen modes

Screen mode sequences change terminal modes such as switching to an alternate buffer, enabling application cursor keys, or switching line wrapping. These are often implemented as DEC private modes using sequences that begin with ESC[?.

SequenceDescription
ESC[?1049hEnable alternate screen buffer and save cursor
ESC[?1049lDisable alternate screen buffer and restore
ESC[?25lHide cursor
ESC[?25hShow cursor
ESC[?1hEnable application cursor keys
ESC[?1lDisable application cursor keys
Using the alternate buffer is common for full-screen programs so the original screen is restored on exit.

example - enter alternate buffer and hide cursor

# save state, switch to alternate buffer, hide cursor
printf '\x1b[?1049h\x1b[?25l'
# ... full-screen UI ...
# restore main buffer and show cursor
printf '\x1b[?1049l\x1b[?25h'

private modes

Private modes are vendor extensions that begin with ESC[?. They are widely used but nonstandardized. Many emulators implement a similar set of private modes for convenience.

  • ESC[?25l - hide cursor
  • ESC[?25h - show cursor
  • ESC[?1049h - enable alternate buffer
  • ESC[?1049l - disable alternate buffer
  • ESC[?2004h - enable bracketed paste
  • ESC[?2004l - disable bracketed paste
Tmux and screen may filter or translate some private modes. Test interactions when running under multiplexers.

keyboard strings

Some control sequences let you program keys or request special strings from the terminal. These are less commonly used in modern scripts but are part of termcap/terminfo and older terminal control sets.

Keyboard string syntax is often something like ESC[{code};{string}p where code identifies the key and string defines the new content. Terminfo/termcap provide higher level access to these features when writing portable applications.

Key Code SHIFT+code CTRL+code ALT+code
F10;590;840;940;104
F20;600;850;950;105
F30;610;860;960;106
F40;620;870;970;107
F50;630;880;980;108
F60;640;890;990;109
F70;650;900;1000;110
F80;660;910;1010;111
F90;670;920;1020;112
F100;680;930;1030;113
F110;1330;1350;1370;139
F120;1340;1360;1380;140
HOME (num keypad)0;71550;119--
UP ARROW (num keypad)0;7256(0;141)--
PAGE UP (num keypad)0;73570;132--
LEFT ARROW (num keypad)0;75520;115--
RIGHT ARROW (num keypad)0;77540;116--
END (num keypad)0;79490;117--
DOWN ARROW (num keypad)0;8050(0;145)--
PAGE DOWN (num keypad)0;81510;118--
INSERT (num keypad)0;8248(0;146)--
DELETE (num keypad)0;8346(0;147)--
HOME(224;71)(224;71)(224;119)(224;151)
UP ARROW(224;72)(224;72)(224;141)(224;152)
PAGE UP(224;73)(224;73)(224;132)(224;153)
LEFT ARROW(224;75)(224;75)(224;115)(224;155)
RIGHT ARROW(224;77)(224;77)(224;116)(224;157)
END(224;79)(224;79)(224;117)(224;159)
DOWN ARROW(224;80)(224;80)(224;145)(224;154)
PAGE DOWN(224;81)(224;81)(224;118)(224;161)
INSERT(224;82)(224;82)(224;146)(224;162)
DELETE(224;83)(224;83)(224;147)(224;163)
PRINT SCREEN----0;114--
PAUSE/BREAK----0;0--
BACKSPACE88127(0)
ENTER13--10(0
TAB90;15(0;148)(0;165)
NULL0;3------
A976510;30
B986620;48
C996730;46
D1006840;32
E1016950;18
F1027060;33
G1037170;34
H1047280;35
I1057390;23
J10674100;36
K10775110;37
L10876120;38
M10977130;50
N11078140;49
O11179150;24
P11280160;25
Q11381170;16
R11482180;19
S11583190;31
T11684200;20
U11785210;22
V11886220;47
W11987230;17
X12088240;45
Y12189250;21
Z12290260;44
14933--0;120
2506400;121
35135--0;122
45236--0;123
55337--0;124
65494300;125
75538--0;126
85642--0;126
95740--0;127
04841--0;129
-4595310;130
=6143---0;131
[91123270;26
]93125290;27
\92124280;43
;5958--0;39
'3934--0;40
,4460--0;51
.4662--0;52
/4763--0;53
`96126--(0;41)
ENTER (keypad)13--10(0;166)
/ (keypad)4747(0;142)(0;74)
* (keypad)42(0;144)(0;78)--
- (keypad)4545(0;149)(0;164)
+ (keypad)4343(0;150)(0;55)
5 (keypad)(0;76)53(0;143)--
Most uses today rely on terminfo and libraries like ncurses to avoid manually manipulating keyboard mappings.

usage and examples

Here are practical, battle-tested patterns mixing raw escape sequences and small shell snippets. Each example has a short explanation, then a code wrapper with either raw escapes or a tiny script. The goal is: readable, copy/pasteable, and predictable. Test on the target terminal (xterm, kitty, alacritty, gnome-terminal, Termux, tmux etc.) before deploying.

Note - I use \e in human-friendly examples and \x1b in scripts where portability matters. Both represent the Escape (27) character.

1) Moving up and clearing lines - in-place multi-line updates

Common trick to update N previous lines: move cursor up N lines, go to start of line, erase line or erase to end of screen, then print new content. Useful for progress that writes several lines or status blocks.

# raw sequence example - move up one line, return start, erase to end of screen
printf '\e[1A\r\e[J'

Explanation:

  • \e[1A - move cursor up 1 line
  • \r - carriage return (start of current line)
  • \e[J - erase from cursor to end of screen

2) Multi-line status block - update previous 3 lines atomically

Write a status block and later overwrite it by saving the cursor after writing and restoring + rewriting when needed.

# minimal pattern - write 3-line block and update it later
printf '\e[s'            # save cursor (position after the block)
printf 'job: running\n'
printf 'progress: 10%\n'
printf 'eta: 00:10\n'

# ... later update: restore cursor, move up 3 lines, rewrite block, restore end
printf '\e[u'            # restore saved position (cursor at end of block)
printf '\e[3A'         # move up 3 lines
printf '\r\e[2Kjob: running\n'   # rewrite line1 (erase whole line then print)
printf '\r\e[2Kprogress: 42%%\n'
printf '\r\e[2Keta: 00:06\n'
printf '\e[s'            # re-save new end position

Notes:

  • Use \e[2K to erase the whole current line before printing new content.
  • Saving cursor with \e[s and restoring with \e[u is handy but be careful when nested or when other code may change the saved stack.

3) Spinner that hides cursor and restores it on exit

Hide the cursor while spinner is running and ensure the cursor is restored on any exit using a trap.

#!/usr/bin/env bash
trap 'printf "\e[?25h\e[0m\n"; exit' EXIT
printf '\e[?25l'    # hide cursor
chars='|/-\'
i=0
while true; do
	printf '\r%s' "${chars:i%4:1}"
	i=$((i+1))
	sleep 0.08
done

Key points:

  • \e[?25l hides cursor; \e[?25h shows it.
  • Trap EXIT ensures cursor is restored no matter what.

4) Clear screen but keep cursor at top-left

Two variants - clear everything and home, or clear region below cursor.

# clear whole screen and move to home
printf '\e[2J\e[H'

# clear from cursor to end of screen only
printf '\e[J'

5) Move relative and reposition - single-line rewrite

Useful when you want to update part of the line: move left n columns and overwrite part of it.

# print then overwrite last 10 chars
printf 'Download: [##########]\n'
sleep 1
printf '\e[1A'     # move up 1
printf '\r'        # go to start of line
printf 'Download: [####......]\n'

6) Raw escape combos - compact snippets and what they do

Below are small raw examples - each line is a single atomic action you can copy and test. I give the raw escape then a short explanation.

# raw snippet: move up 1, go to start, erase to end of screen
\e[1A\r\e[J

# raw snippet: move cursor to row 10 col 5
\e[10;5H

# raw snippet: save cursor, hide cursor, switch to alt buffer
\e[s\e[?25l\e[?1049h

# raw snippet: restore main buffer, show cursor, restore cursor pos
\e[?1049l\e[?25h\e[u

# raw snippet: set bright cyan foreground, dim, on red background
\e[2;96;41m

# raw snippet: 256-color foreground id 208
\e[38;5;208m

# raw snippet: truecolor foreground orange
\e[38;2;255;165;0m

Short notes:

  • \e[10;5H - absolute positioning (line 10, column 5). Many terminals count from 1.
  • Switching to alternate buffer \e[?1049h is what full-screen programs do. When you exit they restore main buffer.
  • Use \e[0m to reset attributes after color changes.

7) Progress bar with color gradient (256 -> fallback)

Attempt true gradient using 256 colors. If 256-color support is missing, fallback to simple colors. This snippet is self-contained and prints a one-line bar.

#!/usr/bin/env bash
cols=$(tput cols 2>/dev/null || echo 80)
width=$((cols-20))
for i in $(seq 0 $width); do
	id=$((16 + (i * 215 / width)))  # pick across 16..230 roughly
	printf '\r['
	printf '\e[48;5;%sm' "$id"
	for j in $(seq 0 $i); do printf ' '; done
	printf '\e[0m'
	for j in $(seq $((i+1)) $width); do printf ' '; done
	printf '] %3d%%' "$((i*100/width))"
	sleep 0.01
done
printf '\n'

Notes:

  • Uses 256-color background (48;5;{id}m) to draw the bar.
  • Ensure tput exists or fallback to a default width.

8) Request cursor position and parse robustly (tty-aware)

This version checks for a tty and uses a short timeout to avoid blocking when stdin/out are redirected.

#!/usr/bin/env bash
if [ ! -t 1 ]; then
	echo "not a tty"
	exit 1
fi

exec 3>&1 4<&0
# ask for cursor position
printf '\e[6n' > /dev/tty

# read response with small timeout
IFS=';' read -r -t 0.2 -d R -a pos < /dev/tty || { echo "no reply"; exit 2; }
row=${pos[0]#*[}
col=${pos[1]}
echo "row=$row col=$col"

# restore fds
exec 3>&- 4<&-

Caveats:

  • If the program is not attached to a tty (for example when piped), this will fail or block. Use checks and timeouts.
  • Different emulators may reply slightly differently; parsing must be defensive.

9) Bracketed paste - enable and handle pasted text safely

Bracketed paste tells the terminal to wrap pasted text between paste start and end markers, so the application can treat pasted input differently from typed input.

# enable bracketed paste
printf '\e[?2004h'

# pasted text will be wrapped:
# \e[200~pasted text\e[201~`

# disable bracketed paste
printf '\e[?2004l'

Implementation tip:

  • Read input and detect sequences \e[200~ and \e[201~ to handle paste blocks specially (strip newlines, avoid accidental shortcuts).

10) Quick cheatsheet of tiny useful raw sequences

Copy these to remember common micro-actions.

\e[0m      # reset attributes
\e[1m      # bold
\e[2m      # dim
\e[3m      # italic
\e[4m      # underline
\e[7m      # inverse
\e[8m      # hidden
\e[38;5;196m # set foreground to 256-color id 196
\e[48;2;10;10;10m # set background to RGB(10,10,10)
\e[6n     # request cursor pos (reply ESC[row;colR)
\e[s\e[u  # save and restore cursor

Do not embed uncontrolled user input into escape sequences. If you must, sanitize numeric arguments and avoid letting users inject arbitrary bytes that could be interpreted as control sequences.

about tput

tput is a utility that queries the terminfo database and outputs terminal control sequences in a portable way. It is useful when you want to avoid hardcoding escape sequences and prefer terminfo capability names.

Unlike raw escape sequences, tput uses the terminal description to select the correct sequence for the current terminal type. This makes scripts using tput more portable across different environments.

basic tput examples

# move cursor to row 10 col 5
tput cup 9 4

# clear the screen
tput clear

# hide and show cursor
tput civis
tput cnorm

# set bold
tput bold
# reset attributes
tput sgr0

tput can also output capability values like number of columns:

cols=$(tput cols)
lines=$(tput lines)
echo "size: $cols x $lines"
Use tput for portability and when writing scripts intended to run on many terminal types. For very new features (truecolor) terminfo entries may be missing and manual sequences are still required.

outro

ANSI escapes give you precise control over terminal behavior. Start with simple sequences for color and cursor movement, use terminfo and tput for portability, and rely on 256-color or truecolor only when you know the target environment supports it. Always restore terminal state on exit: show cursor, reset attributes, and restore buffer when appropriate.

If this article helps, consider a small donation via >link<.