consola
consola is a console wrapper for Node CLIs. Instead of console.log everywhere you get typed helpers like consola.info, consola.start, consola.success, consola.warn, and consola.box, each with its own icon, color, and numeric level, so a build script reads like a build script. Output goes through pluggable reporters: a fancy colored one by default, a plain one when it detects CI or a test runner, and a browser one in the browser. It can also swallow the rest of your program's output, since wrapConsole and wrapStd redirect console.log and raw stdout writes into the same pipeline. It declares no runtime dependencies and ships both ESM and CommonJS builds.
The most pleasant way to make a Node CLI's output look intentional, and the mocking and console-wrapping features are genuinely useful in tests. Treat the stalled release cadence as the main risk, and never reach for it as your server's logger.
Use it if
- You are writing a CLI or build script and want levelled, icon-prefixed output that automatically degrades to plain text in CI, without wiring up color detection yourself
- You want prompts, boxes, colors, and logging from one install: consola.prompt covers text, confirm, select, and multiselect, and consola/utils exports box, colors, and stripAnsi
- You need to capture output in tests: consola.mockTypes(() => vi.fn()) mocks every log type at once, and wrapAll() pulls stray console.log calls from your dependencies into the same mocks
- You are already in the unjs or Nuxt world, where consola is the logger those tools use, so it is in your tree either way
- You need a production server logger: consola formats for humans, and pino writes structured JSON an order of magnitude faster with serializers, redaction, and transports that consola has no answer for
- Releases matter to you: 3.4.2 shipped in March 2025 and the repo has kept getting commits since with no stable release behind them, so fixes you can read on main are not fixes you can install
- You ship to the browser and care about bytes: the default entry drags in the fancy reporter, and the README puts the saving from consola/basic or consola/core at about 80 percent of bundle size
- You rely on the default cancel behavior of prompts: pressing Ctrl+C resolves with the default value rather than aborting, so a confirm that defaults to yes will happily proceed with a destructive step unless you pass cancel: "reject"
- You are writing a library rather than an app: the shared global instance plus wrapConsole and wrapStd is process-wide mutable state, and hijacking a consumer's console is not yours to do
Setup reality
npm i consola and import { consola } from 'consola' works from both ESM and CommonJS, with types bundled and no dependencies to install, on Node ^14.18 or >=16.10. The surprises are all about which build you get and what level is active. Export conditions hand Node the fancy reporter and browsers the browser build, so the same import behaves differently by target and consola/basic, consola/core, and consola/browser exist for when you want to choose. Level defaults are environment-derived: std-env reports test and the default level drops to warn, which is why consola.info goes missing under vitest until you set consola.level yourself. CI detection also swaps the fancy reporter for the basic one, so your local screenshots will not match your pipeline logs. CONSOLA_LEVEL overrides the level in Node but is ignored by the browser and core builds.
Patterns
Use the built-in log typeslog-types
import { consola } from "consola";
consola.start("Building project...");
consola.info("Using consola");
consola.warn("A newer version is available");
consola.success("Project built!");
consola.error(new Error("Something broke"));
consola.box("I am a simple box");Each type carries its own level: fatal and error are 0, warn is 1, log is 2, info/success/start/ready/box are 3, debug 4, trace 5. Anything above the active level is dropped without a trace.
Create an instance instead of using the global onecreate-instance
import { createConsola } from "consola";
const logger = createConsola({
level: 4,
fancy: true,
formatOptions: { columns: 80, colors: true, compact: false, date: false },
});
logger.debug("now visible");fancy: true forces the colored reporter even in CI, which is what you want when your CI renders ANSI. The global consola is shared across every module in the process, so a library should make its own instance.
Control which logs appearlog-level
import { consola, LogLevels } from "consola";
consola.level = LogLevels.debug; // 4
consola.level = -999; // silent
// from the shell, Node builds only:
// CONSOLA_LEVEL=5 node cli.mjsThe default level is derived from the environment: debug when a debug env var is set, warn under a test runner, info otherwise. That warn default is why consola.info produces nothing inside vitest until you raise the level.
Tag logs by subsystemtagged-logger
import { consola } from "consola";
const db = consola.withTag("db");
const http = consola.withTag("http");
db.info("connected"); // [db] connected
http.warn("slow response");withTag returns a child instance that inherits the parent's options, including mocks, so mocking the parent in a test also mocks every tagged child. withScope is an alias for the same thing.
Emit JSON with a custom reportercustom-reporter
import { createConsola } from "consola";
const logger = createConsola({
reporters: [
{ log: (logObj) => console.log(JSON.stringify(logObj)) },
],
});
logger.log("foo bar");
// {"date":"...","args":["foo bar"],"type":"log","level":2,"tag":""}Passing reporters replaces the defaults entirely, so you lose colored output unless you add it back. Use addReporter to append one instead, for example a reporter that calls process.exit(1) when logObj.type is "fatal".
Redirect console and stdout into consolawrap-console
import { consola } from "consola";
consola.wrapAll(); // console + stdout/stderr
console.info("routed through consola");
process.stdout.write("so is this\n");
consola.restoreAll();wrapStd alone also catches console, since console writes to stdout, but wrapConsole is what preserves the type mapping so console.info arrives as an info log. Always restore in a finally block or later code inherits the patched globals.
Assert on log output in vitest or jestmock-in-tests
beforeAll(() => consola.wrapAll());
beforeEach(() => consola.mockTypes(() => vi.fn()));
test("logs the message", async () => {
await run();
const messages = consola.log.mock.calls.map((c) => c[0]);
expect(messages).toContain("your message");
});Re-mock in beforeEach or calls leak between tests. The callback receives (typeName, type); return a falsy value to leave a type unmocked, as in mockTypes((t) => t === "fatal" && vi.fn()).
Ask the user somethingprompts
const name = await consola.prompt("Project name?", {
type: "text",
placeholder: "my-app",
});
const ok = await consola.prompt("Deploy to production?", {
type: "confirm",
initial: false,
cancel: "reject", // Ctrl+C throws instead of returning the default
});Types are text, confirm, select, and multiselect only. The default cancel strategy resolves with the default value, so Ctrl+C on a confirm whose initial is true reads as a yes. Set cancel: "reject" on anything destructive.
Queue logs while something else owns the terminalpause-resume
consola.pauseLogs();
await renderProgressBar();
consola.resumeLogs(); // queued logs flush herePausing is global for that instance and its children, not scoped to a callback, so a thrown error between the two calls leaves logging paused for the rest of the process. Wrap it in try/finally.
Import a lighter entry pointsmaller-builds
// no fancy reporter, plain output
import { consola } from "consola/basic";
// browser reporter
import { consola } from "consola/browser";
// no reporter at all, bring your own
import { createConsola } from "consola/core";
const logger = createConsola({ reporters: [myReporter] });consola/core exports only createConsola, with no default instance and no reporter, so nothing prints until you add one. CONSOLA_LEVEL is not read by the core or browser builds.
Use the formatting helpers on their ownconsole-utils
import { colors, box, stripAnsi, align } from "consola/utils";
console.log(colors.green("ok"));
console.log(box("Deployed", { title: "done" }));
console.log(stripAnsi(coloredString).length);These are plain functions with no reporter behind them, so they ignore consola.level and print whatever you pass. stripAnsi is what you want before measuring string width or writing to a log file.
Log an object that looks like a log objectraw-logging
consola.log({ message: "hello" }); // prints: hello
consola.log.raw({ message: "hello" }); // prints: { message: 'hello' }A first argument with message or args keys is interpreted as a log object, so user data with those field names gets silently unwrapped. The .raw variant, available on every type, forces it to be treated as data.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pino | npm | The logs go to a machine (a log aggregator, a container runtime) rather than a person, and you need JSON, levels, and speed. |
| @clack/prompts | npm | You only want the interactive prompts; consola wraps clack for exactly this and clack gives you the full prompt set directly. |
| chalk | npm | You just want to color strings you print yourself and do not need levels, reporters, or console interception. |