Build a dependent dropdown system
Make the second dropdown filter its options based on the first selection — country → city, client → project.
Published Sep 17, 2025
A flat dropdown of every project Northwind has ever run is no help when you are filling in a timesheet. Pick the wrong one, log time against the wrong client, and the invoice is wrong too. The fix everyone wants is the obvious one: choose a client, and the project dropdown should only show that client’s projects.
Sheets has no built-in dependent dropdown, but an onEdit trigger gets you
there. When someone picks a client, the script looks up that client’s projects
and rewrites the data-validation rule on the project cell next to it. The second
dropdown is always scoped to the first selection.
What you’ll need
- A
Timesheetssheet where the client goes in column B and the project in column C. - A
Lookupssheet with two columns —clientandproject— listing every valid client/project pair, one per row. - An installable
onEdittrigger (set up below). The simple trigger cannot be used here because the script reads another spreadsheet.
The script
// The spreadsheet that holds the client -> project pairs.
const LOOKUPS_SHEET_ID = '1abcLookupsSheetId';
// Column numbers on the Timesheets sheet (1-based).
const CLIENT_COL = 2; // column B — the dropdown you pick first
const PROJECT_COL = 3; // column C — the dropdown that gets filtered
// The tab the dependent dropdown applies to.
const TARGET_SHEET = 'Timesheets';
/**
* Installable onEdit handler. When a client is chosen in column B,
* rewrites the project dropdown in column C to that client's projects.
*
* @param {Object} e The edit event passed in by the trigger.
*/
function onEdit(e) {
// Guard: ignore calls with no event or range (e.g. manual runs).
if (!e || !e.range) return;
const sheet = e.range.getSheet();
// Only react to edits in column B of the Timesheets tab.
if (sheet.getName() !== TARGET_SHEET || e.range.getColumn() !== CLIENT_COL) {
return;
}
const client = e.value;
// Guard: a cleared client cell means there is nothing to filter on.
if (!client) return;
// Look up the projects for this client.
const projects = getProjectsFor(client);
// Build a fresh data-validation rule from that project list.
const cell = sheet.getRange(e.range.getRow(), PROJECT_COL);
const rule = SpreadsheetApp.newDataValidation()
.requireValueInList(projects, true) // true = show as a dropdown
.setAllowInvalid(false) // reject anything off the list
.build();
// Apply the rule and clear any stale project from the old client.
cell.setDataValidation(rule);
cell.clearContent();
}
/**
* Returns the list of projects belonging to one client,
* read from the Lookups sheet.
*
* @param {string} client The client name to filter by.
* @return {string[]} That client's project names.
*/
function getProjectsFor(client) {
const [, ...rows] = SpreadsheetApp.openById(LOOKUPS_SHEET_ID)
.getSheets()[0]
.getDataRange()
.getValues();
return rows
.filter((row) => row[0] === client) // match the client column
.map((row) => row[1]); // keep the project column
}
How it works
- The installable
onEdittrigger firesonEditafter every edit and passes an event object describing what changed. - The handler bails out unless the edit was a real range edit in column B of
the
Timesheetstab — every other edit on the sheet is ignored. - It reads the newly entered client from
e.value. If the cell was cleared, there is nothing to filter on, so it stops. getProjectsForopens theLookupssheet, drops the header row, keeps only rows whose first column matches the client, and returns their project names.- The handler builds a new data-validation rule with
requireValueInList, passing the filtered project list.setAllowInvalid(false)blocks any value not on the list. - It applies that rule to column C of the same row, then clears the cell — so a project left over from a previous client cannot stay behind invalidly.
Example run
The Lookups sheet holds:
| client | project |
|---|---|
| Atlas Foods | Q3 rebrand |
| Atlas Foods | Packaging refresh |
| Borealis | Website build |
A user opens a timesheet row and picks Atlas Foods in column B. The script runs and the column C dropdown immediately offers only:
- Q3 rebrand
- Packaging refresh
Change column B to Borealis and column C clears, then re-opens showing just Website build. The project list can never contradict the chosen client.
Wire it up
The simple onEdit cannot open another spreadsheet, so an installable trigger
is required:
- In the Apps Script editor, open Triggers (the clock icon).
- Click Add trigger.
- Choose the
onEditfunction, event source From spreadsheet, event type On edit. - Save and approve the authorisation prompt — installable triggers run with
full permissions, which is what lets the script read the
Lookupssheet.
Watch out for
- The simple trigger is not enough. A function named
onEditruns as a simple trigger by default, but simple triggers cannot callopenById. You must add the installable trigger above, or the lookup silently fails. e.valueis only set for single-cell edits. A paste or fill across column B will not carry ae.value, so those rows are skipped — paste with care.- Changing the client clears column C. That is intentional, but a user who re-picks the same client by accident still loses their project entry.
- Every client edit reads the whole
Lookupssheet. For a large lookup table this adds latency to each edit; cache the projects withCacheServiceif it feels sluggish. - New client/project pairs must be added to
Lookupsto appear. The dropdown is only ever as current as that sheet.
Related
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
Auto-format new rows to match the table style
Apply borders, fonts, and data validation to appended rows in the Projects sheet automatically.
Updated Sep 3, 2025
Build an OKR tracker with progress rollups
Aggregate Northwind's key-result progress into objective-level scores automatically.
Updated Jan 14, 2026