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.

JavaScript
Frontend
3 snippets
logging
input-output
code-template
carlosherrera

By @carlosherrera

May 12, 2026

·

Updated May 18, 2026

1,187 views

34

4.2 (11)

// ANSI escape codes are simple. \x1b[31m is red, \x1b[0m is reset.
// process.stdout.isTTY is true when output is a real terminal; when piped to
// a file or another process we drop the codes so log files stay clean.

const color = (() => {
    const isTTY = (typeof process !== 'undefined' && process.stdout && process.stdout.isTTY) || false;
    const wrap = (open, close) => (s) => isTTY ? `\x1b[${open}m${s}\x1b[${close}m` : String(s);
    return {
        bold:    wrap(1, 22),
        dim:     wrap(2, 22),
        red:     wrap(31, 39),
        green:   wrap(32, 39),
        yellow:  wrap(33, 39),
        blue:    wrap(34, 39),
        magenta: wrap(35, 39),
        cyan:    wrap(36, 39),
        gray:    wrap(90, 39),
    };
})();

function stamp() {
    const d = new Date();
    return d.toISOString().slice(11, 19);
}

const logger = {
    info:  (...a) => console.log(color.gray(stamp()), color.cyan('info'),  ...a),
    warn:  (...a) => console.log(color.gray(stamp()), color.yellow('warn'),  ...a),
    error: (...a) => console.error(color.gray(stamp()), color.red('error'), ...a),
    ok:    (...a) => console.log(color.gray(stamp()), color.green('ok'),    ...a),
    step:  (label, fn) => {
        console.log(color.gray(stamp()), color.blue('step'), color.bold(label));
        return fn();
    },
};

logger.info('starting build');
logger.step('compile', () => {
    logger.info('compiling lib/');
    logger.warn('deprecated import detected in lib/legacy.js');
});
logger.ok('build complete');
logger.error('but tests failed');

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.