ICAEW.com works better with JavaScript enabled.
Exclusive

Automated Grouping

Author: Liam Bastick

Published: 23 Jul 2026

Exclusive content
Access to our exclusive resources is for specific groups of students, subscribers and members.
Liam Bastick discusses Grouping as a preferred option to produce summaries of spreadsheet reports, without the reconciliation issues that may arise with using separate worksheets for these reports.

Many of us that prepare spreadsheet reports realise that we have to produce summaries at various levels of granularity.  Some make the mistake of using different worksheets for these reports, but this can cause unnecessary reconciliation issues if things change. Grouping may be a preferred option – which we will come to shortly.

Sometimes when constructing spreadsheets, modellers may want to hide certain data / calculations.  This can be undertaken by formatting the cells or if the rows or columns should be hidden by hiding the necessary rows / columns (CTRL + 9 and CTRL + 0 respectively).

screenshot from an excel spreadsheet

This illustration (above) shows that it can be difficult to spot hidden rows and columns; you have to look at the row and column headers and this presupposes that these are visible.

An alternative to hiding is grouping:

screenshot from an excel spreadsheet

Grouping is undertaken by selecting the row or column (similar to hiding) but then go to the Data tab on the Ribbon and in the Outline section, select the ‘Group’ button and press Group.  The keyboard shortcut is faster and more intuitive: ALT + SHIFT + Right Arrow (whereas ALT + SHIFT + Left Arrow removes the grouping).

Grouping introduces ‘levels’ to a spreadsheet: in the graphic above, notice how the plus buttons are aligned with the buttons marked ‘Number 1’.  Level 1 is always all grouped sections collapsed.  If all of the plus buttons are pressed, our example would be displayed as follows:

screenshot from an excel spreadsheet

The plus (+) buttons have become minus (-) buttons, still aligned with the ‘Number 1’ buttons.  This is because if you pressed the numbered buttons all of the rows / columns grouped to the same level would be displayed consistently, rather than pressing a multitude of plus and minus buttons.  The hidden rows and columns are easier to identify, and simpler to collapse / expand as required.

Grouping can be taken to eight [8] levels, viz.

screenshot from an excel spreadsheet

Rows 5 to 11 in this illustration have been grouped at differing levels by holding ALT + SHIFT depressed whilst pressing the Right Arrow key (you can see the dots on the left-hand side of the image before the row headers).  Here, because the rows are next to each other, Excel cannot display separate plus buttons so users would have to use the numbered buttons instead.

The positioning of the plus buttons can be confusing as they actually align with the row / column after the grouping in the default setting:

screenshot from an excel spreadsheet

The settings are changed by returning to the Outline section of the Data tab and clicking on the small icon in the bottom right-hand corner (keyboard shortcut, ALT + A + L):

screenshot from an excel spreadsheet

Then, clear both check boxes in the dialog box that appears and then press OK:

screenshot from an excel spreadsheet

This moves the buttons to the row / column prior to the grouping which, personally, makes more sense to me.

screenshot from an excel spreadsheet

Grouping is very useful in practice. For example, imagine you are a management accountant that has to provide a Gross Profit analysis for your line manager, the Chief Financial Officer and the Board.  It is likely that all three stakeholders will require different levels of detail.  In the past, I have seen many clients prepare three [3] different spreadsheets for these customers, which causes unnecessary re-work and potential reconciliation issues.

This might be a solution which can summarise at those three levels easily:

screenshot from an excel spreadsheet

Pressing the numbered buttons will produce all reports required – simple!

With all this borne in mind, it is useful to be able to automate such grouping, as it is a laborious manual task otherwise.  It requires some VBA code, but it is fairly straightforward (i.e. you can copy it).  Consider this screenshot from the attached Excel file:

screenshot from an excel spreadsheet

I appreciate it is difficult to read, but the structure is the key concept to get from this image.  We want to group the worksheet as follows, using the structure presented above.

First level:

screenshot from an excel spreadsheet

Second level:

screenshot from an excel spreadsheet

Third level:

screenshot from an excel spreadsheet

One simple way here is to group all the rows with used range to the highest level and then ungroup each heading at different levels.  I appreciate the remainder of this article may be overkill for some – so feel free to skip and copy the code or else refer to the attached Excel workbook.

The first step is to define the variables for integers and ranges.

Dim rng As Range
Dim firstRow, lastRow as Integer

If there are existing groupings, we need to expand all rows to avoid potential errors in the following sections of code.

ActiveSheet.Outline.ShowLevels RowLevels:=8, ColumnLevels:=8

Next, we define the starting row and the last row of formula by using the UsedRange method to find the first row and use the total count method to find the last row.  The values of rows are assigned to the variables we defined above.

firstRow = ActiveSheet.UsedRange.Row +5
lastRow = ActiveSheet.UsedRange.Rows(ActiveSheet.UsedRange.Rows.Count).Row

Then, we need to unhide all rows (if any) and clear the existing outline for all rows between the starting row and the last row.  Thus, we can remove all existing groupings and get the used range ready for the grouping pattern, as required.

Rows(firstRow & ":" & lastRow).EntireRow.Hidden = False
Rows(firstRow & ":" & lastRow).ClearOutline
On Error Resume Next

Next, we group the rows between first row and last row three [3] times to the fourth [4th] level of grouping.  We group all the rows at highest level of grouping and then we can start to ungroup each heading.  This is more efficient than selectively grouping.

Rows(firstRow & ":" & lastRow).Rows.Group
Rows(firstRow & ":" & lastRow).Rows.Group
Rows(firstRow & ":" & lastRow).Rows.Group

For the third level of headings, we loop through column D.  If the cell’s value in column D is not blank, then we ungroup the row.  Thus, the headings in column D will be grouped at third level.

For Each rng In Range("D" & firstRow & ":D" & lastRow)
    If Not IsEmpty(rng.Value) Then
        rng.Rows.Ungroup
    End If
Next

For the second level of headings, we loop through each range in column C.  If the cell’s value in column C is not blank, then we ungroup the current row twice and use the VBA Offset function to locate the row above and below the current row and ungroup both rows.  Specifically, if there is a heading in column C, we ungroup the row twice to make the headings grouped at second level.

The reason why the rows above the current row need to be grouped is because we need to keep a gap between the first heading and the second heading.  Therefore, the grouping level at this row should be the same as the second level of headings.  As for the rows beneath the current row, we need to ungroup them in order to create the gap between the second level of headings and the third level of headings.

There is also a complication with column C here.  You will notice that it contains 'Heading level 2 Text' style and also 'Heading level 1 Text' style, whereas on column B it only contains 'Heading level 1 Number' style.  Hence we need a simple VBA If here to check if our headings is actually 'Heading level 2':

If IsEmpty(rng.Offset(-2, -1)) Then rng.Offset(-1, 0).Rows.Ungroup

This needs to be inserted at the end of the following code for the heading in column C:

For Each rng In Range("C" & firstRow & ":C" & lastRow)
    If Not IsEmpty(rng.Value) Then
        rng.Rows.Ungroup
        rng.Rows.Ungroup
        rng.Offset(1, 0).Rows.Ungroup
        If IsEmpty(rng.Offset(-2, -1)) Then rng.Offset(-1, 0).Rows.Ungroup
    End If
Next

For the first level of headings, we loop through the column B.  If the cell’s value in column B is not blank, then we ungroup the current row three [3] times to make the headings at first level of grouping.  Again, we use the Offset function to locate the row above and below the current row and ungroup them twice and once respectively.

For the rows above the current row, we need to ungroup twice to make sure that the first levels of headings are adjacent to each other.  For the rows beneath the current row, we ungroup them to keep a gap between the first heading and the second heading.  Also, we use error handling in the loop.

This is because we need to use error handling in this loop so that if there is no outline in the rows defined above, the macro will not return an error and stop at a specific step in the loop.  However, the error handling ensures the macro will ignore the rows without outline and continue grouping until the end of loop.  Finally, the error handling is closed off with ‘GoTo 0’ syntax.

For Each rng In Range("B" & firstRow & ":B" & lastRow)
    If Not IsEmpty(rng.Value) Then
        On Error Resume Next
        rng.Offset(-1, 0).Rows.Ungroup
        rng.Offset(-2, 0).Rows.Ungroup
        rng.Offset(1, 0).Rows.Ungroup
        rng.Rows.Ungroup
        rng.Rows.Ungroup
        rng.Rows.Ungroup
        On Error GoTo 0
    End If
Next

ActiveSheet.Outline.SummaryRow = xlAbove

Finally, we point back to the range A1, it helps to reset the worksheet to the top left-hand side.

Range("A1").Select
Putting it all together, we get the following:
Sub rowGrouping()

Dim rng As Range
Dim firstRow, lastRow As Integer

'Expand all grouped rows (if any)
ActiveSheet.Outline.ShowLevels RowLevels:=8, ColumnLevels:=8

'Define first row and last row
firstRow = ActiveSheet.UsedRange.Row + 5
lastRow = ActiveSheet.UsedRange.Rows(ActiveSheet.UsedRange.Rows.Count).Row

'Unhide all rows (if any) and clear the outline
Rows(firstRow & ":" & lastRow).EntireRow.Hidden = False
Rows(firstRow & ":" & lastRow).ClearOutline
On Error Resume Next

'Group the rows from first row to last row to grouping level 4
Rows(firstRow & ":" & lastRow).Rows.Group
Rows(firstRow & ":" & lastRow).Rows.Group
Rows(firstRow & ":" & lastRow).Rows.Group

'Ungroup each row in column D that is not empty
For Each rng In Range("D" & firstRow & ":D" & lastRow)
    If Not IsEmpty(rng.Value) Then
        rng.Rows.Ungroup
    End If
Next

'Loop through each range in column C that is not empty
'If the range is not empty, ungroup current row twice
'If the range is not empty, ungroup the rows above and below the current row
For Each rng In Range("C" & firstRow & ":C" & lastRow)
    If Not IsEmpty(rng.Value) Then
        rng.Rows.Ungroup
        rng.Rows.Ungroup
        rng.Offset(1, 0).Rows.Ungroup
        If IsEmpty(rng.Offset(-2, -1)) Then rng.Offset(-1, 0).Rows.Ungroup
    End If
Next

'Loop through each range in column B that is not empty
'If the range is not empty, ungroup the row below current row twice
'If the range is not empty, ungroup the row above current row
'If the range is not empty, ungroup current row three times
For Each rng In Range("B" & firstRow & ":B" & lastRow)
    If Not IsEmpty(rng.Value) Then
        On Error Resume Next
        rng.Offset(-1, 0).Rows.Ungroup
        rng.Offset(-2, 0).Rows.Ungroup
        rng.Offset(1, 0).Rows.Ungroup
        rng.Rows.Ungroup
        rng.Rows.Ungroup
        rng.Rows.Ungroup
        On Error GoTo 0
    End If
Next

ActiveSheet.Outline.SummaryRow = xlAbove

'Go back to range A1
Range("A1").Select

End Sub

Lovely.  This way, we may group the different level of headings automatically.

Word to the Wise

You can make this VBA code more robust by using the code to identify the style of a range:

rng.Style = "Heading 3 Text"
rng.Style = "Heading 2 Text"
rng.Style = "Heading 1 Text"
rng.Style = "Heading 1 Number"

To do this we will rewrite the loop on column D as follows:

For Each rng In Range("D" & firstRow & ":D" & lastRow)
    If Not IsEmpty(rng.Value) And (rng.Style = "Heading 3 Text" Or rng.Style = "Heading 3 Number") Then
        rng.Rows.Ungroup
    End If
Next

This ensures rows are ungrouped correctly, even if users accidentally enter free text in column D. viz

screenshot from an excel spreadsheet

Similarly, you can write the same If condition for column C:

For Each rng In Range("C" & firstRow & ":C" & lastRow)
    If Not IsEmpty(rng.Value) And (rng.Style = "Heading 2 Text" Or rng.Style = "Heading 2 Number") Then
        rng.Rows.Ungroup
        rng.Rows.Ungroup
        rng.Offset(-1, 0).Rows.Ungroup
        rng.Offset(1, 0).Rows.Ungroup
    End If
Next

Finally, we can consider column B:

For Each rng In Range("B" & firstRow & ":B" & lastRow)
    If Not IsEmpty(rng.Value) And (rng.Style = "Heading 1 Text" Or rng.Style = "Heading 1 Number") Then
        On Error Resume Next
        rng.Offset(-1, 0).Rows.Ungroup
        rng.Offset(-1, 0).Rows.Ungroup
        rng.Offset(-2, 0).Rows.Ungroup
        rng.Offset(1, 0).Rows.Ungroup
        rng.Rows.Ungroup
        rng.Rows.Ungroup
        rng.Rows.Ungroup
        On Error GoTo 0
    End If
Next

Adding the style check helps the code identify the correct heading level, rather than simply detecting whether columns B, C or D contain a value.  This version confirms that the cell contains both a value and an appropriate heading style.  Here is the full code for grouping the headings by style:

Sub rowGroupingwithStyle()

Dim rng As Range
Dim firstRow, lastRow As Integer

'Expand all grouped rows (if any)
ActiveSheet.Outline.ShowLevels RowLevels:=8, ColumnLevels:=8

'Define first row and last row
firstRow = ActiveSheet.UsedRange.Row + 5
lastRow = ActiveSheet.UsedRange.Rows(ActiveSheet.UsedRange.Rows.Count).Row

'Unhide all rows (if any) and clear the outline
Rows(firstRow & ":" & lastRow).EntireRow.Hidden = False
Rows(firstRow & ":" & lastRow).ClearOutline
On Error Resume Next

'Group the rows from first row to last row to grouping level 4
Rows(firstRow & ":" & lastRow).Rows.Group
Rows(firstRow & ":" & lastRow).Rows.Group
Rows(firstRow & ":" & lastRow).Rows.Group

'Ungroup each row in column D that is not empty
For Each rng In Range("D" & firstRow & ":D" & lastRow)
    If Not IsEmpty(rng.Value) And (rng.Style = "Heading 3 Text" Or rng.Style = "Heading 3 Number") Then
        rng.Rows.Ungroup
    End If
Next

'Loop through each range in column C that is not empty
'If the range is not empty, ungroup current row twice
'If the range is not empty, ungroup the rows above and below the current row
For Each rng In Range("C" & firstRow & ":C" & lastRow)
    If Not IsEmpty(rng.Value) And (rng.Style = "Heading 2 Text" Or rng.Style = "Heading 2 Number") Then
        rng.Rows.Ungroup
        rng.Rows.Ungroup
        rng.Offset(-1, 0).Rows.Ungroup
        rng.Offset(1, 0).Rows.Ungroup
    End If
Next

'Loop through each range in column B that is not empty
'If the range is not empty, ungroup the row below current row twice
'If the range is not empty, ungroup the row above current row
'If the range is not empty, ungroup current row three times
For Each rng In Range("B" & firstRow & ":B" & lastRow)
    If Not IsEmpty(rng.Value) And (rng.Style = "Heading 1 Text" Or rng.Style = "Heading 1 Number") Then
        On Error Resume Next
        rng.Offset(-1, 0).Rows.Ungroup
        rng.Offset(-1, 0).Rows.Ungroup
        rng.Offset(-2, 0).Rows.Ungroup
        rng.Offset(1, 0).Rows.Ungroup
        rng.Rows.Ungroup
        rng.Rows.Ungroup
        rng.Rows.Ungroup
        On Error GoTo 0
    End If
Next

ActiveSheet.Outline.SummaryRow = xlAbove

'Go back to range A1
Range("A1").Select

End Sub

Archive and Knowledge Base

This archive of Excel Community content from the ION platform will allow you to read the content of the articles but the functionality on the pages is limited. The ION search box, tags and navigation buttons on the archived pages will not work. Pages will load more slowly than a live website. You may be able to follow links to other articles but if this does not work, please return to the archive search. You can also search our Knowledge Base for access to all articles, new and archived, organised by topic.

Open AddCPD icon