I’m right about tabs vs. spaces, and you can’t tell me otherwise.

Tabs vs. Spaces.

Amongst developers, it’s a debate that’s more important and divisive than Brexit, the 2014 Scottish Independence Referendum, and whether it’s pronounced “Gif” or “Jif”. Combined.

Ok, maybe it’s overstating things a bit, but every developer has an opinion on using tabs or spaces for indentation, and they’re sure their right.

Well, much like those other debates, I’m the one who’s right.

(And since sarcasm doesn’t translate over the internet very well, I am being facetious. Mostly.)

Before I alienate half of you…

Both within individual files, and within your codebase as a whole, consistency is important.

Whether you prefer tabs or spaces, it’s far more important to be consistent — within files, projects, and your organisation. Especially when you have multiple developers working on the same codebase.

You should have defined coding standards that everyone working on the code understands and follows.

To make this easier, you can set up code linting tools like ESLint to reject or automatically fix instances of indentation (and other stylistic preferences) that don’t match your organisation’s coding standards — something I would highly recommend doing.

And you can make use of the incredibly useful .editorconfig — a file that you can include in the root directory of your project that allows people’s IDEs to automatically configure themselves, making sticking to these standards seamless.

[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4

.editorconfig is natively supported by many of the most popular IDEs, and others have plugins or extensions to add support.

Why use tabs?

Tab advocates (tabvocates?) generally give three main advantages to using tabs over spaces:

  1. It’s easier to type a tab on the keyboard than X number of spaces
  2. Tabs are designed for indentation
  3. A tab is a single character, therefore the size of files on disk can be smaller
  4. Tabs let individuals change indent size, without having to actually modify the file

That actually sounds pretty reasonable, right?

Before I go into why I prefer using spaces over tabs, let’s talk about those advantages for a bit.

Typing spaces

Yup. Hitting the tab key is a lot easier than tying a bunch of spaces.

However, when most modern IDEs are configured to use spaces, pressing the tab key inserts the required number of spaces. In fact, “soft tabs” (spaces) is actually the default in most popular IDEs.

This is also true in a lot of web-based code editors: when editing code directly on GitHub or GitLab can cleverly and correctly use the tab key to insert the right number of spaces.

So yes, if you’re editing code in NotePad or another basic text editor, if you use spaces, you’ll be left mashing the spacebar. But for the most part, you don’t even need to think about it.

Designed for indentation

I feel like “what a character was intended for” is a bit of a non-argument: what a character was intended doesn’t really matter.

For example, the “backtick” character (`) that’s now frequently used in place of the quote or double-quote characters around strings in coding wasn’t “intended” for that — it’s a grave accent, that was meant to be used in addition to letters to create accented versions — àèìòù — in words like “crème”.

But tabs weren’t designed for indentation anyway. The clue for what their purpose was is in the name: they were designed for tabulation. That is, to “arrange data in a tabular, or table, form”. That’s not the same as indentation.

So it’s not really an accurate statement to begin with, but I don’t really think it would necessarily be a reason to use tabs for indentation even if it were true.

File Size

Again, yes, you’ll be saving a bit of file size by using a single tab character compared to multiple space characters.

But the gains here are pretty small.

As an example, I took one of the larger JavaScript files in one of my projects. We tend to be pretty good at modularising our code, but this file is on the large side, approaching 1,000 lines. Using 4 spaces for indentation, the file weighs in at 37,749 bytes. Converting that file to use tabs, reduces the size to 33,174 bytes: a reduction of only about 12%.

If code is well documented through comments, they can account for more than 25% of the size of a file. If reducing the size of the files on disk is important, removing these comments would be a far bigger win.

But you shouldn’t do that either.

While there are cases where the size on disk is important, those are very limited. And for the purposes of websites and web applications, client-side code should probably minified anyway to reduce bandwidth, which will usually remove all indentation regardless.

Changing the indent size

This tends to be the main reason to use tabs over spaces: people can adjust their IDE to make the indentation the way they want, without having to change the actual file.

And honestly, that’s sounds pretty appealing: being able to make the code look the way you want without affecting how other people see it.

However it also leads us pretty neatly into the advantages of spaces over tabs…

Spaces are better

From the beginning, code has been monospaced. You open up any code editor, and you get a blank window with a nice, monospaced font. That is, a font where every character you type takes up the same width.

There’s several reasons why we use monospaced fonts for code, but the main thing is that monospaced fonts make it easier to read and edit our code because they enforce a structure.

Coding standards for many languages, and long established best practice is based on the fact that code is monospace: that no matter what you type, all characters are the same width.

Tabs break the monospaced nature of code. A tab is a variable width character, within our monospaced world.

And that variability causes breaks.

Aligning across multiple lines

Consider the following CSS:

.page {
    grid-template-areas: "header  header "
                         "content sidebar"
                         "footer  footer ";
}

When using spaces, this kind of inter-line alignment is easy and consistent. No matter who’s viewing the code, each part will be aligned, and the relationship between each line is clear.

Now consider that same example using tabs.

If we assume a 4 character tab-width, it’ll leave an uneven alignment that will have to be corrected with a space:

.page {
⇥   grid-template-areas: "header  header "
⇥   ⇥   ⇥   ⇥   ⇥   ⇥   ␣"content sidebar"
⇥   ⇥   ⇥   ⇥   ⇥   ⇥   ␣"footer  footer ";
}

Ok, but then if someone else views the code with an 8 character tab-width, they’d see something like this:

.page {
⇥       grid-template-areas: "header  header "
⇥       ⇥       ⇥       ⇥       ⇥       ⇥       ␣"content sidebar"
⇥       ⇥       ⇥       ⇥       ⇥       ⇥       ␣"footer  footer ";
}

It breaks the alignment because we’re writing monospace code, and we have the reasonable assumption that the code we write will be displayed consistently because all characters are the same width. But we’re then also inserting a variable-width character into our code.

“Yes Lewis, but that’s why you use tabs for indentation, and spaces for alignment!” I hear you say.

But that relies on people remembering to do that. And in cases like the above example, which isn’t an uncommon little bit of CSS, you’ll be typing in far more spaces than tabs.

.page {
⇥       grid-template-areas: "header  header "
⇥       ␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣"content sidebar"
⇥       ␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣"footer  footer ";
}

So why not just go all in on spaces?

Using spaces instead of tabs means that you don’t need to separate indentation from alignment. You can write your code in a consistent way, and it’ll always look and read consistent, no matter who’s viewing it.

Line lengths

It’s common in coding standards to specify a maximum line length, typically around 80–100 columns.

There’s a lot of good reasons for sticking to a maximum line length for code, but the main one is readability: it helps make sure that code is concise and readable, without automatic wrapping (which could break code in odd places and make it harder to quickly read), and without horizontal scrolling.

But that a maximum line length is again based on the assumption that code is monospaced.

Let’s take a look at an example of a pretty basic and useless TypeScript function. Just to make the example a bit easer to illustrate, we’ll use a shorter than normal 60 column line length. The comment at the top shows the columns, with pipes every 10 columns.

//     10|       20|       30|       40|       50|       60|       70|

const foo = function foo(arg:SomeInterface):string {
    const array = Object.entries(arg).map([key, value] => {
        if(testValue(value) === 'odd' && value < 100) {
            return `${key} matches!`;
        }

        return `${key} doesn’t match!`;
    });

    return array.join(',');
}

But if we have a different tab-width — 8 characters, for example — we’re suddenly going over our maximum line length:

//     10|       20|       30|       40|       50|       60|       70|

const foo = function foo(arg:SomeInterface):string {
⇥       const array = Object.entries(arg).map([key, value] => {
⇥       ⇥       if(testValue(value) === 'odd' && value < 100) {
⇥       ⇥       ⇥       return `${key} matches!`;
⇥       ⇥       }

⇥       ⇥       return `${key} doesn’t match!`;
⇥       });

⇥       return array.join(',');
}

A person viewing this code might see that line is over the maximum line length, and try and reformat the line. But then a third developer who has a 2 character tab-width might open the file and for then, that newly reformatted line looks needlessly narrow. So they might reformat it again to take advantage of the space.

//     10|       20|       30|       40|       50|       60|       70|

const foo = function foo(arg:SomeInterface):string {
  const array = Object.entries(arg)
    .map([key, value] => {
    if(
      testValue(value) === 'odd'
      && value < 100
    ) {
      return `${key} matches!`;
    }

    return `${key} doesn’t match!`;
  });

  return array.join(',');
}

By introducing a variable-width character into a format that’s assumes it’ll be monospaced we’re making it far more difficult to keep our code consistent and easy to read.

Error traces don’t work properly with tabs

Stack traces in code can lead us directly to where an error is occurring to help us quickly find the source to fix it. Traces will include the name of the file, and the line and column number where the error occurred.

Let’s take a look at this code example with 8 character tab spaces again:

const foo = function foo(arg:SomeInterface):string {
⇥       const array = Object.entries(arg).map([key, value] => {
⇥       ⇥       if(testValue(value) === 'odd' && value < 100) {
⇥       ⇥       ⇥       return `${key} matches!`;
⇥       ⇥       }

⇥       ⇥       return `${key} doesn’t match!`;
⇥       });

⇥       return array.join(',');
}

Now imagine we got this error:

ERROR in example.js:3:23
    Type 'string' is not assignable to type 'number'.

That error is telling us that the error is occurring at on the third line, at column 23. In our code above, column 23 is midway through the testValue function name.

Once again, the problem is that we’re using a variable width character in an environment assumed to be monospace. The parser doesn’t know what width we’ve set our tabs to, so it assumes a reasonable default of a 4 character tab-width, and is reporting the error column based on that assumption.

But that assumption is wrong in this case, and any other case where we’re using anything except a 4 character tab-width.

And while the source of the issue would be quite easy to work out in this simple code example, it’s easy to see how an error in a more complicated bit of code might be far harder to track down; where you can be nested far deeper, so the difference in the reported error column and what you’re seeing in your editor would be greater.

And if the compiler made a different assumption about the tab-width, it could be worse. For example, in the above example, if the parser assumed a 2 character tab-width, and your IDE is set to 8 characters, it would have said that error was in column 16, which would be the start of the if statement.

The only way to ensure that the parser doesn’t have to make any assumptions is to keep the monospaced coding environment monospaced, and not use a variable-width character. Or, to put it another way, to avoid tabs in code.

You’re probably already using spaces

Most IDEs default to using spaces for indentation. Editors like Visual Studio Code, Atom, and my personal favourite, Nova (you just can’t beat an app that’s actually native) all default to using “Soft Tabs”. That’s spaces.

And linting tools like ESLint use 4 spaces as the default for their indent rule.

If you haven’t changed the default settings on your IDE or linter and are writing code thinking you’re loving the tab lifestyle, you’re probably actually using spaces.

Consistency is king

When working with multiple developers in large codebases, consistency is important. It makes code easier to read and understand.

Using spaces for indentation makes your code more consistent: no matter who looks at it — people or parser — it will look the same. No one has to make assumptions about how the code should be displayed.

But if you do want to continue to be wrong and use tabs, then I hope you’ll at least be consistent about it: make sure you have defined standards for your projects; and use tools like ESLint and the .editorconfig file to enforce your standards, and fix or reject non-compliant code.

Because the only thing worse than using tabs for indentation, is mixing tabs and spaces for indentation.

About Lewis Dorigo

Here’s some more articles you might like:

  1. WCAG 3.0: First Draft, First Impressions

    On Thursday last week, the W3C published the first working draft of the W3C Accessibility Guidelines (WCAG) 3.0.

    Read More
  2. “A person face-palming”: a quick guide to writing good alt text

    Writing good alt text is one of the quickest ways to improve the accessibility of your website. I share some tips for writing alt text that makes sense.

    Read More
  3. Your website UX SUX, or: Why you should consider accessibility from the start.

    The web is one of the most transformative inventions in human history. But too often, people with different accessibility needs are left behind.

    Read More