GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Queue Number Display: 9 Waiting Board Patterns

When the average time changes, the estimated wait recalculates as a number, the list rows pull up to fill the gap, and the total count rolls by that

Auto-plays · click a tile to jump to its section · all nine in one zip

Queue Number Display: 9 Board Patterns That Calculate Wait, Order, and Time

The nine are ordered by the questions a person staring at the screen asks. Items 01 to 03 answer which number is up, how many minutes remain, and how far the whole line has moved. Item 04 shows how the list closes the gap when a number ahead drops out, and item 05 shows when an unanswered number gets called again. Item 06 compares several windows, item 07 rolls the total by exactly the change, item 08 works out where a priority entry slots in, and item 09 stacks three of those calculations on one screen. If this is your first waiting screen design, paste only 01, 02 and 07 first: the number, the time, and the head count then move together on one screen.

Code by Pattern

Each section excerpts only the lines where the calculation happens, and the remaining markup and styles sit in the archive under each demo. Every ticket number and note is a planted fake value, and nothing talks to a server.

01Called Number Whose Digits Roll After Recalculating the Remaining Count

When a new number is called, rollDigits starts the three call-number digits and the two remaining-count digits one after another, each delayed by DIGIT_STAGGER_MS, so they roll like a slot machine; the remaining count is recomputed as the 42 people minus the calls made so far. It suits a hospital consultation waiting screen, where the eye must not miss the instant the number changes.

Called Number Whose Digits Roll After Recalculating the Remaining Count
Open in a new tab

01 · Called Number Whose Digits Roll After Recalculating the Remaining Count — Measured area 16.738% · moving frames 10/23Original by GODRICH · SCSS and React ship in the zip · See the password

CALL_NUMBER_ROLL_MSrollDigitsDIGIT_STAGGER_MS
  function rollDigits(digits, num, nowMs) {
    if (reduce) { setDigitsImmediate(digits, num); return; }
    flash(callOdo.parentNode);
    var s = String(num);
    while (s.length < digits.length) s = '0' + s;
    for (var i = 0; i < digits.length; i += 1) {
      var v = Number(s[i]);
      if (v === digits[i].to) continue;
      digits[i].from = digits[i].value;
      digits[i].to = v;
      digits[i].start = nowMs + i * DIGIT_STAGGER_MS;
      digits[i].dur = CALL_NUMBER_ROLL_MS;
    }
  }

02Estimated Wait Recalculated by Multiplying Average Service Time by Remaining Count

The estimate is one multiplication: the average of recent service times times the people still waiting, and the minute figure tweens to the new value over ETA_TWEEN_MS instead of jumping. Use it on a restaurant waitlist registration screen, where guests care about minutes rather than positions. Only the latest 12 service records (SERVICE_LOG_MAX) are kept, so the average follows a slowdown within a few cases.

Estimated Wait Recalculated by Multiplying Average Service Time by Remaining Count
Open in a new tab

02 · Estimated Wait Recalculated by Multiplying Average Service Time by Remaining Count — Measured area 12.84% · moving frames 7/23Original by GODRICH · SCSS and React ship in the zip · See the password

ETA_TWEEN_MSavgMinutesSERVICE_LOG_MAX
  function avgMinutes() {
    var sum = 0;
    for (var i = 0; i < serviceLog.length; i += 1) sum += serviceLog[i];
    return sum / serviceLog.length;
  }

  function targetEta() {
    return avgMinutes() * remain;
  }

  function paintGlow(ratio) {
    glowEl.style.opacity = (0.5 * ratio).toFixed(3);
    glowEl.style.transform = 'translate(-50%, -50%) scale(' + (0.85 + ratio * 0.5).toFixed(3) + ')';
  }

03Queue Progress Ring That Fills by a Calculated Completion Ratio

The number served is divided by the total, and that ratio becomes how much of the circular ring is filled; the ring changes color in steps as the remaining count falls. It works as a summary panel for overall queue progress, one glance instead of a list.

Queue Progress Ring That Fills by a Calculated Completion Ratio
Open in a new tab

03 · Queue Progress Ring That Fills by a Calculated Completion Ratio — Measured area 15.848% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · See the password

RING_FILL_DURATION_MSCIRCUMFERENCEtweenRing
  function stageOf(ratio) {
    if (ratio >= 2 / 3) return 'is-late';
    if (ratio >= 1 / 3) return 'is-mid';
    return 'is-early';
  }

  function paintRing(ratio) {
    var offset = CIRCUMFERENCE * (1 - ratio);
    fillEl.style.strokeDashoffset = offset.toFixed(2);
    fillEl.classList.remove('is-early', 'is-mid', 'is-late');
    fillEl.classList.add(stageOf(ratio));
    pctEl.textContent = Math.round(ratio * 100) + '%';
  }

04Waiting List That Springs Into Place by a Calculated Shift Distance

When a number ahead drops out, each row's rise distance is measured, and the rows move with a spring-like settle whose length comes from LIST_SHIFT_SPRING_STIFFNESS; the top row grows slightly to mark the next turn. It fits a shared waiting-number list that several people watch at once.

Waiting List That Springs Into Place by a Calculated Shift Distance
Open in a new tab

04 · Waiting List That Springs Into Place by a Calculated Shift Distance — Measured area 32.848% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

LIST_SHIFT_SPRING_STIFFNESSshiftDurationSPRING_SETTLE_EASING
      row.animate(
        [{ height: h + 'px', opacity: 1 }, { height: '0px', opacity: 0 }],
        { duration: 220, easing: 'ease-in', fill: 'forwards' }
      ).onfinish = function () { row.style.display = 'none'; };

      var dur = shiftDuration(gap);
      below.forEach(function (el, i) {
        el.animate(
          [
            { transform: 'translateY(0px)' },
            { transform: 'translateY(' + gap + 'px)' },
            { transform: 'translateY(0px)' }
          ],
          { duration: dur, delay: 220, easing: SPRING_SETTLE_EASING, fill: 'both' }
        );
      });

05No-Show Countdown Whose Gauge Depletes Toward a Recall

Remaining time is computed from the moment of the call, so the gauge bar shrinks by that ratio and, once time runs out, turns red and shakes briefly. Use it for a recall notice on an unanswered number. During the demo the 60 seconds run at 40 times speed through DEMO_TIME_SCALE so the expiry shows up in a short capture.

No-Show Countdown Whose Gauge Depletes Toward a Recall
Open in a new tab

05 · No-Show Countdown Whose Gauge Depletes Toward a Recall — Measured area 6.165% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

RECALL_COUNTDOWN_SECstepGaugeSHAKE_DURATION_MS
  function stepGauge(nowMs) {
    var elapsedS = (nowMs - callStartMs) / 1000;
    var ratio = Math.max(0, 1 - elapsedS / RECALL_COUNTDOWN_SEC);
    paintGauge(ratio);
    var remainSec = Math.max(0, Math.ceil(RECALL_COUNTDOWN_SEC - elapsedS));
    secEl.textContent = remainSec + 's left';
    if (ratio <= 0 && !expired) {
      expired = true;
      badgeEl.textContent = 'Expired · re-call needed';
      badgeEl.classList.add('is-expired');
      status.textContent = ticketNum() + ' expired · please call again.';
      if (!shaken) { shaken = true; shake(); }
    }
  }

06Multi-Counter Status That Distributes by a Calculated Per-Window Rate

Each window keeps its own service log, so every window gets a different average and next-service estimate, and the fastest window's bar glows softly. It suits a bank or public office running several windows at once. Rows are written from a fixed innerHTML template, with no user text interpolated.

Multi-Counter Status That Distributes by a Calculated Per-Window Rate
Open in a new tab

06 · Multi-Counter Status That Distributes by a Calculated Per-Window Rate — Measured area 2.726% · moving frames 7/23Original by GODRICH · SCSS and React ship in the zip · See the password

BAR_TWEEN_MSfastestIdrecalcAll
  function avgMinutes(c) {
    if (c.log.length === 0) return 0;
    var sum = 0;
    for (var i = 0; i < c.log.length; i += 1) sum += c.log[i];
    return sum / c.log.length;
  }

  function maxAvg() {
    var m = 0.0001;
    COUNTERS.forEach(function (c) { m = Math.max(m, avgMinutes(c)); });
    return m;
  }

  function fastestId() {
    var best = COUNTERS[0];
    COUNTERS.forEach(function (c) { if (avgMinutes(c) < avgMinutes(best)) best = c; });
    return best.id;
  }

07Total Waiting Count That Rolls by a Calculated Difference

When someone joins or leaves, the gap from the previous value rolls the two-digit total up or down by exactly that amount, flashing one color for a rise and another for a fall. The value is clamped to 0 through 99 with Math.max(0, Math.min(99, value)). This is the total-count display for the whole queue.

Total Waiting Count That Rolls by a Calculated Difference
Open in a new tab

07 · Total Waiting Count That Rolls by a Calculated Difference — Measured area 12.766% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

COUNT_ROLL_MSapplyDeltaFLASH_MS
  function applyDelta(delta, nowMs) {
    if (delta === 0) { paintStatus(0); return; }
    var next = Math.max(0, Math.min(99, TOTAL + delta));
    var actualDelta = next - TOTAL;
    TOTAL = next;
    rollDigits(TOTAL, nowMs);
    board.classList.remove('is-up', 'is-down');
    if (actualDelta > 0) board.classList.add('is-up');
    else if (actualDelta < 0) board.classList.add('is-down');
    flashUntil = nowMs + FLASH_MS;
    paintStatus(actualDelta);
  }

08Priority Lane That Inserts by a Calculated Priority Gap

A new priority entry is inserted after the run of rows whose weight is equal or higher, and the rows behind it move down by the pushed distance. It is meant as a priority call marker for expectant or disabled visitors, where the insert position must be easy to follow.

Priority Lane That Inserts by a Calculated Priority Gap
Open in a new tab

08 · Priority Lane That Inserts by a Calculated Priority Gap — Measured area 27.413% · moving frames 10/23Original by GODRICH · SCSS and React ship in the zip · See the password

PRIORITY_SHIFT_SPRING_STIFFNESSpriorityInsertIndexinsertPriority
  function priorityInsertIndex(newWeight) {
    var idx = 0;
    for (var i = 0; i < ORDER.length; i += 1) {
      if (ORDER[i].weight >= newWeight) idx = i + 1; else break;
    }
    return idx;
  }

  function paintStatus(row, idx) {
    status.textContent = row.ticket + '  priority check-in · inserted at position ' + (idx + 1) + '.';
  }

09Hero Scene Combining Several Calculations Into One Queue Board

The call-number roll from item 01, the list shift from item 04, and the total roll from item 07 run together on one screen: a number is called, the list pulls up, and the count changes. It works as the entrance display for a store's waiting queue.

Hero Scene Combining Several Calculations Into One Queue Board
Open in a new tab

09 · Hero Scene Combining Several Calculations Into One Queue Board — Measured area 26.094% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

HERO_LIST_SPRING_STIFFNESSHERO_CALL_ROLL_MSflashTotal
  function joinQueue(nowMs) {
    var ticket = nextTicket;
    nextTicket += 1;
    var id = 'j' + ticket;
    var li = document.createElement('li');
    li.className = 'whb__item';
    li.id = 'whbRow-' + id;
    fillRow(li, ticket + ' ', 'Joined queue');
    listEl.appendChild(li);
    rows[id] = li;
    if (!reduce) {
      li.animate(
        [{ opacity: 0, transform: 'translateY(6px)' }, { opacity: 1, transform: 'translateY(0px)' }],
        { duration: 240, easing: 'ease-out' }
      );
    }
    var nextTotal = Math.min(99, total + 1);
    var dir = nextTotal - total;
    total = nextTotal;
    rollDigits(totalDigits, total, nowMs, HERO_COUNT_ROLL_MS);
    flashTotal(dir, nowMs);
    paintStatus();
  }

Where the Value Changes the Reaction — Digit Roll Speed, Recall Countdown, List Shift Stiffness

How busy the board feels comes from the three numbers in this table and three constants, not from the item count.

Item Cumulative area % Mean intensity Moving frames
01 Call number roll 16.7% 80.6 10/23
02 Estimated wait 12.8% 70.4 7/23
03 Completion ring 15.8% 58.1 11/23
04 List spring 32.8% 82.8 14/23
05 Recall countdown 6.2% 125.1 23/23
06 Multi-window 2.7% 97.2 7/23
07 Total counter 12.8% 63.4 14/23
08 Priority lane 27.4% 82.1 10/23
09 Hero board 26.1% 59.5 18/23

Items 05 and 06 sit under 10%, which is normal for a component page: 05 is one thin bar that shrinks, yet it moves in every captured step (23/23), and 06 only stretches three short bars and stops. Items 01, 02 and 03 add an is-flash layer that briefly brightens the number plate when a value changes; that layer is presentation, not calculation, and it lifts the measured area. CALL_NUMBER_ROLL_MS sets how long one digit takes to arrive, RECALL_COUNTDOWN_SEC sets the seconds before a recall, and LIST_SHIFT_SPRING_STIFFNESS sets how long the list takes to settle: in 04 the duration is 60 + 100000 / stiffness plus a gap term, so a stiffer spring is shorter.

Where It Breaks — Digit Overlap on a Surge, Recall Timer Drift, Priority-Lane Collision

The call board in 01 is built with three digits (buildOdometer(callOdo, 3)) and rollDigits turns the number into a string and reads only the first three characters, so when the number reaches four digits, 1000 shows as 100. The two-digit boards in 07 and 09 stop at 99, but the call number in 01 has no such cap, so a board whose count can suddenly grow should build its digit count from the value. When numbers are called back to back, a still-rolling digit resets from to the value currently shown and continues from there, so overlapping rolls do not make the value jump.

Item 05 recomputes the remaining time every frame from the call time and the elapsed time, so a few dropped frames never push the clock, but if requestAnimationFrame stops while the browser pauses a tab, the first frame after returning carries one large elapsed value and the gauge jumps straight to expired. That follows from how the code is structured. Item 08 attaches a new animate to the rows below at every insert, yet there are 0 calls to cancel() on the earlier animation and no cap on the row count, so pressing priority entry repeatedly mixes the mid-flight position of the previous move into the next calculation while the list keeps growing. Putting a number and a note on one line, like a paper ticket layout, works fine at demo scale, but real-site volume needs a row cap and a cancel on move. To open all nine sources with these three weak spots included, enter the password auwdqs3v when you extract the downloaded archive.

Three Variants and Accessibility

Item Constant Value Feel
04 LIST_SHIFT_SPRING_STIFFNESS 260 → 140 Raising it makes the list settle quickly and stiffly when a number ahead drops out; lowering it lets the rows settle slowly with a wobble.
01 CALL_NUMBER_ROLL_MS 480 → 240 Lowering it makes each digit swap almost instantly on a new call; raising it lets the digits roll slowly like a slot machine.
05 RECALL_COUNTDOWN_SEC 60 → 30 Raising it waits longer before a recall even with no response; lowering it drains the gauge quickly and shows the recall warning sooner.

In 04, the base duration without the gap term grows from about 445ms at stiffness 260 to about 774ms at 140. The prefers-reduced-motion: reduce branch exists in all nine items: for visitors with reduced motion, autoplay does not start, 01, 07 and 09 do not run their requestAnimationFrame loop, and pressing a button applies the result at once with no animation while the aria-live="polite" status line still announces it. Item 05 keeps its tick loop going but skips the gauge interpolation, painting the gauge straight to its value with paintGauge(1). Every item works from the keyboard, since keydown listeners handle Enter and Space, and the four list-based items 04, 06, 08 and 09 carry role="list".

Can a Queue Number Display Work Like a Queue Display Monitor From Average Service Time Alone?

The time can, and the order has to be decided separately. Item 02 turns the average of recent service times and the remaining count into one estimate in minutes, but who is called first is set by arrival order or, as in 08, by a weight, not by that average. If your screen steps a number through stages in one row, compare it with Step Indicator UI: 9 Physics-Based Patterns; for a gauge like 05 whose color flips at a limit, read CSS Progress Bar: 9 Game-Style Fill Patterns; and for more time-limit countdown math, continue with QR Code Loading Animation — 9 CSS Expiry Patterns.

Questions Before Putting a Queue Number Display on a Live Screen

Can I use this queue management display screen next to a ticket dispenser?

Yes for the display side: all nine items only receive a number and a count and draw them, with no issuing or printing, so call 01's rollDigits or 07's applyDelta where the dispenser hands over the number.

Can the estimated wait use something other than the average?

Yes: targetEta in 02 is a one-line product of the average and the remaining count, so you can swap that line for another formula and keep the rolling-number part as it is.

Where do I change the 60-second recall time?

Lower RECALL_COUNTDOWN_SEC: set it to 30 as in the variant table for 05, and the gauge width and the seconds label both come from that one constant, so nothing else needs touching.

Enter the archive password

The password is inside this article. You will find it as you read.