How to build a year, make, model dropdown for Australian cars
Build a cascading year, make, model and variant dropdown for Australian cars with SQL Server queries, vanilla JavaScript, indexing and NVIC capture.
Updated
Almost every Australian website that asks about a car - insurance quotes, finance applications, classifieds, workshop bookings, parts shops - uses the same pattern: pick a year, then a make, then a model, then a variant. Each list depends on the one before it, so users only ever see valid options. This guide shows how to build that cascading selector on top of the CarsData dataset in SQL Server, with a small vanilla JavaScript front end, and how to store the result properly so it stays useful later.
The pattern
The selector walks down from broad to specific:
- Year - every year that has vehicles.
- Make - makes that sold vehicles in the chosen year.
- Model - models for that year and make.
- Variant - the exact versions for that year, make and model, each with its own NVIC.
Year comes first because it filters the other lists the most and because people know it with confidence. Some sites put make first instead; the queries below work either way with the order of the filters swapped.
The columns you need
The SQL download creates a table called dbo.CarsData_Vehicles. For the selector you only need a handful of its columns:
| Column | Used for |
|---|---|
Year | First dropdown |
Make | Second dropdown |
Model | Third dropdown |
Variant | Main label in the final dropdown |
Series | Helps tell similar variants apart |
NVIC | The value you store - identifies the exact vehicle version |
Columns such as Body, Transmission, EngineCC and Fuel are useful extras for the variant label. The data dictionary describes every column.
SQL Server queries
Each step is one simple query. Always use parameters rather than string concatenation.
1. Years
SELECT DISTINCT Year
FROM dbo.CarsData_Vehicles
ORDER BY Year DESC;
Newest first suits most forms, because most people are describing a recent car.
2. Makes for a year
SELECT DISTINCT Make
FROM dbo.CarsData_Vehicles
WHERE Year = @Year
ORDER BY Make;
3. Models for a year and make
SELECT DISTINCT Model
FROM dbo.CarsData_Vehicles
WHERE Year = @Year AND Make = @Make
ORDER BY Model;
4. Variants for a year, make and model
SELECT NVIC, Variant, Series, Body, Transmission, EngineCC, Fuel
FROM dbo.CarsData_Vehicles
WHERE Year = @Year AND Make = @Make AND Model = @Model
ORDER BY Variant, Series, Body, Transmission;
Unlike the earlier steps, this one returns whole rows, because each variant is a separate vehicle with its own NVIC.
Making variant labels readable
Variant names alone can repeat - the same trim might be sold as a sedan and a hatch, or with manual and automatic transmissions. Build a label from several columns so every option is distinct:
SELECT NVIC,
CONCAT(Variant,
CASE WHEN Series <> '' THEN ' ' + Series ELSE '' END,
' - ', Body, ' ', Transmission,
CASE WHEN EngineCC > 0 THEN CONCAT(' ', EngineCC, 'cc') ELSE '' END,
CASE WHEN Fuel <> '' THEN ' ' + Fuel ELSE '' END) AS Label
FROM dbo.CarsData_Vehicles
WHERE Year = @Year AND Make = @Make AND Model = @Model
ORDER BY Label;
Transmission and body use short codes (for example A for automatic, H/BAC for hatchback). You may want to map them to words in your application - see body types, transmission and drive codes explained.
Indexing
The table has a primary key on (NVIC, Year), which is ideal for looking up a stored selection but does not help the dropdown queries. Add an index that matches how you filter:
CREATE INDEX IX_CarsData_Vehicles_Make_Model_Year
ON dbo.CarsData_Vehicles (Make, Model, Year)
INCLUDE (Variant, Series, Body, Transmission, EngineCC, Fuel);
CREATE INDEX IX_CarsData_Vehicles_Year_Make
ON dbo.CarsData_Vehicles (Year, Make);
The first index covers the model and variant lookups (and make-first selectors). The second makes "makes for a year" cheap. With roughly 133,279 rows the table is small by database standards, but these indexes keep each request to a quick seek rather than a scan.
A minimal API
Expose each step as a small endpoint that returns JSON. The exact framework doesn't matter; the shape might be:
GET /api/vehicles/years -> [2026, 2025, 2024, ...]
GET /api/vehicles/makes?year=2024 -> ["Abarth", "Alfa Romeo", ...]
GET /api/vehicles/models?year=2024&make=Mazda -> ["BT-50", "CX-3", "CX-5", ...]
GET /api/vehicles/variants?year=2024&make=Mazda&model=CX-5
-> [{"nvic": "...", "label": "Maxx Sport - WAGON A 2488cc PETROL"}, ...]
Return one level at a time. This keeps responses small and fast, and it keeps you within the data licence, which allows showing individual records to users but not exposing the whole dataset through an open API.
Vanilla JavaScript front end
Four select elements and a little script are all you need:
<select id="year"></select>
<select id="make" disabled></select>
<select id="model" disabled></select>
<select id="variant" name="nvic" disabled></select>
const el = id => document.getElementById(id);
const year = el('year'), make = el('make'), model = el('model'), variant = el('variant');
async function getJson(url) {
const res = await fetch(url);
if (!res.ok) throw new Error(res.status + ' ' + url);
return res.json();
}
function fill(select, items, placeholder) {
select.replaceChildren(new Option(placeholder, ''));
for (const item of items) {
const opt = typeof item === 'object'
? new Option(item.label, item.nvic)
: new Option(item, item);
select.add(opt);
}
select.disabled = items.length === 0;
}
function reset(...selects) {
for (const s of selects) { s.replaceChildren(); s.disabled = true; }
}
year.addEventListener('change', async () => {
reset(make, model, variant);
if (!year.value) return;
fill(make, await getJson(`/api/vehicles/makes?year=${year.value}`), 'Make');
});
make.addEventListener('change', async () => {
reset(model, variant);
if (!make.value) return;
const q = new URLSearchParams({ year: year.value, make: make.value });
fill(model, await getJson(`/api/vehicles/models?${q}`), 'Model');
});
model.addEventListener('change', async () => {
reset(variant);
if (!model.value) return;
const q = new URLSearchParams({ year: year.value, make: make.value, model: model.value });
fill(variant, await getJson(`/api/vehicles/variants?${q}`), 'Variant');
});
getJson('/api/vehicles/years').then(years => fill(year, years, 'Year'));
Using new Option() rather than building HTML strings means make, model and variant names are always treated as text, so there is no risk of markup injection. URLSearchParams takes care of encoding names that contain spaces, hyphens or ampersands.
Caching
The dataset only changes when you load a new release, so the dropdown data is ideal for caching:
- Server side - cache each query result in memory, keyed by its parameters. Clear the cache when you import a release.
- HTTP - send a
Cache-Controlheader (for examplepublic, max-age=86400) so browsers and CDNs can reuse responses. - Pre-built files - for a fully static site, you could generate one small JSON file per year and make at build time. Keep each file to a single level of the selection, in line with the licence.
The years list rarely changes at all, so you can also render it straight into the page and save a request.
Store the NVIC, not just the names
When the user submits the form, save the NVIC and Year alongside the text they saw. The names are useful for display and for searching your own records; the NVIC and Year let you join back to the vehicle table at any time:
CREATE TABLE dbo.Quotes (
QuoteID int IDENTITY PRIMARY KEY,
VehicleNVIC varchar(10) NOT NULL,
VehicleYear smallint NOT NULL,
VehicleLabel nvarchar(300) NOT NULL, -- what the user saw, kept for the record
CreatedAt datetime2 NOT NULL DEFAULT SYSUTCDATETIME()
);
Later, a join on NVIC and Year returns full specifications and the latest indicative values, and keeps working after you load a new release. Because the variant list can include several versions with similar names, never try to work out which vehicle a user meant from its label alone.
Tip: add an "I'm not sure" option at the variant step. Store the year, make and model, and let staff or the user pick the exact variant later. A guess at the variant is worse than an honest blank.
Get the data
CarsData covers 125 makes and 1,543 models from 1990 to 2026, and the SQL download creates dbo.CarsData_Vehicles ready for the queries above. Try the free sample, check the data dictionary, browse every make, or get the full dataset for $149 once.
Related guides: loading the data into SQL Server, MySQL, PostgreSQL and Python and what an NVIC code is.