// Sheets · Apps Script

Convert a column number to a letter in Google Sheets.

Turn a column index like 28 into its A1 letter (AB) in Apps Script, handling the wraparound past column 26. Includes the catch that most people don't actually need the letter at all.

I have a column number and I need its A1 letter so I can build a range string like 'AB1:AB100'.

The script

copy · paste · trigger
columnToLetter.gs
Apps Script
// Convert a 1-based column index (1, 2, ... 27, 28) into its A1
// letter ('A', 'B', ... 'AA', 'AB'). Handles the base-26 wraparound
// past column Z. Returns '' for non-positive input.
function columnToLetter(column) {
  if (column < 1) return '';
  var letter = '';
  var n = column;
  while (n > 0) {
    var remainder = (n - 1) % 26;
    letter = String.fromCharCode(65 + remainder) + letter;
    n = Math.floor((n - 1) / 26);
  }
  return letter;
}

// columnToLetter(1)  -> 'A'
// columnToLetter(26) -> 'Z'
// columnToLetter(27) -> 'AA'
// columnToLetter(28) -> 'AB'

Need a variant? Gnaw writes a custom version from one sentence — fields, triggers, edge cases handled.

Walkthrough

The base-26 part that trips everyone

Spreadsheet columns look like base-26 with the letters A-Z standing in for digits, but they aren't quite. Real base-26 has a zero; spreadsheet columns don't. There's no column whose label is empty, and after Z (26) you get AA (27), not BA. That off-by-one is why a naive String.fromCharCode(64 + column) falls apart the instant you cross column 26 and starts emitting punctuation like '[' for column 27.

The fix is the (n - 1) % 26 and Math.floor((n - 1) / 26) pair. Subtracting 1 before each step shifts the math into a system where A behaves like 0 inside the digit, but the labels still read 1-based. So 27 becomes remainder 0 (an 'A') plus a carry of 1 (another 'A'), giving 'AA'. The loop prepends each letter because we generate the least-significant digit first, same as you would converting any number to a positional base by hand.

I keep this exact function in a utilities file and have copied it into more projects than I can count. It is fifteen lines, it has no dependencies, and it is correct up to column 16384 (XFD), which is as far as Sheets goes anyway.

You probably don't need it

Here is the thing nobody tells you when you go searching for this. getRange(row, column) takes numbers. Both arguments. sheet.getRange(1, 28) is the same cell as sheet.getRange('AB1'), and the numeric form never makes you convert anything. If your end goal is to read or write a cell, you can throw the letter away entirely and pass the index straight through.

So when do you actually need the letter? When you're assembling an A1-notation string by hand: a Range.getA1Notation() substitute, a named-range definition, a formula you're writing into a cell as text ('=SUM(' + col + '2:' + col + ')'), or a chart reference that wants letters. Those are real cases. They are also narrower than the search volume for this question suggests.

Before you paste columnToLetter into your project, ask whether the next line is a getRange call. If it is, delete the conversion and pass the number. The first time I hit this I had written a careful column-to-letter helper, used it to build an A1 string, and fed that string right back into getRange, a full round trip from number to letter to number for nothing.

Want a custom version?

Describe your sheet and the rule you want. Gnaw writes the Apps Script — fields, triggers, edge cases — in one shot.

FAQ

4 questions
Is the column index 0-based or 1-based?
1-based. Column A is 1, not 0. This matches getRange(row, column), which is also 1-based. If you have a 0-based index from an array loop, add 1 before calling columnToLetter.
How do I go the other way, letter to number?
Walk the string left to right: var n = 0; for (var i = 0; i < letter.length; i++) { n = n * 26 + (letter.charCodeAt(i) - 64); }. The same -1/+1 logic is baked into the charCodeAt offset of 64.
What's the highest column Sheets supports?
16384, which is XFD. columnToLetter handles it. A single sheet caps at 18,278,000 total cells, so you'll hit the cell limit long before the column letter runs out of room.
Can I get the letter from an existing Range instead?
Yes, and it's simpler: range.getA1Notation() returns something like 'AB1:AB100', and you can split off the letters. Only build the letter from a raw index when you don't already have a Range object in hand.