Skip to content
Hi, I'm Jun
  • User Experience Design
    • Human-Centered AI
    • Business

Expedia Group

Building VRBO’s property detail page to surface gaps in the design system

Role
Design Intern
Timeline
10 Weeks
Team
Cross-Functional
Platforms
  • Figma
  • Cursor
  • Claude

Background

The Why

EGDS 2 is Expedia Group’s design system: a machine-readable spec that generates a code library for each brand and platform. The Prototype Kit is its React library for building EGDS screens. I rebuilt VRBO’s property detail page in it.

    • EGDS 2 spec

      Machine-readable. The source of truth.

    • Figma UI Kit

      What designers draw with.

    • AI Factory

      Generates the libraries.

    • Component code libraries

      One per brand, per platform.

    • Production

      Web, iOS, Android, server-driven.

    • Prototype Kit

      React. What I built in.

No edge returns to the spec.

Every arrow points away from the spec and none comes back, so a gap found while building has no route home. My job for the summer was to be that return trip.

  1. Phase 1

    Prototyping

    • 14 flows
    • gap log
  2. Phase 2

    Opportunity Definition

    • Verification grading
    • proposal set
  3. Phase 3

    Exploration

    • Iteration
    • revision rounds
  4. Phase 4

    Implementation

    • 8 enhancements
    • handoff across three surfaces

Research

What the Build Found

Fourteen flows came out of one page, and building them was the research. Every workaround went into a log.

Product Detail Page About Photo Tour Reviews Rules Cancellation Booking Summary Share Save Map Lightbox Reviews-Sticky Contact Host

A central Product Detail Page hub connects to nine flows: About, Photo Tour, Reviews, Rules, Cancellation, Booking, Summary, Share and Save. Four more open inside another screen rather than off the hub — Map and then Contact Host below About, Lightbox below Photo Tour, and Reviews-Sticky below Reviews.

Four of the fourteen open only from inside another screen.
Review my source of truth file (composed of The Rivalry & Evolution file) Create a high-level task list for Cursor to execute (provide context, references, etc) via Claude Qualitative testing checklist + other designer feedback Tyler reviews + /review-feedback automation Prompt, refine, bugfix Cursor notes down in feedback.md any inconsistencies, struggles, gaps in design system Send/Update PR until merge into main

A source-of-truth file feeds a high-level task list written in Claude, which fans out to a qualitative testing checklist and to Tyler's PR reviews; both feed prompt, refine, bugfix, then send and update the PR until it merges to main, which loops back to the task list. Off to the side, Cursor writes every inconsistency, struggle and design-system gap into feedback.md.

Every gap the build hit is written to feedback.md in the same pass.
  • EGDS first — anything else is a finding.
  • No hardcoding; stand-ins are recorded.
  • Don't hack around a recorded gap. The gap is the deliverable.

Four entries, on the screens that produced them.

Three VRBO property-detail frames — reviews, house rules, and the map with filter pills
  1. Inline row. The reference drew a rotated pile.
  2. Filter row cut off mid-word. Five layout attempts.
  3. Label leading, value trailing. Nothing in the kit.
  4. Frosted pills at 20%, over whatever the basemap does.

Translation

From Finding to Proposal

Running notes conflate I couldn’t find this with this doesn’t exist. I was told much of what I’d flagged already existed, so every claim was graded against the spec.

How every claim was graded

  1. Grade A: It exists — the claim was wrong
  2. Grade B: It exists under another name
  3. Grade C: It partially exists
  4. Grade D: Genuinely absent, after searching aliases
Four things the log got wrong
FindingWhat the log claimedWhat re-verification found
Grade A. Bottom sheetNo EGDS bottom-sheet variant at allegdsBottomSheet was in the core spec throughout
Grade B. Tonal buttonMissingegdsButtonExperimentalTonal — a separate experimental set
Grade C. Select and removable pillsMissingBoth exist. Only the padding diverged
Brand foundationsVRBO overrides one corner-radius stepIt overrides four of five — 8px is l in base EGDS, m in VRBO
Cursor ran the audit; I directed it and checked every entry.
  • 18

    claims graded against the spec, the kit and the built tokens

  • 1

    graded A — it existed, and my claim was wrong

  • 10

    graded D — genuinely absent, after searching aliases

Two entries, at the two ends of the scale.

  • Grade D. OverlayFilterPillGenuinely absent, after searching aliases

    What changed
    Pills over the map carry a background instead of adapting to it.
    Why
    White labels measure 2.76:1 on the selected fill, and nothing in the kit brings its own surface.
    Proposed spec
    Select Pill, Button Overlay, 44px. Frost 20% to 55%, plus a solid fallback.
    Open question
    Frosted or solid as the default.

    there’s no EGDS component equivalent for this … and the design does not meet contrast ratio requirementsFaith, map screen

  • Grade A. BottomSheetIt exists — the claim was wrong

    What changed
    A bottom sheet was hand-rolled for gallery and house rules.
    Why
    The log recorded EGDS as having none, which is what justified building one.
    Proposed spec
    Withdrawn — it was in the core spec, so the entry became four changes instead.
    Open question
    Whether the keyboard defect was ever filed.

    Told in phase 2 that a lot of these do exist in the kit already.

Four corrections did the most work. The first I argued against.

  1. This file was purely us looking at foundation applications… we didn't blow out detailed designs with real content

    Andrew Rutkowski · source-of-truth correction

    What changed

    I’d read a foundations exploration as a content spec, and defended it.

    What it became

    Reversed — 176 lines of DetailsPage.tsx.

  2. Update to stacked 1:1 images, propose as component need

    Faith · reviews screen

    What changed

    Overlapping was impossible; inline shipped, the pile became the proposal.

    What it became

    ImageStack — layout variant.

    The kit's single inline row of photo tiles, beside the proposed stacked arrangement
  3. Flag component need for an overlay filter pill

    Faith · map screen

    What changed

    2.76:1 on the selected fill; 1.06–1.54:1 on the basemap. Frost 20% to 55%, plus solid.

    What it became

    OverlayFilterPill — Select Pill, Button Overlay, 44px.

    Filter pills over the property map, crossing white land, blue water and green park tiles
    1. Over land — basemap bands run 1.06–1.54:1.
    2. Same pill, water underneath.
  4. Check whether some could be composed from what's already in the library, as variants instead of new builds

    Amy D · two revision rounds

    What changed

    Items scoped as new were re-checked against existing anatomy.

    What it became

    Five of the eight became revisions, not additions.

Exploration

What It Took to Get There

Not everything took one attempt.

  1. Attempt 1

    Dated rows with radio-style dots

    Nothing was selectable, and a refund state is an interval, not a date.

  2. Attempt 2

    A hand-built segmented rail

    Read correctly, but composed a rail for a gap EGDS does not fill.

  3. Attempt 3

    Period rows on native DetailRows

    Native throughout, and modelled no temporal state at all.

  4. Attempt 4

    StatusTimeline, followed literally

    Ranges, cutoff times, a summary — every fact already stated further down the page.

  5. Attempt 5

    Cut back to the rail alone

    Answers one question: which refund window is in effect now.

Round one

  • TodayFull refund
  • Jun 6Partial refund
  • Jun 13No refund
  • Check-in

Round fiveCurrent = middle window

Refund status

  • Full refundEnded
  • Partial refund8 days leftNow
  • Non-refundableUpcoming
Rounds one and five. Iteration three declared the gap closed; four reopened it.

Three pills in a 390px frame took three.

  1. Attempt 1

    Horizontal-scroll carousel

    Chopped the third pill mid-word.

    Described, not shown

  2. Attempt 2

    Let the row wrap

    Fixed the cutoff and pushed the third pill onto a second line.

    Described, not shown

  3. Attempt 3

    Nowrap, content-sized pills that refuse to shrink

    Inline padding 4× down to 3×, gap 2×, trailing inset trimmed to 8px.

24leading inset354three pills and their gaps8trailing inset386inside a 390px frame

Four pixels of slack. Attempt three ships as the overflow variant.

Contribution

Three New Components

Eight enhancements came out of the audit. Three the kit could not build from what it had.

StickyFilterBar

New set
The bar inline with the content, above a ratings summary and a row of filter pills
State

In place

Composed ofEGDS Tabs + PopoverSheet + ActionList. No single ancestor — the binding is the proposal.

Pins under the header, collapsing the pills that no longer fit into a menu.

OpenWhether the stuck container gets a neutral token of its own.

Toolbar

Improvement
Toolbar over a Lake Como property header, collapsed to a title and a back control

Composed ofEGDS Toolbar — an additive sub-toolbar slot, not a new component.

A sub-toolbar slot holding the property title once the header scrolls away.

The collapse is driven by a target ref (an IntersectionObserver), not by a scroll offset.

OpenHeight is a constant, not a bound token — Figma and code carry 56 separately.

StatusTimeline

New set
Refund status with Full refund current, 17 days left and marked Now; Partial refund and Non-refundable both Upcoming
State

First window

Composed ofPorted from EGDS 1, absent from EGDS 2. Disclosure is Card + Divider + Icon — no expando primitive exists.

The refund schedule, after five rounds cut it back to the rail alone.

OpenNot localised, and whether the urgency styling is tokenised or only copy is unsettled.

Contribution

Five Variants

Five change components the library already had, where Amy’s review pushed them.

1 of 5

ImageStack

Variant
The kit's single inline row of photo tiles, beside the proposed stacked arrangement

Composed ofKit-local, not in EGDS. Promotion into the system is the prerequisite.

Review photos shipped as an inline row; the overlapping pile stayed a proposal.

OpenProposed and never drawn — no tile in the file carries a rotation.

2 of 5

BottomSheet

Variant
Before and after — a filter sheet with a toolbar header and close button, beside the same sheet led by a drag handle

Composed ofEGDS Bottom Sheet — the existing header prop, with handle added as a new enum value.

Four changes to the modal side sheet, one a keyboard defect every consumer inherits.

The other three: contentHeight sizes the sheet to its own content up to a maxHeight instead of the viewport; the scrim gains a frosted appearance with a solid fallback where blur is unavailable; and the focus behaviour below is corrected.

The defect nobody filed — keyboard behaviour
CurrentCorrected
Focus stays on the control that opened it, behind the scrimInitial focus moves into the sheet container
Tab escapes into the page behind the scrimTab is trapped, wrapping first to last
Escape does nothingEscape closes it, through the same animated dismiss
A plain container, to assistive technologyrole="dialog" with aria-modal="true"

Open decisionHandle fill token

White on the handle's marketing-token fill measures under 3:1.

  • Keep it — decorative and aria-hidden, so the threshold does not apply, and it matches every grabber in the system.
  • Move to on-surface-variant, which clears 3:1.

Open. A grabber's fill is a system call, and changing it here alone would break step with the kit.

DecidedSide Sheet parity

Side Sheet shares this anatomy, so a new prop is normally offered to both.

  • Forward the handle, for parity.
  • Leave it off, and recommend an edge swipe instead.

Not forwarded. A horizontal grabber on a side-anchored sheet advertises a vertical drag that does nothing.

3 of 5

ListItem

Variant
The library's disclosure row, beside the proposed navigational row

Composed ofEGDS ExpandoListItem anatomy, with navigational semantics instead of disclosure.

House-rules rows, as a navigational item and as a disclosure that expands in place.

OpenWhether the navigational variant should accept a button at all.

4 of 5

DataRow

Variant
The library's action row, beside the proposed row with the value on the trailing edge

Composed ofEGDS ActionList / ActionListItem anatomy — leading label, trailing value.

Label leading, value trailing — a variant of ActionList, not a new primitive.

The label truncates on overflow; the value stays on the trailing edge on a single line. A divider separates rows within the list but not after the last row.

OpenWhether the value slot takes rich content or only a string.

5 of 5

OverlayFilterPill

Variant
Three basemaps compared — the current pill at 20% scrim, the proposed frost at 55%, and the solid fill with a hairline outline

Composed ofEGDS Select Pill, extended with the Button Overlay appearance at 44px.

Pills that carry their own background instead of adapting to the basemap.

One system, two appearances: both use the same chip anatomy and DS tokens. The frosted-strong version raises the scrim tint (--egds-frost-overlay-strong-tint) for legible white text; solid fills with surface plus an outline-variant hairline. Selected chips fill with primary.

  • Over #a8d8e8, 1.54:1 — fails the 4.5 minimum.

  • Over #c8d8e8, 1.45:1 — fails the 4.5 minimum.

  • Over #d8f8e8, 1.13:1 — fails the 4.5 minimum.

  • Over #f8f8f8, 1.06:1 — fails the 4.5 minimum.

1.06:1 to 1.54:1, depending only on where it lands.

Water, coast, park, road. White fails on all four.
  • 2.76:1 improved to 4.62:1

    White on the selected pill fill, before the change and after

  • 11.20:1

    The label over the frosted tint, on the proposed appearance

Open decisionFrosted or solid by default

Frost needs backdrop-filter, so solid ships either way — the question is which one is the default.

  • Frosted, extending the existing overlay-blur system rather than adding a second one.
  • Solid, with frost as the opt-in where the platform supports it.

Open, both specified. It reaches Badge, Button and IconButton too, so a pill proposal is the wrong place to settle it.

Final Product

High Fidelity

The page the eight came out of, running.

Validation

Parity and What Didn’t Ship

Shipping into a design system means three places must agree: UI Kit, spec, prototype kit.

Parity table mapping each bottom-sheet change across the Figma UI Kit, the spec and the prototype kit
Four changes across three surfaces. Two rows still open.

The through-line

  • Composed, not net-new primitives

    Each one binds existing EGDS parts — Tabs, PopoverSheet, ActionList, Card, Divider, Icon — into a behaviour the kit was missing, rather than adding fresh atoms.

  • Token-driven end to end

    Colour, spacing, radius, and type all resolve to the design-system variables, so every surface re-themes from one source with no hardcoded values.

  • State is the real surface area

    Value lives in the states — pinned vs at rest, expanded vs collapsed, near vs far — so each is proven across its full range, not just a happy path.

Outcome

What Shipped

What left the internship is uneven: prototypes finished, proposals specified, the last mile somebody else’s call.

  • 14

    VRBO PDP flows prototyped

  • 3

    new components

  • 5

    variants of components the library had

The deck calls all three net-new primitives. Toolbar is a slot on a component that already exists, so the handoff file counts it an improvement: two new sets, one improvement, five variants.

How much of the prototype is actually the design system

  • 93%Custom CSS that is layout and composition

    A floor — prop-level customisation never shows in a selector count.

  • 72%Style values resolving to design tokens

    Five hardcoded hex colours across the whole prototype.

  • 58%Running UI that is library code

    7,301 lines of EGDS against 5,313 written here.

  • 84%Component instances that are EGDS

    Text primitives stripped; 81% with icons out too.

No product metric: nobody measured one.

Three colleagues carried a piece further.

Additional contributions

  • Mobile travel search — a map of results above a list of package refinements, mid-load

    Faith

    Map content loading

    Pan, skeleton, results update — the kit couldn’t show it.

  • The /plan skill invoked in Cursor, checking branch and recent work before sequencing

    Matt

    The /plan skill

    Asks scope and starting point, then sequences the work.

  • A recorded EGDS Show conversation, paused on a Cursor prompt for the About this home screen

    Andrew Wade

    The EGDS Show

    Where the workflow actually reached other designers.

Waiting on others
Whether any proposal reached the EGDS backlog, whether the keyboard defect was ever filed, whether the prototype pull request was ever reviewed.
Found in my own file
The rotated ImageStack was proposed and never drawn — no tile in the file carries a rotation.

Looking Forward

What I Took Away

I joined as Expedia Group’s first design builder intern — a new role built around AI-assisted design and prototyping — and spent it on the design system team. EGDS 2 is deep infrastructure: it reaches nearly every product in the company, which makes it an unusual place to hand an intern real scope. What made it work was that the project was horizontal. I wasn’t designing one feature; I was testing the system through a product and reporting back.

Not everything I flagged was a system problem. Some of it was VRBO-specific and some of it was me not knowing the kit well enough, and sorting out which was which is not something I could have done on my own.

  1. Design-system work doesn't fit the usual case-study shape

    The problem came out of building, and what I handed off isn't a screen.

  2. You can't find gaps by reviewing a library

    They only appear when you try to build with it.

  3. The system belongs to whoever reuses it

    Other people's input isn't a review step at the end — it's how the thing gets decided.

Jun's Portfolio