log-update: rewriting one line of a terminal over and over
Log by overwriting the previous output in the terminal. Useful for rendering progress bars, animations, etc.
At a glance
- What is it?
- A small ESM package that overwrites its previous output in the terminal, so a spinner or a progress bar is one line that changes rather than a hundred lines that accumulate. The interesting part is the plumbing underneath, with six dependencies that do the measuring, the cursor handling and the ANSI-aware slicing.
- Who is it for?
- log-update suits a Node CLI that needs a spinner, a progress bar or a status line, and that wants the output to stay readable when the command is watched rather than scrollable. It does not suit a tool whose output is piped into a file, where overwriting means the file contains escape sequences and a single final frame, and it does not suit a program that needs several lines updating independently, because the API is one active region.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 34 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What it does, and the one-paragraph description
log-update logs by overwriting the previous output in the terminal, and the README says it is useful for rendering progress bars, animations and so on. The example is the classic spinner:
import logUpdate from 'log-update';
const frames = ['-', '\\', '|', '/'];
let index = 0;
setInterval(() => {
const frame = frames[index = ++index % frames.length];
logUpdate(
`
♥♥
${frame} unicorns ${frame}
♥♥
`
);
}, 80);The call is `logUpdate(text)` and it takes the whole block of text to display, replacing whatever was there before. Nothing accumulates in the scrollback, and the terminal shows one region that changes.
The one implementation detail the README leads with is that it performs partial redraws when possible to reduce flicker. That is the difference between a spinner that repaints the whole screen and one that only rewrites the lines that changed, and on a fast refresh rate the difference between usable and distracting.
Colouring the output is done with the usual suspects, yoctocolors or chalk, and the README links both. The module itself does not bring a colour library, which is the right division for a package this small.
The project is MIT licensed and the primary language is JavaScript. It is a pure ESM package, with the manifest setting `type` to `module` and exporting `index.js` with `index.d.ts` for the types. The engine requirement is Node 22 or newer, and the last push was on 2026-09-18 with releases v8.0.0 from 2026-04-05, v7.2.0 from 2026-02-27 and v7.1.0 from 2026-02-05.
The API, and what each method does to the scrollback
The API is small and the distinctions between the methods are the whole design.
`logUpdate(text)` logs to stdout, replacing the previous output. `logUpdate.clear()` clears the logged output. And `logUpdate.done()` persists the logged output, which is useful if you want to start a new log session below the current one.
Then `logUpdate.persist(text)` writes text to the terminal in a way that persists, similar to `console.log()`. The README explains the difference from the main method: unlike `logUpdate()` which updates in place, `persist()` writes to the terminal in a way that preserves the output in the scrollback history.
The example puts the three together in a way that makes the workflow obvious:
import logUpdate from 'log-update';
// Update in place
logUpdate('Processing...');
logUpdate('Still processing...');
// Write permanent output
logUpdate.persist('✓ Task complete');
// Continue updating
logUpdate('Next task...');So the pattern for a multi-step tool is to update in place while a step runs, persist a line when the step is done, and carry on. The tick mark in the example is not decoration; a persisted completion line is how the user knows what finished when the rest of the output has been overwritten.
There is a parallel set of methods for stderr. `logUpdateStderr(text)`, `logUpdateStderr.clear()`, `logUpdateStderr.done()` and `logUpdateStderr.persist(text)` mirror the stdout versions.
The reason stderr has its own namespace is that the two streams are separately redirected in most pipelines. A progress indicator belongs on stderr precisely so that stdout can carry the actual data, and having a separate instance for stderr is what makes that possible.
createLogUpdate, and the three options that matter
The `createLogUpdate(stream, options?)` function returns a `logUpdate` method that logs to the specified stream, which is how you use the library with something other than stdout or stderr.
The first option is `showCursor`, a boolean that defaults to false. It shows the cursor, and the README says this can be useful when a CLI accepts input from a user. The logic is straightforward: the library hides the cursor while it owns the terminal region, and a program that is waiting for input needs the cursor back.
import {createLogUpdate} from 'log-update';
// Write output but don't hide the cursor
const log = createLogUpdate(process.stdout, {
showCursor: true
});The other two options are `defaultWidth`, a number defaulting to 80, and `defaultHeight`, a number defaulting to 24. They are the width and the height to use when the stream does not provide a `columns` property and a `rows` property respectively. The README says they are useful when the output is piped, redirected, or in environments where the terminal size is not available.
import {createLogUpdate} from 'log-update';
// Use custom width when the stream doesn't provide columns
const log = createLogUpdate(process.stdout, {
defaultWidth: 120
});These two options are the ones to get right in practice. A stream that is not a terminal has no `columns`, and the library has to guess, and the guess of 80 columns is wrong on a modern terminal and on a narrow one. If your tool's output is piped and then rendered, the wrapping will be at 80 columns unless you say otherwise.
The six dependencies, and what each one is doing
The dependency list is the clearest description of how the module works, because a package that overwrites a line of terminal output has to do a surprising amount of measurement.
`ansi-escapes` at 7.3.0 supplies the escape sequences for cursor movement and line erasure, which is the raw material for the partial redraw. `cli-cursor` at 5.0.0 handles showing and hiding the cursor, which is what the `showCursor` option toggles. `strip-ansi` at 7.2.0 removes the escape sequences from a string so its real length can be measured, and `slice-ansi` at 9.0.0 does the same job while keeping the styling intact, so a line can be cut at a visible column without cutting a colour sequence in half.
Then `string-width` at 8.2.0 measures the display width of a string, which is not the same as its length, because a CJK character occupies two columns and a combining mark occupies none. And `wrap-ansi` at 10.0.0 wraps text to a width, again counting columns rather than characters.
Six dependencies, and five of them exist because the library has to know how wide a string is once it is on screen. That is the real cost of a terminal-aware package: the width of a line in a terminal is a function of the terminal, the locale and the string content, and getting it wrong means the overwrite lands in the wrong place.
The dev dependencies are equally telling. `xo` is the linter, `tsd` checks the type definitions against `index.test-d.ts`, and `node --test` is the test runner, with the test script being `xo && node --test && tsd`. There is also `terminal.js`, which is a terminal emulator used in the tests, and `wcwidth` for character width, which is how the width measurements are checked.
The example files, and the shape of the package
The repository is small and flat, which is what a package like this should look like.
There are three example files at the root: `example.js`, `example-no-scroll.js` and `example-persist-output.js`. The names tell you what the library is actually good for, because a spinner that leaves the scrollback full of frames is the common complaint about this pattern, and an example named for not scrolling is the answer to it.
Then `index.js` with `index.d.ts` beside it, and `index.test-d.ts` for the type tests, and `test.js` for the runtime tests. There is a `screenshot.gif` which is what the README shows, and the `license` and `readme.md` files are lowercase, following the convention that npm packages use an exact lowercase `license` and `readme.md` so they render on the package page.
The manifest is worth one more look for its packaging choices. The `files` array lists only `index.js` and `index.d.ts`, so nothing else is published, and `sideEffects` is false, which lets a bundler drop the import entirely if nothing uses it. The exports map has just `types` and `default`, so there is no CommonJS entry point, which is consistent with `type: module` and with the Node 22 floor.
The keyword list is a good summary of the intent: log, logger, logging, cli, terminal, term, console, shell, update, refresh, overwrite, output, progress, bar, animation. The last three are the real use cases, and the rest is how people search for the thing that solves them.
Where log-update is the wrong choice
The first wrong case is a tool whose output is piped or redirected to a file. Overwriting means the file receives escape sequences and, if the program ends without calling `done()`, a single final frame rather than the history. For a command that is meant to be read later, that is broken output, and the fix is `console.log` and a final newline rather than this module.
The second is a program that needs several independent regions updating at once. The API has one active region per instance, and while `createLogUpdate` lets you make more instances for more streams, each one owns its own terminal area, and coordinating two of them in one column is not something the library does.
The third is a long piece of text that should scroll. The module is for status, not for output, and a multi-line block that changes size forces a full redraw, which is exactly the flicker the partial redraw is meant to avoid.
The fourth is a team that wants a progress bar with a fixed layout, a label, a percentage, a bar and an ETA. This module is a primitive underneath that, and the README's own examples section points at the packages that do the whole job: listr for an interactive task list, ora for spinners, and speed-test, which uses it to render a spinner.
So log-update is the layer that ora and listr are built on, and using it directly is the right call when you want a status line and nothing else.
The fifth is an environment where the terminal does not report a size, and where the default of 80 by 24 is wrong. The `defaultWidth` and `defaultHeight` options exist for that, and forgetting them is the most common way this module produces surprising wrapping.
Editorial conclusion
log-update suits a Node CLI that needs a spinner, a progress bar or a status line, and that wants the output to stay readable when the command is watched rather than scrollable. It does not suit a tool whose output is piped into a file, where overwriting means the file contains escape sequences and a single final frame, and it does not suit a program that needs several lines updating independently, because the API is one active region. Before adopting it, check the `defaultWidth` and `defaultHeight` options, because in a non-interactive context the terminal reports no size and the defaults of 80 by 24 will wrap your output unexpectedly.
Frequently asked questions
What does log-update do?
It logs by overwriting the previous output in the terminal, which is useful for rendering progress bars and animations. It performs partial redraws when possible to reduce flicker.
How do I install and use log-update?
Install it with `npm install log-update`, then import it and call `logUpdate(text)` with the full text to display. The output is replaced in place each time rather than appended to the scrollback.
What is the difference between logUpdate.persist and logUpdate?
The main `logUpdate()` method updates in place, while `persist()` writes to the terminal in a way that preserves the output in the scrollback history, similar to `console.log()`. `logUpdate.done()` persists the current output and is useful for starting a new log session below it.
What options does createLogUpdate accept?
Three. `showCursor`, a boolean defaulting to false, for when the CLI accepts user input. `defaultWidth`, a number defaulting to 80, used when the stream does not provide a `columns` property. And `defaultHeight`, a number defaulting to 24, used when the stream does not provide a `rows` property.
Which Node version does log-update require?
Node 22 or newer. The package is a pure ESM module with `type` set to `module`, exports `index.js` and `index.d.ts` only, and its test script runs xo, the node test runner and tsd for the type tests.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sindresorhus-log-update)