TL;DR —
rng.Offset(rowDelta, colDelta)returns a new range shifted by that many rows and columns fromrng. It's a step, not a place: it keeps the same size, doesn't changerng, and doesn't select anything. The two numbers are relative —Offset(1, 0)is one row down,Offset(0, 0)is the cell itself, and negatives go up/left. Offset moves;Resizechanges size. Put them together and you get the single most useful range idiom in VBA:data.Offset(1).Resize(data.Rows.Count - 1)— "everything except the header."
Offset is how a macro says "the cell next to the one I just found." You located
a match, a total, a header — now you want to read or write the cell one column
over or one row down. That relative move is Offset's whole job, and it becomes
second nature once you stop reading the arguments as coordinates and start reading
them as steps.
What you'll learn
- The mental model: Offset is a relative step, not an absolute address
- The off-by-one trap — why
Offset(1, 1)lands on B2, not A1 - Offset keeps the size;
Resizechanges it — and why you usually need both - The header-skip idiom
Offset(1).Resize(n - 1), explained - How
Range.Offset(VBA) differs from theOFFSET()worksheet function
The mental model: Offset is a step, not a place
Stand on a cell. Offset(rowsDown, colsRight) takes that many steps and hands you
the cell (or block) you land on — a brand-new range. The original is
untouched. Because the move is relative, the same Offset(1, 0) means "one row
down" whether you start at A1 or Z500.
Sub OffsetSteps()
Debug.Print Range("B2").Offset(1, 0).Address ' -> $B$3 (down one row)
Debug.Print Range("B2").Offset(0, 1).Address ' -> $C$2 (right one column)
Debug.Print Range("B2").Offset(-1, 0).Address ' -> $B$1 (up one row)
Debug.Print Range("B2").Offset(0, 0).Address ' -> $B$2 (no move)
End Sub
Notice Offset(0, 0) is the cell itself. That's the tell that these are deltas,
not positions. If you want an absolute cell, don't compute an offset from A1 —
use Cells(row, col). Offset is for "relative to something I already have."
The rule that catches beginners: the off-by-one trap
The number-one Offset bug is reading the arguments as a coordinate. Offset(1, 1)
does not mean cell A1 or "row 1, column 1." It means one row down and one
column right from the anchor. From A1 that lands on B2.
Range("A1").Offset(1, 1).Address ' -> $B$2, NOT $A$1
Range("A1").Offset(0, 0).Address ' -> $A$1
Say the two arguments out loud as "down, right" every time and the bug
disappears. It also explains the classic loop pattern: to write a result next to
each row you found, you offset by the loop counter, and the first row uses
Offset(0, …), not Offset(1, …).
The distinction that trips everyone: Offset moves, Resize changes size
Offset never changes how big a range is — it slides the whole shape. Move a
ten-cell column and you get a ten-cell column somewhere else:
Range("A1:A10").Offset(0, 1).Address ' -> $B$1:$B$10 (10 cells, moved right)
To change the size of a range you need a different method, Resize(rows, cols),
which redraws the block from its top-left cell:
Range("A1").Resize(5, 2).Address ' -> $A$1:$B$5 (5 rows, 2 columns)
They're the two halves of range arithmetic: Offset picks where, Resize picks how big. You reach for both together constantly — most often to drop a header row.
The idiom worth memorizing: skip the header with Offset + Resize
You have a table including its header, and you want just the data body. Offset down one row to move off the header — but Offset alone keeps the same height, so the range now runs one row past the data. Resize shrinks it back by that one row:
Sub DataBodyOnly()
Dim tbl As Range, body As Range
Set tbl = Range("A1").CurrentRegion ' header + data
Set body = tbl.Offset(1).Resize(tbl.Rows.Count - 1)
body.Select ' data rows only, no header
End Sub
Offset(1) (the column delta defaults to 0) slides the whole block down one row;
Resize(tbl.Rows.Count - 1) trims the now-overhanging bottom row. The result is
the data body with the header removed — the exact range you want to sort, copy,
or clear. This one line replaces a lot of manual A2:A string-building. To find
tbl without hardcoding, see finding the last row and
CurrentRegion.
The edge-of-sheet trap
Because Offset is relative, stepping past the edge of the worksheet is an error, not a silent clamp:
Range("A1").Offset(-1, 0) ' run-time error 1004 — can't go above row 1
If a macro walks up or left near the top-left corner (common when scanning for
headers), guard the move or check rng.Row > 1 before offsetting upward.
Offset the method vs. OFFSET the worksheet function
They share a name and the same idea, but they are not the same tool. The
worksheet function OFFSET() lives in a cell, is
volatile (it recalculates on every change, which can slow a workbook), and
returns a value or reference for a formula. Range.Offset is a VBA method: it
returns a Range object, is not volatile, and runs only when your code runs.
If you're writing a macro, use the method; reach for the worksheet function only
when you need a live, self-updating reference inside a formula.
How ExcelMaster helps
Offset-heavy code is usually building one thing: a result placed relative to something you found — a flag next to each match, a running total one column over, a value copied from the row below a label. ExcelMaster takes that intent in plain English — "next to every overdue invoice, write the days late" — and generates the relative-reference logic, so you don't hand-count deltas or debug an off-by-one.
You'll still write Offset inside a tight macro loop. But for the everyday
"put a result beside each row" task, describing the placement is faster and
harder to get wrong than reasoning about (1, 0) versus (0, 1).
Frequently asked questions
What does Offset do in VBA?
rng.Offset(rowDelta, colDelta) returns a new range shifted by that many rows and
columns from rng, keeping the same size. It's a relative move: Offset(1, 0) is
one row down, Offset(0, 1) is one column right, and negative numbers move up or
left. It doesn't change or select the original range.
Why does Offset(1, 1) not return cell A1?
Because the arguments are deltas, not coordinates. Offset(1, 1) means one row
down and one column right from the anchor cell, so Range("A1").Offset(1, 1) is
B2. Use Cells(row, col) when you want an absolute cell instead of a relative
step.
What's the difference between Offset and Resize?
Offset moves a range without changing its size; Resize(rows, cols) changes the
size from the top-left cell without moving it. You often combine them —
rng.Offset(1).Resize(rng.Rows.Count - 1) skips a header row by moving down one
and shrinking by one.
How do I skip the header row with Offset?
Use table.Offset(1).Resize(table.Rows.Count - 1). Offset(1) moves the block
down past the header; Resize trims the row that now overhangs the bottom,
leaving just the data body.
Is VBA Range.Offset the same as the OFFSET worksheet function?
No. Range.Offset is a VBA method that returns a Range object and is not
volatile. The OFFSET() worksheet function lives in a cell, is volatile
(recalculates constantly), and returns a value or reference for formulas. Use the
method in macros; use the function only for live formula references.
Tested in
Tested in: Excel 365 (Windows 11), VBA 7.1 — last verified 2026-07-30.
Related guides: VBA Last Row · VBA UsedRange vs CurrentRegion · VBA Range · OFFSET Worksheet Function
