How to Freeze Columns in an Interactive Grid in Oracle APEX

Keep key columns in view while users scroll a wide Interactive Grid: freeze from the column menu, save the default report, and keep personal layouts in private reports.

Wide grids are hard to read. When a row has a dozen columns, users scroll to the right to see dates or amounts, and the column that names the row scrolls out of view. They then lose track of which row they are looking at.

The Oracle APEX Interactive Grid can freeze columns. A frozen column stays at the left edge while the other columns scroll past it. This guide freezes a column as a user, saves it as the default for everyone, freezes a second column, and keeps a personal layout in a private report.

The guide Oracle APEX Interactive Grid: A Complete Guide mentions freezing in one sentence, and How to Use the Grid, Table, and Single Row Views of an Oracle APEX Interactive Grid shows the freezeColumn JavaScript method. This article covers the declarative way: the column menu, the saved reports, and how freezing behaves.

The example uses a list of fleet vehicles; the same steps work for any table.

Step 1: Create the Tables

Two tables hold the data:

  • FLEET_DEPOTS lists the depots where vehicles are kept.
  • FLEET_VEHICLES has one row per vehicle, with thirteen visible columns: enough to make the grid wider than the screen.

Example:

set define off

create table fleet_depots (
    depot_id    number generated by default on null as identity,
    depot_name  varchar2(40) not null,
    city        varchar2(40) not null,
    constraint fleet_depots_pk primary key (depot_id),
    constraint fleet_depots_name_uk unique (depot_name)
);

create table fleet_vehicles (
    vehicle_id         number generated by default on null as identity,
    reg_no             varchar2(12)  not null,
    make               varchar2(30)  not null,
    model              varchar2(30)  not null,
    model_year         number(4)     not null,
    fuel_type          varchar2(10)  not null,
    depot_id           number        not null,
    driver_name        varchar2(60),
    odometer_km        number(7)     default on null 0 not null,
    last_service_date  date,
    next_service_date  date,
    insurance_expiry   date          not null,
    purchase_price     number(10,2)  not null,
    status             varchar2(12)  default on null 'ACTIVE' not null,
    constraint fleet_vehicles_pk primary key (vehicle_id),
    constraint fleet_vehicles_reg_uk unique (reg_no),
    constraint fleet_vehicles_depot_fk foreign key (depot_id)
        references fleet_depots (depot_id),
    constraint fleet_vehicles_fuel_ck check (fuel_type in ('DIESEL', 'PETROL', 'ELECTRIC', 'HYBRID')),
    constraint fleet_vehicles_status_ck check (status in ('ACTIVE', 'IN SERVICE', 'RETIRED')),
    constraint fleet_vehicles_year_ck check (model_year between 2000 and 2030)
);

create index fleet_vehicles_depot_ix on fleet_vehicles (depot_id);

insert into fleet_depots (depot_name, city) values ('North Yard', 'Leeds');
insert into fleet_depots (depot_name, city) values ('City Hub', 'Manchester');
insert into fleet_depots (depot_name, city) values ('Harbour Base', 'Liverpool');

insert into fleet_vehicles (reg_no, make, model, model_year, fuel_type, depot_id, driver_name, odometer_km, last_service_date, next_service_date, insurance_expiry, purchase_price, status)
select 'FV21 KTA', 'Ford', 'Transit Custom', 2021, 'DIESEL', depot_id, 'Asha Menon', 84210, date '2026-06-14', date '2026-12-14', date '2027-03-31', 31450, 'ACTIVE' from fleet_depots where depot_name = 'North Yard';
insert into fleet_vehicles (reg_no, make, model, model_year, fuel_type, depot_id, driver_name, odometer_km, last_service_date, next_service_date, insurance_expiry, purchase_price, status)
select 'FV22 LMB', 'Volkswagen', 'Crafter', 2022, 'DIESEL', depot_id, 'Brian Cole', 61500, date '2026-08-02', date '2027-02-02', date '2027-05-15', 38900, 'ACTIVE' from fleet_depots where depot_name = 'North Yard';
insert into fleet_vehicles (reg_no, make, model, model_year, fuel_type, depot_id, driver_name, odometer_km, last_service_date, next_service_date, insurance_expiry, purchase_price, status)
select 'FV23 EVA', 'Renault', 'Kangoo E-Tech', 2023, 'ELECTRIC', depot_id, 'Carla Diaz', 22340, date '2026-09-10', date '2027-09-10', date '2027-01-20', 34200, 'ACTIVE' from fleet_depots where depot_name = 'City Hub';
insert into fleet_vehicles (reg_no, make, model, model_year, fuel_type, depot_id, driver_name, odometer_km, last_service_date, next_service_date, insurance_expiry, purchase_price, status)
select 'FV20 PQR', 'Mercedes-Benz', 'Sprinter', 2020, 'DIESEL', depot_id, 'Dev Patel', 128900, date '2026-10-01', date '2026-11-05', date '2026-12-31', 41750, 'IN SERVICE' from fleet_depots where depot_name = 'City Hub';
insert into fleet_vehicles (reg_no, make, model, model_year, fuel_type, depot_id, driver_name, odometer_km, last_service_date, next_service_date, insurance_expiry, purchase_price, status)
select 'FV24 HYB', 'Toyota', 'Proace City', 2024, 'HYBRID', depot_id, 'Elena Rossi', 9800, date '2026-07-21', date '2027-07-21', date '2027-06-30', 29900, 'ACTIVE' from fleet_depots where depot_name = 'City Hub';
insert into fleet_vehicles (reg_no, make, model, model_year, fuel_type, depot_id, driver_name, odometer_km, last_service_date, next_service_date, insurance_expiry, purchase_price, status)
select 'FV19 OLD', 'Vauxhall', 'Vivaro', 2019, 'DIESEL', depot_id, null, 176400, date '2025-11-30', null, date '2026-11-30', 26300, 'RETIRED' from fleet_depots where depot_name = 'Harbour Base';
insert into fleet_vehicles (reg_no, make, model, model_year, fuel_type, depot_id, driver_name, odometer_km, last_service_date, next_service_date, insurance_expiry, purchase_price, status)
select 'FV22 PET', 'Peugeot', 'Partner', 2022, 'PETROL', depot_id, 'Farid Khan', 45120, date '2026-05-18', date '2026-11-18', date '2027-04-12', 23800, 'ACTIVE' from fleet_depots where depot_name = 'Harbour Base';
insert into fleet_vehicles (reg_no, make, model, model_year, fuel_type, depot_id, driver_name, odometer_km, last_service_date, next_service_date, insurance_expiry, purchase_price, status)
select 'FV23 ELC', 'Nissan', 'Townstar EV', 2023, 'ELECTRIC', depot_id, 'Grace Lee', 18760, date '2026-09-25', date '2027-09-25', date '2027-08-01', 33100, 'ACTIVE' from fleet_depots where depot_name = 'Harbour Base';

commit;

select v.reg_no, v.make, v.model, v.fuel_type, d.depot_name, v.driver_name,
       v.odometer_km, to_char(v.next_service_date, 'YYYY-MM-DD') as next_service, v.status
from fleet_vehicles v
join fleet_depots d on d.depot_id = v.depot_id
order by v.reg_no;

Output:

Table FLEET_DEPOTS created.

Table FLEET_VEHICLES created.

Index FLEET_VEHICLES_DEPOT_IX created.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

Commit complete.

REG_NO      MAKE             MODEL             FUEL_TYPE    DEPOT_NAME      DRIVER_NAME       ODOMETER_KM NEXT_SERVICE    STATUS
___________ ________________ _________________ ____________ _______________ ______________ ______________ _______________ _____________
FV19 OLD    Vauxhall         Vivaro            DIESEL       Harbour Base                           176400                 RETIRED
FV20 PQR    Mercedes-Benz    Sprinter          DIESEL       City Hub        Dev Patel              128900 2026-11-05      IN SERVICE
FV21 KTA    Ford             Transit Custom    DIESEL       North Yard      Asha Menon              84210 2026-12-14      ACTIVE
FV22 LMB    Volkswagen       Crafter           DIESEL       North Yard      Brian Cole              61500 2027-02-02      ACTIVE
FV22 PET    Peugeot          Partner           PETROL       Harbour Base    Farid Khan              45120 2026-11-18      ACTIVE
FV23 ELC    Nissan           Townstar EV       ELECTRIC     Harbour Base    Grace Lee               18760 2027-09-25      ACTIVE
FV23 EVA    Renault          Kangoo E-Tech     ELECTRIC     City Hub        Carla Diaz              22340 2027-09-10      ACTIVE
FV24 HYB    Toyota           Proace City       HYBRID       City Hub        Elena Rossi              9800 2027-07-21      ACTIVE

8 rows selected.
  • FLEET_VEHICLES_DEPOT_FK links each vehicle to a depot, and FLEET_VEHICLES_DEPOT_IX indexes that foreign key.
  • FLEET_VEHICLES_REG_UK keeps the registration number unique. It is the column users need to see all the time.
  • The check constraints allow only known fuel types, statuses and model years. ODOMETER_KM and STATUS use DEFAULT ON NULL, so new rows get 0 and ACTIVE without a trigger.

Step 2: Create the Interactive Grid Page

In App Builder, open the application and click Create Page. On the Component tab, select Interactive Grid:

Create a Page wizard with the Interactive Grid tile marked with a red box
Choose the Interactive Grid page type.

Set Page Number to 950 and Name to Fleet Vehicles. Keep Page Mode Normal:

Create Interactive Grid screen with Page Number 950 and Name Fleet Vehicles marked with a red box
The page number and name.

Under Data Source, keep Source Type Table and set Table / View Name to FLEET_VEHICLES. Leave Editing Enabled off, because this page only displays the vehicles:

Data Source section with Table / View Name FLEET_VEHICLES marked with a red box
A read-only grid on the vehicles table.

Keep the Navigation settings and click Create Page. A read-only grid has no Primary Key screen, so the page is created right away:

Navigation section with Use Breadcrumb and Use Navigation on and the Create Page button
The default navigation settings.

Step 3: Show the Depot Name and Set an HTML DOM ID

The wizard shows DEPOT_ID as a number. In Page Designer, expand Fleet Vehicles and Columns, and select DEPOT_ID. Set Type to Select List and Heading to Depot:

Column DEPOT_ID with Type Select List and Heading Depot marked with red boxes
The depot column becomes a select list.

Under List of Values, set Type to SQL Query and enter this query:

Example:

select depot_name as display_value,
       depot_id as return_value
from fleet_depots
order by depot_name
List of Values group of DEPOT_ID with Type SQL Query and the query marked with a red box
A read-only grid shows the depot name instead of the ID.

Select the Fleet Vehicles region and, under Advanced, set HTML DOM ID to fleet_vehicles. The test section uses it to read the frozen columns from the browser console. Click Save:

Advanced group of the Fleet Vehicles region with HTML DOM ID fleet_vehicles marked with a red box
A fixed ID for the grid region.

Step 4: Give the Columns a Minimum Width

Run the page while you are logged in to App Builder. The grid squeezes all thirteen columns into the window, so long headings and values are cut off:

Fleet Vehicles grid with thirteen narrow columns and cut-off headings such as Last Service D and Purchase Pric
All columns fit, but none of them is readable.

Freezing only helps when the grid scrolls sideways. Open Actions and click Columns:

Actions menu with the Columns entry marked with a red box
The Columns dialog sets the order, visibility and width.

Click Reg No in the list and enter 150 in Minimum Column Width (Pixel). Repeat this for every column, then click Save:

Columns dialog with Reg No selected and Minimum Column Width 150 marked with a red box
Each column gets a minimum width of 150 pixels.

Thirteen columns of 150 pixels need 1950 pixels, so the grid now has a horizontal scroll bar. The columns are readable:

Fleet Vehicles grid with readable columns from Reg No to Last Service Date and the rest off screen
The grid is wider than the window.

Scroll the grid to the right. The registration numbers disappear, and the rows can no longer be told apart:

Grid scrolled to the right showing Depot to Status columns without the Reg No column
Without a frozen column, the row names scroll away.

Step 5: Freeze the Reg No Column

Scroll back to the left and click the Reg No heading. The heading menu has four buttons: Control Break, Aggregate, Freeze and Hide. Click Freeze, the snowflake icon:

Reg No heading menu open with the Freeze button marked with a red box
Freeze is in the column heading menu.

Scroll to the right again. Reg No stays at the left edge, and the other columns slide under it:

Grid scrolled to the right with the frozen Reg No column at the left edge marked with a red box
Every row still shows its registration number.

Step 6: Save the Frozen Column as the Default

The freeze and the widths are now part of your own view only. To give them to every user, open Actions, Report and click Save:

Actions menu with Report open and the Save entry marked with a red box
Save the layout into the default report.

Because you run the page as a developer, APEX saves the layout as the primary report and shows Default report saved for all users. The frozen column, the column order and the widths are stored with the page and exported with the application.

Step 7: Check the Default as an End User

Log in as an end user and open Fleet Vehicles. The grid opens with the 150-pixel columns and Reg No frozen. Scrolled to the right, it looks like this:

Grid of an end user scrolled to the right with the frozen Reg No column marked with a red box
The end user gets the developer's frozen column.

Step 8: Freeze More Columns

Frozen columns always form one block at the left of the grid. In a test, the end user clicked the Driver Name heading and clicked Freeze. APEX froze Driver Name and every column to its left, seven columns in all:

Grid with seven frozen columns from Reg No to Driver Name marked with a red box and little room left to scroll
Freezing Driver Name froze Make, Model, Model Year, Fuel Type and Depot too.

Seven frozen columns take 1050 of the 1246 pixels of the grid, so only a narrow strip is left for scrolling. To undo it, open the Driver Name heading again. The button is now pressed and named Unfreeze:

Driver Name heading menu with the pressed Unfreeze button marked with a red box
The same button unfreezes the column.

Unfreeze on the last frozen column releases only that column. In another test, Unfreeze on Make, inside the block, released Make and all frozen columns to its right; only Reg No stayed frozen.

Step 9: Move a Column Next to the Frozen One

To freeze Reg No and Driver Name only, Driver Name must sit right after Reg No. Open Actions, Columns, click Driver Name and click the Move Up button five times. Then click Save:

Columns dialog with Driver Name moved under Reg No and the Move Up button marked with red boxes
Driver Name is now the second column.

Now reload the page (F5 or Cmd+R) before you freeze the column. In three tests, freezing a column right after moving it in the Columns dialog looked fine in the browser. The request sent to the server had an empty column list, though, so the freeze was lost after a reload and in saved reports.

After the reload, click the Driver Name heading and click Freeze. Two columns are frozen, and the rest of the grid scrolls:

Grid scrolled to the right with the frozen Reg No and Driver Name columns marked with a red box
Reg No and Driver Name stay in view.

Step 10: Keep the Layout in a Private Report

An end user cannot change the default report. The changes so far live only in the current session; after the next login, the grid opens with the default layout again. To keep them, open Actions, Report and click Save As:

Actions menu with Report open and the Save As entry marked with a red box
Save As creates a new saved report.

Keep Type Private, enter My Fleet View as the Name and click Save:

Report Save As dialog with Type Private and Name My Fleet View marked with a red box
A private report belongs to this user only.

APEX shows Private report saved, and a Saved Reports list appears in the toolbar with My Fleet View selected:

Grid toolbar with the saved reports list showing My Fleet View marked with a red box
The user can switch between Primary Report and My Fleet View.

Log out and log in again as the same user. The grid opens My Fleet View, with both columns frozen:

New session with My Fleet View selected and the frozen Reg No and Driver Name columns marked with red boxes
The private report keeps the frozen columns across sessions.

Selecting Primary Report in the list brings back the default layout with only Reg No frozen.

Test the Saved Reports

First, check in the browser which columns are frozen. Open the browser console on the page and run:

Example:

apex.region("fleet_vehicles").call("getViews", "grid").view$
    .grid("getColumns")
    .filter(c => c.frozen)
    .map(c => c.label)

Output:

["Reg No", "Driver Name"]

With Primary Report selected, the same call returned only ["Reg No"].

Second, check the saved reports in SQL. The APEX dictionary views show the frozen columns of each report:

Example:

select r.type, r.name, r.application_user, ic.name as column_name, rc.width
from apex_appl_page_ig_rpts r
join apex_appl_page_ig_rpt_columns rc on rc.report_id = r.report_id
join apex_appl_page_ig_columns ic on ic.column_id = rc.column_id
where r.application_id = 400
and r.page_id = 950
and r.type in ('PRIMARY', 'PRIVATE')
and rc.is_frozen = 'Yes'
and ic.name <> 'VEHICLE_ID'
order by r.type, rc.display_seq;

Output:

TYPE       NAME             APPLICATION_USER    COLUMN_NAME       WIDTH
__________ ________________ ___________________ ______________ ________
PRIMARY                                         REG_NO              150
PRIVATE    My Fleet View    SALES_USER          REG_NO              150
PRIVATE    My Fleet View    SALES_USER          DRIVER_NAME         150

The primary report freezes REG_NO for everyone, and the private report of SALES_USER freezes REG_NO and DRIVER_NAME. The query leaves out VEHICLE_ID: APEX also marked the hidden primary key column, which sits before Reg No, as frozen.

Every session that changes the grid gets its own working copy of type SESSION. The test sessions of this example left eleven of them:

Example:

select type, count(*) as reports
from apex_appl_page_ig_rpts
where application_id = 400
and page_id = 950
group by type
order by type;

Output:

TYPE          REPORTS
__________ __________
PRIMARY             1
PRIVATE             1
SESSION            11

Third, freeze too much. With the primary report, the end user froze Last Service Date, which froze nine columns of 150 pixels. They are wider than the grid, so the frozen block covers the whole view:

Grid with nine frozen columns filling the whole width so that Next Service Date to Status cannot be seen
Next Service Date, Insurance Expiry, Purchase Price and Status cannot be reached.

Scrolling did not bring the last columns into view, and their headings could not be clicked. Unfreeze from the heading menu, or select the saved report again, to get them back.

Things to Know

  • There is no Freeze property on a column in Page Designer. A frozen column is a setting of a saved report, so the developer sets it at runtime and saves the primary report (Step 6).
  • Frozen columns are always the first columns of the grid. Freezing a column further right freezes everything to its left; move the column first (Step 9).
  • After moving a column in the Columns dialog, reload the page before you freeze it. Without the reload, the freeze was not saved in three tests, even though the grid showed it.
  • Keep the frozen block narrow. The grid stretches columns to fit the window, so freezing makes sense only when the columns have a minimum width and the grid scrolls sideways.
  • End user changes last for the session only. A private report (Save As) keeps them, and in this example APEX opened it again at the next login.

Related Guides

Conclusion

To freeze columns in an Interactive Grid in Oracle APEX, give the columns a minimum width so the grid scrolls, then click Freeze in the column heading menu. Save the primary report as a developer to make the frozen column the default for everyone.

In this example, Reg No stayed in view for every user, and an end user added Driver Name in a private report that survived the next login. Freezing works best for one or two key columns placed at the start of the grid.

Get more Vinish.dev tutorials on Google

See my articles more often when you search Google.

Vinish Kapoor
Vinish Kapoor

An Oracle ACE, author of four books on Oracle APEX, SQL and PL/SQL, and Oracle Forms, and a software developer building Oracle database applications since 2001.

guest

0 Comments
Oldest
Newest Most Voted