Auto-format new rows to match the table style
Apply borders, fonts, and data validation to appended rows in the Projects sheet automatically.
Published Sep 3, 2025
Northwind’s Projects sheet is a shared workspace, and shared sheets drift.
Someone adds a row at the bottom, the borders stop, the status column loses its
dropdown, and the table slowly stops looking like a table. Asking everyone to
copy the formatting by hand never sticks.
This automation does it for them. Whenever a row is inserted into the
Projects sheet, the script copies the formatting and data validation from the
row directly above it onto the new row. Borders, fonts, number formats and the
status dropdown all carry down automatically, so the table stays consistent no
matter who is editing it.
What you’ll need
- A Google Sheet with a tab named exactly
Projects. - At least one fully formatted data row already in place — the script copies from the row above, so the table needs a style to copy.
- Edit access to the spreadsheet, so you can install the trigger.
The script
// The tab this automation watches. Rows added elsewhere are ignored.
const TARGET_SHEET = 'Projects';
/**
* Runs on every spreadsheet change. When a row is inserted into the
* Projects sheet, it copies formatting and data validation from the
* row above so the new row matches the table style.
*
* @param {Object} e The onChange event object Apps Script passes in.
*/
function onChangeFormat(e) {
// 1. Only react to row insertions — ignore edits, deletes, formatting, etc.
if (e.changeType !== 'INSERT_ROW') return;
// 2. Only act on the Projects sheet; leave every other tab alone.
const sheet = SpreadsheetApp.getActive().getActiveSheet();
if (sheet.getName() !== TARGET_SHEET) return;
// 3. Need at least a header row and one formatted data row to copy from.
const lastRow = sheet.getLastRow();
if (lastRow < 3) return;
// 4. The source is the row above the new one; the target is the new row.
const lastColumn = sheet.getLastColumn();
const sourceRange = sheet.getRange(lastRow - 1, 1, 1, lastColumn);
const targetRange = sheet.getRange(lastRow, 1, 1, lastColumn);
// 5. Copy the visual formatting (borders, fonts, number formats, fills).
sourceRange.copyTo(targetRange, SpreadsheetApp.CopyPasteType.PASTE_FORMAT, false);
// 6. Copy the data validation rules (dropdowns, checkboxes, etc.).
sourceRange.copyTo(targetRange, SpreadsheetApp.CopyPasteType.PASTE_DATA_VALIDATION, false);
}
How it works
onChangeFormatreceives the change event. It checkse.changeTypeand returns immediately unless the change was anINSERT_ROW— edits, deletes and column changes are all ignored.- It checks the active sheet’s name. Only the
Projectstab is formatted; inserting rows anywhere else does nothing. - It reads the last row. If the sheet has fewer than three rows, there is no formatted data row to copy from yet, so the script stops.
- It builds two ranges spanning the full width of the table:
sourceRange, the row above the insertion, andtargetRange, the newly inserted row. - The first
copyTocall pastes only the formatting — borders, fonts, fills and number formats — without touching the cell values. - The second
copyTocall pastes only the data validation, so any dropdowns or checkboxes on the source row are recreated on the new row.
Example run
Before — a teammate inserts a blank row 8 under the formatted table:
| Project | Owner | Status |
|---|---|---|
| Atlas rebrand | Priya | Done |
| Harbour app | Sam | In progress |
| (new row 8 — plain, no borders, no Status dropdown) |
After the insert fires the trigger, row 8 inherits the table style instantly:
| Project | Owner | Status |
|---|---|---|
| Atlas rebrand | Priya | Done |
| Harbour app | Sam | In progress |
| (row 8 — bordered, correct fonts, Status dropdown ready) |
The person just types into a row that already looks right.
Wire it up
The onChange event needs an installable trigger — a simple onChange
function name will not fire on its own:
- In the Apps Script editor, open Triggers (the clock icon).
- Click Add trigger.
- Choose function
onChangeFormat, event source From spreadsheet, and event type On change. - Save and approve the authorisation prompt.
Watch out for
onChangefires for many change types — inserts, deletes, formatting — so the earlychangeTypecheck is essential. Without it the script would run on every edit.- The new row inherits whatever the row above looks like. If someone leaves the bottom row of the table in an odd state, the next inserted row copies that oddness down.
- Inserting several rows at once still copies from a single source row. The
lastRowis the bottom of the new block, so only the bottom new row gets formatted reliably; for bulk inserts, copy the format down by hand. INSERT_ROWevents do not always carry the exact range that was inserted, so the script assumes the insertion is at the bottom of the table. Rows inserted in the middle of the table are not handled.- The trigger runs under the editor who installed it. Other collaborators can still insert rows and the trigger fires, but it does so with that one person’s permissions.
Related
Build a dependent dropdown system
Make the second dropdown filter its options based on the first selection — country → city, client → project.
Updated Sep 17, 2025
Auto-protect ranges after a deadline
Lock cells in the Submissions sheet once a deadline passes — no more late edits.
Updated Sep 13, 2025
Auto-sort a sheet whenever data changes
Keep the Projects sheet ordered by due date without anyone manually sorting.
Updated Sep 10, 2025
Detect and highlight data-entry anomalies
Flag outliers and impossible values on the Expenses sheet the moment a row is added.
Updated Sep 6, 2025
Build a multi-sheet search-and-jump tool
Find a value across every tab of a workbook and click through to the cell that contains it.
Updated Jan 7, 2026