DE

Code Style in Neovim: Tabs vs. Spaces

By default Neovim indents with no less than 8 spaces. It is not always easy to determine the individual effects of the four options involved. This article will describe how they influence each other.

Before it even comes to the question of how many spaces to use, it has to be decided whether to use spaces at all. Instead, one tab character could be inserted. To avoid inconsistencies, project standards generally prescribe how to deal with the issue.

Before we dive into setting up Neovim according to your preference, I’ll briefly outline the pros and cons of both sides of the “timeless” debate.

Why Use Tabs?

At first glance it seems like a rather pointless argument without practical consequences. Differences in file size are hardly worth mentioning today. Even in terms of the keys you have to press, the two cases are very similar.

Like other editors, Neovim simulates the behavior of tabs in relevant contexts. To remove one level of indentation, a single press of the Backspace key is sufficient, regardless of whether there is one tab character or multiple spaces at the beginning of the line.

When indenting with spaces, the question arises as to how many should be used. On this level, the trade-off is between readability and the waste of space. Coding standards provide guidelines that should well suit all members of the project.

The main difference when using tab characters is that they are quite flexible: the way that indentation levels are to be displayed is no longer stored in the text file itself. Instead, the visual appearance is determined by settings of the editor. Team members can decide for themselves what they find appealing, or simply stick with what they’re used to.

It also makes it easier to use available screen space in a way that suits each person’s workflow and setup. This difference can matter – for example, when working with multiple windows side by side on the same screen, or when using large terminal fonts.

This is also the main disadvantage of tabs: the ASCII character is interpreted differently in different contexts. Even when options are available, many users may not have configured their editor to handle tabs properly.

As a result, indentation levels may appear too deep, and it can be jarring to realize that you can’t reach the beginning of lines. For some users, it may not be immediately obvious that the tab character is causing the seemingly irregular behavior.

What Spaces are at Issue?

In the introduction, I’ve said that Neovim uses spaces for indentation. The help documentation too refers to the “number of spaces” in its explanation for three of the relevant options: tabstop, softtabstop, and shiftwidth.

In fact, indentation and the Tab key are handled by using a tab character. Whether the visual spaces are actually filled by space characters depends on expandtab, which is turned off by default.

When the documentation talks of spaces in these instances, it actually means purely visual whitespace. In other places it refers to these units for measuring window size as columns. Even though columns may be as wide as space characters, they can be filled by any character.

Terminals typically use monospace fonts, which means that all letters, special characters, and spaces have the same width. This ensures that all characters fit neatly in the visual slots marked out as columns.

What Functionality is at Issue?

The Tabs vs. Spaces debate concerns two related but distinct areas:

  • Pressing the Tab key
  • Indenting a line, either automatically or manually (e.g., with >)

Strictly speaking, the second point is a special case of the first. However, the distinction is important because different options handle them differently.

The issue isn’t primarily about aligning symbols. For instance, certain style guides require operators in multi-line expressions to be aligned on a vertical line. For this purpose, only spaces can be used.

The key difference is that the placement of an operator on the first line isn’t generally predictable and cannot align with the tabular grid that spans the entire text buffer. To better understand the issue, some historical context might help.

The tabulator key was already in use when typewriters were still in fashion. Pressing it would move the carriage forward to the next tab stop. This made it possible to left-align text on different lines, as if in tabular columns.

The movement depends on the current position of the carriage and the distance to the next tab stop. While the distances between tab stops could be adjusted, in what follows we’ll assume that the resulting tabular columns are of uniform width.

Implementation

Modern text editors reproduce the behavior of the mechanical tabulator when the Tab key is pressed. Rather than moving a physical carriage, the cursor now jumps forward by a number of columns, understood as the measuring unit defined above.

When using spaces, the implementation is straightforward. Suppose tab stops are set 8 columns apart: if <Tab> is pressed on an empty line in insert mode, the cursor jumps to column 9 – its full jump distance.

If two characters have already been entered, pressing <Tab> only needs to insert 6 spaces to reach the next tab stop at position 8. These inserted spaces become part of the text buffer and are saved in the file.

In the case of a tab character, the cursor’s position doesn’t matter for the file itself. Instead, the correct visual spacing must be calculated by the editor when the file is opened. In the same scenario, the editor must render the tab character as if 6 spaces had been inserted.

Setup

In Neovim, four options are responsible for indentation behavior and related concerns. As expected, using spaces won’t pose any difficulties. The configuration for the use of tab characters, on the other hand, can be a bit more involved.

Using Spaces

Most coding standards recommend the use of spaces. In Neovim, you can follow this convention by enabling expandtab. Once it’s set to true, two additional options control the functionalities mentioned earlier:

  • tabstop defines the maximum number of spaces inserted when pressing <Tab>.
  • shiftwidth specifies the number of spaces used for indentation.

For example, a common setup for languages of the C-family might look like this:

vim.opt.expandtab = true
vim.opt.shiftwidth = 4
vim.opt.tabstop = 4

We’ll leave softtabstop at its default, which effectively disables it. Its purpose will become clearer in the next subsection.

Using Tabs

As mentioned above, expandtab is disabled by default. This means pressing <Tab> inserts a literal tab character. How this character visually appears in Neovim is determined by the tabstop option.

Indentation is treated separately. By default, shiftwidth is set to 8, which means indentation uses spaces, regardless of whether it’s applied automatically or via the > command. Importantly, this also happens regardless of the expandtab setting.

So in the default configuration, Neovim does not take a strong position on the tabs vs. spaces issue. To ensure that tab characters are used consistently – both when pressing <Tab> and when indenting – you’ll need to set shiftwidth to 0.

As just mentioned, tabstop determines how the tab character is displayed. For indentation, this is simple: the tab character jumps up to the next tab stop, 8 columns ahead. In other contexts, things get a bit trickier.

The editor needs to calculate where the next tab stop is relative to the position of the tab character, and adjust spacing accordingly. While the location of the tab is fixed in the text buffer, the placement of the tab stops depends on editor settings.

softtabstop can complicate things further. If it’s set to a positive value, it will be this option that determines the maximum jump distance for <Tab>. tabstop will still determine the visual appearance of tabs, though the spacing will be the complementary effect of both options.

Practically speaking, pressing <Tab> may insert in a combination of tab and space characters. The helpdoc recommends leaving tabstop at its default for best compatibility. If shiftwidth is greater than 8, additional spaces may be inserted.

For example, softtabstop is set to 10 and tabstop is left at the default:

  • Pressing <Tab> at the start of a line results in 10 columns of spacing.
  • One tab character fills 8 of those columns.
  • Two additional spaces fill the remainder.

Best Practices

There are historical reasons for mixing tabs and spaces, though they no longer carry much weight. To avoid unnecessary complexity, it’s best to assign the same value to tabstop and softtabstop.

-- Function to configure tab/space behavior
-- @param use_tabs bool: Whether to use tabs (true) or spaces (false)
-- @param width number: The width of indentation/distance between tab stops
function configure_indentation(use_tabs, width)
  if use_tabs then
    vim.opt.expandtab = false
    vim.opt.tabstop = width -- For visual appearance of tab characters
    vim.opt.softtabstop = width -- For max jump distance of manual tabs
    vim.opt.shiftwidth = 0 -- Use tabs indentation
  else
    vim.opt.expandtab = true
    vim.opt.tabstop = width -- For max jump distance of manual tabs
    vim.opt.shiftwidth = width -- For indentation
  end
end

Many projects use a shared EditorConfig file. If present, your editor will automatically apply its settings, which may override your personal configuration. This is particularly useful for languages with conflicting conventions.

PHP is a good example. WordPress recommends using tabs, while Laravel prefers spaces. Rather than hardcoding preferences in your editor, it’s best to include an .editorconfig file tailored to the project.

Conversion

Mixed use of spaces and tabs within the same file often arises not just from inconsistent editor settings, but more commonly from editing a file that follows a different standard.

If spaces were used where tabs are now preferred, you can convert them using the :retab command. As with softtabstop, this can result in a mixture of tabs and spaces, depending on how the settings are configured.

Despite its name, the command works in both directions. If expandtab is enabled, it will convert tab characters to spaces. tabstop! forces conversion to tabs.

However, caution is advised: :retab will also modify whitespace inside string literals. To avoid unintended changes, it’s best to visually mark the regions you want to convert before running the command.

In practice, it’s often easier to rely on an autoformatter. Most formatters include a Tabs vs. Spaces option, which allows you to standardize an entire document automatically according to the chosen style.

Article from April 18, 2025.