How to Stack Multiple Sublists on One Suitelet Subtab
Sharing a NetSuite tab does not make Suitelet sublists appear one beneath another. The missing piece is a shared subtab. Here is the working serverWidget pattern and its layout caveats.
Sometimes a Suitelet needs two or more related grids to appear as one continuous section.
For example, you might want a short summary sublist at the top of a page and a larger editable sublist directly beneath it:
Data
├── Reporting periods
└── Forecast values
Adding both sublists to the same NetSuite tab sounds like it should produce that layout. It does not.
The missing piece is a shared subtab.
The three sublist layout behaviours
There are three slightly different ways to add multiple sublists to a Suitelet form, and each produces a different result.
| How the sublists are added | What NetSuite renders |
|---|---|
No tab option |
A separate automatic subtab for each sublist |
| The same parent tab ID | An inner tab strip, with one sublist visible at a time |
| The same subtab ID | Sublists stacked vertically in one continuous layout |
The important distinction is between a tab created with form.addTab() and a subtab created with form.addSubtab().
Oracle documents the tab option of Form.addSublist() as the container under which the sublist is displayed. A Form.addSubtab() call can itself be placed beneath a parent tab.
Combining those two APIs gives us the stacked layout.
The working pattern
Create a parent tab, create one subtab inside it, and assign every related sublist to that subtab’s ID.
import serverWidget = require('N/ui/serverWidget');
const form = serverWidget.createForm({
title: 'Forecast Entry',
});
form.addTab({
id: 'custpage_tab_data',
label: 'Data',
});
form.addSubtab({
id: 'custpage_subtab_forecast',
label: 'Forecast',
tab: 'custpage_tab_data',
});
const periods = form.addSublist({
id: 'custpage_periods',
type: serverWidget.SublistType.LIST,
label: 'Reporting Periods',
tab: 'custpage_subtab_forecast',
});
const values = form.addSublist({
id: 'custpage_values',
type: serverWidget.SublistType.INLINEEDITOR,
label: 'Forecast Values',
tab: 'custpage_subtab_forecast',
});
Both sublists now belong to custpage_subtab_forecast, so NetSuite renders Forecast Values beneath Reporting Periods instead of placing them behind separate subtab links.
Add order controls vertical order
NetSuite renders stacked sublists in the order they are added to the form.
In the previous example:
custpage_periodsis added first.custpage_valuesis added second.- The reporting-period grid appears above the forecast-entry grid.
If the larger entry grid should appear first, reverse the two addSublist() calls.
This ordering rule is helpful when you want a small read-only context grid above an editable working area.
Why sharing a tab is not enough
This version looks plausible but does not stack the sublists:
form.addTab({
id: 'custpage_tab_data',
label: 'Data',
});
form.addSublist({
id: 'custpage_periods',
type: serverWidget.SublistType.LIST,
label: 'Reporting Periods',
tab: 'custpage_tab_data',
});
form.addSublist({
id: 'custpage_values',
type: serverWidget.SublistType.INLINEEDITOR,
label: 'Forecast Values',
tab: 'custpage_tab_data',
});
Both sublists share the Data tab, but NetSuite creates an inner tab strip for them. A user still has to switch between the two grids.
Think of the layout hierarchy this way:
Form
└── Tab
└── Shared subtab
├── First sublist
└── Second sublist
The shared subtab is the common container that produces the continuous vertical layout.
A complete Suitelet example
Here is a minimal Suitelet that creates and populates two stacked sublists:
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
import serverWidget = require('N/ui/serverWidget');
import type { EntryPoints } from 'N/types';
export const onRequest: EntryPoints.Suitelet.onRequest = (context) => {
const form = serverWidget.createForm({
title: 'Forecast Entry',
});
form.addTab({
id: 'custpage_tab_data',
label: 'Data',
});
const containerId = 'custpage_subtab_forecast';
form.addSubtab({
id: containerId,
label: 'Forecast',
tab: 'custpage_tab_data',
});
const periods = form.addSublist({
id: 'custpage_periods',
type: serverWidget.SublistType.LIST,
label: 'Reporting Periods',
tab: containerId,
});
periods.addField({
id: 'custpage_period_name',
type: serverWidget.FieldType.TEXT,
label: 'Period',
});
periods.addField({
id: 'custpage_period_status',
type: serverWidget.FieldType.TEXT,
label: 'Status',
});
periods.setSublistValue({
id: 'custpage_period_name',
line: 0,
value: 'July 2026',
});
periods.setSublistValue({
id: 'custpage_period_status',
line: 0,
value: 'Open',
});
const values = form.addSublist({
id: 'custpage_values',
type: serverWidget.SublistType.INLINEEDITOR,
label: 'Forecast Values',
tab: containerId,
});
values.addField({
id: 'custpage_category',
type: serverWidget.FieldType.TEXT,
label: 'Category',
});
values.addField({
id: 'custpage_amount',
type: serverWidget.FieldType.CURRENCY,
label: 'Amount',
});
form.addSubmitButton({
label: 'Save',
});
context.response.writePage(form);
};
The technique works with different sublist types. A common combination is a small LIST sublist above an INLINEEDITOR sublist, but the container behaviour is independent of the types you choose.
Other sublists can remain separate
Not every sublist on the form has to use the shared container.
If a history or audit grid should retain its own automatic subtab, leave out the tab option:
const history = form.addSublist({
id: 'custpage_history',
type: serverWidget.SublistType.LIST,
label: 'Previous Entries',
});
This does not affect the sublists stacked under custpage_subtab_forecast.
Column widths are still independent
Stacking controls vertical placement, but it does not make columns across different sublists share widths.
Each sublist calculates its column layout independently. NetSuite’s documented serverWidget API does not provide a general column-width option for aligning one sublist with another.
That means two stacked grids may look like this:
| Period | Status |
| Category | Amount |
If exact cross-sublist alignment is essential, a Client Script can adjust the rendered table widths during pageInit. Treat that as progressive enhancement: DOM selectors are more fragile than the supported serverWidget API and may need adjustment after a NetSuite UI update.
Where possible, design each sublist to read clearly on its own rather than depending on pixel-perfect shared columns.
If you are editing an actual NetSuite record sublist rather than constructing a Suitelet form, the API depends on the record mode. See SuiteScript Standard vs Dynamic Mode: Which Sublist APIs to Use.
Watch for sublist ID collisions
Use distinct, descriptive IDs for body fields, subtabs and sublists.
LIST sublists can introduce hidden form inputs based on the sublist ID. In particular, avoid naming a body field so that its ID equals a sublist ID followed by type.
For example, do not combine these IDs:
const sublistId = 'custpage_entry';
const bodyFieldId = 'custpage_entrytype';
That naming pattern can collide with NetSuite’s hidden custpage_entrytype marker and cause the body field value to behave unexpectedly. A safer pair would be:
const sublistId = 'custpage_entry_lines';
const bodyFieldId = 'custpage_entry_type';
Keeping IDs in constants also makes a later rename much safer, especially when the same sublist ID is used to parse POST parameters.
Troubleshooting checklist
If your Suitelet sublists are not stacking:
- Confirm that you called
form.addSubtab(). - Give the subtab a unique lowercase
custpage_ID. - If it belongs beneath a parent tab, pass that tab’s ID to
addSubtab({ tab }). - Pass the subtab ID, not the parent tab ID, to every related
addSublist({ tab })call. - Add the sublists in the order they should appear vertically.
- Check field and sublist IDs for hidden-name collisions.
- Treat column alignment as a separate presentation problem.
The final pattern is small, but the distinction matters: a shared tab groups sublists behind navigation; a shared subtab lets them form one continuous layout.