Community JavaScript Snippet
ANSI Colored Logger in 50 Lines
chalk is great. chalk plus chalk-template plus log-symbols plus signal-exit is heavy. Here is the dependency-free ANSI logger I drop into every Node script when I want one-line wins without 14 transitive packages.
ANSI Colored Logger in 50 Lines
chalk is great. chalk plus chalk-template plus log-symbols plus signal-exit is heavy. Here is the dependency-free ANSI logger I drop into every Node script when I want one-line wins without 14 transitive packages.
By @carlosherrera
May 12, 2026
·
Updated May 18, 2026
1,187 views
34
4.2 (11)
Two design choices are doing all the work. First, the colors are wrapped in a closure that checks isTTY once at startup; if you redirect output to a file the codes vanish, which is what every log analysis tool needs. Second, the logger is a plain object with one method per level rather than a class, which means you can destructure const { info, error } = logger if you want a chalk-style import. The step helper is the piece I use most: it tags the start of a logical phase and returns the wrapped function's value, so timing wrappers compose nicely. I have shipped this in every internal CLI; it covers the 80% case in fewer lines than the import statement for chalk.
Honoring NO_COLOR=1 is a one-line check and saves you from a support ticket every six months when someone runs your CLI under a CI system that mangles ANSI codes into garbage. The level filter does the matching trick for LOG_LEVEL: turn the level name into a number, compare to a fixed scale, drop messages above the threshold. The && short-circuit on each method means the suppressed call is essentially free; no string formatting, no console call. I do not implement a silent level here because forcing a logger that always writes to stderr keeps the CI failure-mode capture working; if you really want silence, set the level higher than error.
Two escape codes do everything: \r returns the cursor to the start of the current line, and \x1b[K clears from there to the end of the line. Together they let you rewrite the same line repeatedly without scroll, which is how every progress bar and spinner in npm packages works under the hood. The non-TTY branch is essential: when output is piped to a file, you do NOT want to overwrite lines (they look like garbage) or even use carriage returns; you want flat appended lines so log scrapers can parse them. A subtle pitfall I keep relearning: if the new line is SHORTER than the previous one, the trailing characters stick around without \x1b[K, producing 'compiling lib/y' when 'compiling lib' replaced 'compiling app/y'.
