Oops! Sorry!!


This site doesn't support Internet Explorer. Please use a modern browser like Chrome, Firefox or Edge.

Padzilly — Net Income Screen · Year Dropdown Fix
Padzilly · Pre-Qual · Self-Employed Net Income Screen

Fixing the year dropdown

Two bugs, same field: the list covers the question above it, and it goes back roughly 20 years further than it needs to.

What's happening:
  1. The dropdown isn't anchored to its own field. In the second screenshot it renders starting near the top of the screen, covering the heading and the first year's already-entered value — so picking the second year means losing sight of what you picked for the first. That's a positioning bug, not a content problem: the list should open directly below the button that triggered it and go no further than it needs to.
  2. The list has roughly 23 years in it (2004–2026), when this question only ever needs the current year plus the previous three. A shorter list is also most of the fix for problem 1 — four items can't cover much of anything.

Before / after

Current
2026
2025
2024
2023
2022
2021
2020
2019
2018
2017
2016
2015
2014
Get Pre-Qualified×
What are the 2 most recent year's net income…

The list renders from the top of the modal, hiding the heading and the first year field underneath it.

Fixed

Four years only. Opens directly under its own field — the heading and the other year field stay visible.

Markup

HTML — one year field
<div class="pzn-field">
  <label id="pzn-y1-label">Year</label>

  <button type="button" class="pzn-yearbtn"
    aria-haspopup="listbox" aria-expanded="false"
    aria-labelledby="pzn-y1-label" data-year-trigger="1">
    <span data-year-display="1" class="ph">Year</span>
    <svg class="chev" viewBox="0 0 10 10" fill="none">
      <path d="M2 3.5L5 6.5L8 3.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
    </svg>
  </button>

  <!-- populated by JS with exactly 4 buttons: this year + previous 3 -->
  <div class="pzn-yearlist" role="listbox" data-year-list="1" aria-labelledby="pzn-y1-label"></div>
</div>
CSS — the key rule
/* Anchored to its own field via position:relative on the parent
   .pzn-field, not fixed to the page or the modal. This is what stops
   it from covering the heading — it can only render where its own
   field is, and only as tall as its own content (4 items, capped). */
.pzn-field { position: relative; }

.pzn-yearlist {
  position: absolute;
  top: calc(100% + 3px);
  left: 0; right: 0;
  z-index: 5;
  background: #fff;
  border: 1px solid #C9CFE0;
  border-radius: 4px;
  box-shadow: 0 6px 16px rgba(28,36,64,.14);
  max-height: 160px;   /* generous headroom; 4 items never fill it */
  overflow-y: auto;
  padding: 4px;
  display: none;
}
.pzn-yearlist.is-open { display: block; }

.pzn-yearlist button {
  width: 100%; text-align: left; background: none; border: 0;
  border-radius: 3px; font-family: inherit; font-size: 12.5px;
  color: #4A5061; padding: 6px 8px; cursor: pointer;
}
.pzn-yearlist button:hover { background: #F4F6FA; }
.pzn-yearlist button[aria-selected="true"] { background: #EDF1FB; color: #233C90; font-weight: 700; }
JS — year list, dynamically computed
// Always the current year plus the previous 3 — recalculated at
// render time, never a hardcoded array, so it stays correct every
// January without a content update.
function getYearOptions() {
  const now = new Date().getFullYear();
  return [now, now - 1, now - 2, now - 3];
}

function buildYearList(listEl, selectedYear) {
  listEl.innerHTML = '';
  getYearOptions().forEach(year => {
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.textContent = year;
    btn.setAttribute('role', 'option');
    btn.setAttribute('aria-selected', String(year === selectedYear));
    listEl.appendChild(btn);
  });
}

// Wire each trigger/list pair
document.querySelectorAll('[data-year-trigger]').forEach(trigger => {
  const id = trigger.dataset.yearTrigger;
  const list = document.querySelector(`[data-year-list="${id}"]`);
  const display = document.querySelector(`[data-year-display="${id}"]`);

  buildYearList(list, null);

  trigger.addEventListener('click', () => {
    // close any other open list first
    document.querySelectorAll('.pzn-yearlist.is-open').forEach(l => { if (l !== list) l.classList.remove('is-open'); });
    const open = list.classList.toggle('is-open');
    trigger.setAttribute('aria-expanded', String(open));
  });

  list.addEventListener('click', e => {
    if (e.target.tagName !== 'BUTTON') return;
    display.textContent = e.target.textContent;
    display.classList.remove('ph');
    buildYearList(list, Number(e.target.textContent));
    list.classList.remove('is-open');
    trigger.setAttribute('aria-expanded', 'false');
    // onYearSelect(id, e.target.textContent);
  });
});

// click-away closes any open list
document.addEventListener('click', e => {
  if (e.target.closest('.pzn-field')) return;
  document.querySelectorAll('.pzn-yearlist.is-open').forEach(l => l.classList.remove('is-open'));
  document.querySelectorAll('[data-year-trigger]').forEach(t => t.setAttribute('aria-expanded', 'false'));
});

What changed

ItemCurrentFixed
Year options~23 years (2004–2026)4 years — current + previous 3, computed at render time
List positionRenders near the top of the modal regardless of which field opened itAnchored directly below its own field via position:absolute inside a position:relative field wrapper
List heightTall enough to cover the heading and the first year fieldCapped at 160px; 4 items never come close to filling it
Two lists open at onceNot addressed — could presumably overlap each otherOpening one closes the other automatically
Everything elseQuestion text, help link, Amount fields, Back/Next — unchanged

Notes for the developer

Notes
  • If the current field is a native <select>, this replaces it with a custom button + listbox pattern, since native selects can't be capped in height or reliably positioned across browsers. The trade-off is slightly more JS to wire up, in exchange for control over both bugs at once.
  • The year list is computed, not hardcodedgetYearOptions() always returns the current year and the three before it, so it never needs a content update again.
  • Selecting a year doesn't clear or reset the other field's selection — each field tracks its own state independently, same as the current behavior.