Responsive Tabs

Posted

As you know if you have worked on any of my projects I am a fan of tabs for indentation. They give you flexibility that is not possible with spaces and have no downside (other than having to know how to use them).

I'm not here to start a flame war, but just wanted to share how the ability to choose the size of tabs gave me a benefit on my site.

My site is incredibly responsive. Not because I have different designs for different device sizes but because I have no fixed size elements and only a single column. However I do make some small adjustments for different screen sizes. For example on large screens my code blocks (see the example below) are slightly narrower than most of the text, however once the browser window gets smaller they become full-width. This provides just a bit more space, and reduces the likelihood of requiring scrolling. Try it out yourself by adjusting the size of your browser window.

;;; Calculate the number of ways change can be made.
;;;
;;; amt: The amount in cents for change to be made for.
;;; coins: The denominations (in cents) can can be used.
(defun change (amt coins)
	(cond
		((eql amt 0)  1) ; One way to make change for 0ยข.
		((<   amt 0)  0) ; Not necessary, but an optimization.
		((null coins) 0) ; No available coins to use!
		(t (+
			(change (- amt (car coins)) coins) ; Use the coin.
			(change amt (cdr coins))))))       ; Don't use the coin.

However for small screens it is still hard to get code to fit without scrolling. So I went further and when the screen gets to a certain size I reduce the tab width. You can't see on the previous example because I have lisp tab stops set to 2, but in the python example below you can see the tab stops switch from 4 to 2 as you make the page smaller.

import hashlib

class SpellCheck:
	def __init__(self):
		self.__dic = bytearray(2**8)
	
	def add(self, w):
		for b in self.__bits(w):
			self.__dic[b//8] |= 1 << b%8
	
	def __contains__(self, w):
		for b in self.__bits(w):
			if not self.__dic[b//8] & 1 << b%8:
				return False
		return True
	
	def __bits(self, w):
		return hashlib.sha1(w.encode()).digest()[:4]

This is something you simply can't do with spaces. A naive approach of text.gsub ' '*4, ' '*2 doesn't work because it messes up alignment.

#include <stdio.h>

int main(void)
{
	int v;
	scanf("%d", &v);
	printf("%d (0x%X) is considered %s.\n",
	       v, v,
	       v? "true":"false");
	return 0;
}

If that was indented with spaces it would become the following after a simple substitution. The alignment is now incorrect. Spaces simply do not contain the information required. Tabs clearly say "One indent please!" while spaces say "Don't worry about it, I'll make it look right."

#include <stdio.h>

int main(void)
{
  int v;
  scanf("%d", &v);
  printf("%d (0x%X) is considered %s.\n",
      v, v,
      v? "true":"false");
  return 0;
}

The implementation is simple, simply indent your examples with tabs then apply the following css.

// Default tab-size.
tab-size: 4;
@media (max-width: 32em) { tab-size: 2 }

Then you tabs will be 4 characters when the browser is more then 32em wide, otherwise they will be 2 characters for a nice space saving. The values are of course tweakable, and are just rough guesses about size. It would be interesting to actually detect when and how much indent is needed and adjust it intelligently, but for now a simple media query is sufficient.

I also customize indents per-language as some like to get very nested very quickly. The following css does that for me.

// Per-language size.
.highlight.haml,
.highlight.html,
.highlight.common_lisp,
.highlight.xml {
	tab-size: 2;
}

And there you go. Using tabs to represent indentation allows you (and people you work with) to use whatever indent size they prefer, and when you are on the web you can change it to suit the screen size used to view the code.