Building the Carry-On and Personal Item Size Checkers

Two free tools that tell you whether your bag fits a given airline, built on a hand-verified dataset of 50 carriers, orientation-agnostic math, and a single JSON file.

· · 7 min read

Last summer I watched a gate agent at Stansted pull a woman’s backpack out of her hand, drop it into a Ryanair sizer, and charge her €55 because it was half an inch tall in one dimension. The bag was “carry-on approved” according to the Amazon listing. She had measured it at home. The problem was that she had measured it against American Airlines limits, which is what every US-facing review compared against, and Ryanair is a different airline with different rules.

I went home and started a spreadsheet. A few weeks later the spreadsheet became a JSON file. A few weeks after that, it became two tools: the Carry-On Size Checker and the Personal Item Size Checker.

What they do

Both tools take the same input (length, width, height, in inches or centimeters) and compare your bag against the published baggage rules for 50 airlines worldwide. You see, at a glance, which airlines will accept the bag as a carry-on or as a personal item and which will send you to the fee counter.

You can filter by region (North America, Europe, Asia, etc.) or by category (Full-service, Low-cost, Ultra-low-cost) since what passes on Delta is not the same as what passes on Spirit. Every airline card also links to a detail page with the full baggage policy: checked bag fees, overweight surcharges, basic economy restrictions, gate-check risk, and the source URL for the data.

The stack

This one is boring on purpose. Static Astro 5 site, Tailwind v4, one big JSON file, a few TypeScript helpers, and vanilla JavaScript in the browser for filtering. No React, no API, no database, no server.

Every page is generated at build time. The bag fit check runs entirely client-side against data attributes baked into each airline card during the build. The whole thing deploys to Cloudflare Pages as static HTML and runs faster than any SaaS version of the same tool I tried.

The data lives in src/data/airlines/airlines.json, which is a 4,000-line hand-verified file. Every airline entry includes carry-on dimensions in both inches and centimeters, personal item rules, checked bag fee structure, basic economy restrictions, and a sourceUrl pointing at the airline’s own published policy page. Every entry also has a lastVerified date. When I update a record I update the date. If the date is stale, I know to re-check.

The hard part: bag orientation

This is the detail that almost everyone building a similar tool gets wrong. Airline dimensions are published as length × width × height, but bags are not. A 22×14×9 carry-on measured one way is a 9×22×14 bag measured another way. If you compare them position-by-position you will tell someone their bag does not fit when it does.

The fix is to sort both the bag and the limit largest-to-smallest before comparing:

export function bagFitsPersonalItem(
  bag: Dimensions,
  unit: 'in' | 'cm',
  airline: Airline
): PersonalItemFit {
  const limit = unit === 'in' ? airline.personalItem.dimensionsIn : airline.personalItem.dimensionsCm;
  if (!airline.personalItem.allowed) return 'too-big';
  if (!limit) return 'under-seat-only';
  const b = [bag.length, bag.width, bag.height].sort((a, b) => b - a);
  const l = [limit.length, limit.width, limit.height].sort((a, b) => b - a);
  return b[0] <= l[0] && b[1] <= l[1] && b[2] <= l[2] ? 'fits' : 'too-big';
}

This also runs in the browser for the interactive filter. Same logic, data attributes on each card, no hydration framework needed:

const bagSorted = [bagCheck.l, bagCheck.w, bagCheck.h].sort((a, b) => b - a);
const limSorted = [limL, limW, limH].sort((a, b) => b - a);
const ok =
  bagSorted[0] <= limSorted[0] &&
  bagSorted[1] <= limSorted[1] &&
  bagSorted[2] <= limSorted[2];

It is an embarrassingly small amount of code for how much it matters. But it is the difference between a tool that gives you the right answer and a tool that misses a third of passing bags because the user entered them in a different order.

The “not published” problem

The other thing that bit me is that half of US airlines do not publish personal item dimensions. Delta, American, and United all say something like “must fit under the seat in front of you” and leave it at that. If I treat that as “no limit,” I am lying to users. If I treat it as “does not allow personal items,” I am also lying.

The schema has a three-state return for this reason:

export type PersonalItemFit = 'fits' | 'too-big' | 'under-seat-only';

And the data model lets a personal item be allowed: true with null dimensions:

"personalItem": {
  "allowed": true,
  "dimensionsIn": null,
  "dimensionsCm": null,
  "notes": "Must fit under the seat in front of you. No published dimensions, but purses, small backpacks, and laptop bags are accepted."
}

The UI shows an amber “Under-seat only” badge for these cases and links the user to the airline’s own policy page. It is not a satisfying answer, but it is an honest one, and it is better than pretending the airline has clear rules when it does not.

What went wrong

The original version of the tool supported 20 airlines and had a single dimensions schema. I thought that would be enough. It was not. Within a week of launching I had feedback asking about Wizz Air, TAP Portugal, LATAM, Qantas, and half a dozen Asian carriers. Each one had slightly different rules. TAP uses weight differently. Wizz Air has a “Priority” upgrade that changes the personal item size. Qantas varies by route. The schema grew to cover all of it, but the JSON file tripled in size and every addition meant another trip to the airline’s policy page to re-verify numbers I had already verified two months earlier.

The other thing that went wrong: I underestimated how often airlines change their rules. Delta changed checked bag fees from $35 to $45 halfway through the project. Spirit restructured personal item allowances. The data is only as good as the last verification date, and keeping 50 airlines current is more work than I budgeted for.

Where it is now

The tools have 50 airlines covered, grouped into 5 regions and 3 categories, with full checked bag fee estimation for US airlines. Each tool has its own page, each airline has its own detail page at /tools/airlines/[slug]/, and the whole thing is indexed in the Astro sitemap. Every page ships JSON-LD structured data (FAQPage, CollectionPage, BreadcrumbList, ItemList) so Google and AI search engines can pull answers out of it.

Traffic has been better than expected. The personal item checker in particular gets picked up by “will my bag fit on Ryanair” and “Spirit personal item size” searches, which turn out to be very high-intent queries. People who are about to pay a $75 bag fee are motivated to click.

What I would do differently

First, I would start with the full schema. The rewrite from a 3-field dimension object to a 20-field airline spec was avoidable. If I had spent an extra hour at the start looking at five airline policies side by side, I would have seen the variance and designed for it.

Second, I would build the verification workflow before the data file got big. Right now updating an airline means finding it in 4,000 lines of JSON, editing, updating lastVerified, and committing. A simple CLI that prompts me through the fields for one airline, validates against a schema, and writes the JSON would have saved hours. It is on the list.

Third, I would not have built two separate tools at first. Carry-on and personal item share almost all their logic. The split made sense for SEO because people search for them as distinct things, but the shared code is now duplicated across two index pages and two detail templates. A single generic “bag fit” tool with two SEO landing pages pointing at it would have been cleaner.

Still, both tools work, both are free, and nobody has emailed me to say I gave them wrong dimensions. For a side project built from a grudge at a Ryanair gate, I will take it.

Stay in the loop

Get notified when I publish new posts. No spam, unsubscribe anytime.