East Blue
KeepReady
Countdown Component link

A live timer showing how long is left. Three layouts — boxed units, a single bar, or a compact pill — each with a running, nearly-over and finished state.

Keep — restructured exactly as the last review asked
All four DS Health traits pass. The previous component bundled four presentations into one set and mixed layout into its state setting. The promo card moved out to Countdown Promo, every unit became a Countdown - Unit instance, and the settings resolved into three that each do one job: Style is the layout, State is how much time is left, and Surface is what it sits on. A finished state exists for the first time.
In Context

Adds urgency to anything time-limited — a flash sale, a voucher about to lapse, a payment window closing. Per and One sit inline above a call to action; Pill docks inside a card as a small "ends in" badge.

Live Preview
Properties
Style
State
Surface
hasSeconds
hasMinutes
hasHours
hasDays
hasLeadingIcon
DS Health
Reusable
Pass
Three layouts cover every place a timer appears — a dashboard rail, an inline block above a button, a badge inside a card. Nothing about a particular campaign is baked in.
Self-contained
Pass
Carries its own surface, border, radius, spacing and all three states. The numbers come from Countdown - Unit, so the timer itself never draws type.
Consistent
Pass
All three styles assemble the same way: units named days / hrs / mins / secs with a colon-separator between each pair.
Composable
Pass
Built from Countdown - Unit, and slots into Countdown Promo, which picks whichever Surface contrasts with its own card.
Behavior
State iOS Android Figma Property Notes
Style=Per N/A N/A 329 × 50 One filled box per unit. The most prominent layout.
Style=One N/A N/A 237 × 44 All four units share a single bar.
Style=Pill N/A N/A 198 × 30 Compact badge with a leading icon slot. Uses the inline unit layout.
State=Default N/A N/A running Blue treatment. Plenty of time left.
State=Expiring N/A N/A warning Amber treatment. The threshold is a product decision, not a design one.
State=Expired N/A N/A terminal Recessive grey. The timer has run out and nothing more will change.
Surface=onLight N/A N/A bold fill For placing on a white page — the countdown supplies the colour.
Surface=onColor N/A N/A pale fill For placing on a coloured card — the card supplies the colour.
Issues
  • The promo card is its own component. The old set carried a full promo — header copy, a button and a close icon — alongside three timer layouts, as though they were siblings. That card is now Countdown Promo, and what remains here is only the timer. C1 · Layer Structure & Naming
  • Each setting does one job. State used to mix how much time was left with which layout was showing, and the Style values White and Blue were colour treatments rather than layouts. Now Style is the layout, State is the time remaining, and Surface is the backdrop. C2 · Variant & Property Naming
  • A finished state exists. There was no way to show a timer that had run out, so screens either kept displaying zeros as though still counting, or swapped the component for something else. State=Expired covers it in all three layouts and on both surfaces. C5 · Interaction State Coverage
  • Which units show is no longer a variant. no Days and Mins and Secs encoded the visible unit subset as separate versions, which multiplied the matrix for something that is really data. Both are gone. C2 · Variant & Property Naming
  • The pill's icon is a slot. It used to embed one fixed, feature-specific glyph. It is now ⤷ LeadingIcon, so any surface can supply its own. C6 · Asset & Icon Quality
  • All three layouts are assembled the same way. Per named its boxes by unit while One left its instances unnamed and Pill wrapped everything in generic Time Unit frames with the separator tucked inside each one. Every layout now reads days / hrs / mins / secs with colon-separator as a sibling between them. C1 · Layer Structure & Naming
  • The backdrop setting says what it means. It was Theme = Light | Dark, which reads as a system dark mode and would have been wired to one. It describes what the countdown sits on, so it is now Surface = onLight | onColor — and the values swapped, because the old names described the component's own colour rather than its backdrop. C2 · Variant & Property Naming
Recommendations
  • Define the timer's behaviour contract. Figma cannot express the parts that matter most: what interval it ticks on, when it flips to Expiring, what happens at zero, and whether it keeps running in the background. Document those in the Code tab so every consumer behaves the same way. Docs
  • Consider making the separator a component. The two-dot colon-separator is hand-built and repeated three times per version across all eighteen, with its colour set per state. It works and it is deliberate, but a component would keep the dots from drifting apart over time. Composition
  • Announce the time remaining, not each digit. v2.0.1: Applied — Accessibility carries both halves for each platform: the whole timer is exposed as one label rather than four numbers and four labels read separately, and updates go out on a polite interval rather than every second, which would talk over the user. A11y
  • Let the native component choose which units to show. v2.0.1: Applied, by a different mechanism than the recommendation proposed. Its premise — "all four units are always drawn, so a two-unit countdown means detaching the instance" — is no longer true: the panel carries hasDays, hasHours, hasMinutes and hasSeconds, and each switches a unit off in place. The mapping keeps them as four booleans rather than the array the recommendation asked for, so every one traces 1:1 to a Figma property for Code Connect. Property
Styles
Per
Properties
State
Surface
hasSeconds
hasMinutes
hasHours
hasDays
Properties
Style Per
State Default
Surface onColor
hasSeconds true
hasMinutes true
hasHours true
hasDays true
Versions 18
Colors
Surface #EEF2F9
Border #E5EBF4
#time #005CE5
#unit #005CE5
Separator #9BC5FD
Typography
#time Primary/Headlines/Segment
#unit Primary/Label/Light/Tiny
Layout
Height 50px
Width 329px
Radius 8px — on each unit box
Padding H 0px on the row · 8px inside each unit box
Padding V 0px on the row · 8px inside each unit box
Gap 16px
Alignment Center
Colors by State

The columns are the three states, not interaction states — a countdown is never pressed. Each cell gives the onColor value first and the onLight value after the slash where they differ. Values are Per and One; Pill runs its own warning step, text/color-text-warning-stronger, and carries no opacity override. Two colours are set by layer opacity rather than by the token: the unit label is text/color-text-inverse-weak at 80% on the brand surface, and drops to 70% in Expiring and Expired on the pale one. The 80% pairing still falls short of 4.5:1, and that is deliberate for now: it carries over from the Sticker Sheet design and is the next token up that moved the contrast closer without changing the palette. Raising it further is a design recommendation, not a defect.

Role Token DefaultExpiringExpired
Surface bg/color-bg-inverse-weaker · onLight bg/color-bg-brand · warning and info steps by state #EEF2F9 / #1972F9 #FCF0CA / #F7D96E #F6F9FD / #E5F1FF
Border border/color-border-weak · -warning · -disabled #E5EBF4 / – #EBB30A #C2CFE5
#time text/color-text-primary · -inverse · -warning-strongest on the brand fill, -warning-strong on the pale one · brand-border-primary-disabled #005CE5 / #FFFFFF #966F0B / #453408 #C2CFE5
#unit text/color-text-primary · -inverse-weak · -warning-strong · brand-border-primary-disabled #005CE5 / #F6F9FD @ 80% #966F0B #C2CFE5
Separator border/color-border-info · Pill and One lift it on the brand surface · bg/color-bg-warning · brand-border-primary-disabled #9BC5FD · One #69A6FC, Pill #FFFFFF on brand #CA970C #C2CFE5
Pill
Properties
State
Surface
hasSeconds
hasMinutes
hasHours
hasDays
hasLeadingIcon
Properties
Style Pill
State Default
Surface onColor
hasSeconds true
hasMinutes true
hasHours true
hasDays true
hasLeadingIcon true
LeadingIcon (slot) 6 icons · Task Delayed Small
Versions 18
Colors
Surface #EEF2F9
Border #E5EBF4
#time #005CE5
#unit #005CE5
Separator #9BC5FD
Typography
#time Primary/Label/Light/Base
#unit Primary/Label/Light/Base
Layout
Height 30px
Width 198px
Radius 99px
Padding H 8px left · 10px right
Padding V 4px top · 5px bottom
Gap Auto — the icon and the timer sit at opposite ends of 198px
Alignment Center
One
Properties
State
Surface
hasSeconds
hasMinutes
hasHours
hasDays
Properties
Style One
State Default
Surface onColor
hasSeconds true
hasMinutes true
hasHours true
hasDays true
Versions 18
Colors
Surface #EEF2F9
Border #E5EBF4
#time #005CE5
#unit #005CE5
Separator #9BC5FD
Typography
#time Primary/Headlines/Segment
#unit Primary/Label/Light/Tiny
Layout
Height 44px
Width 237px
Radius 6px
Padding H 16px
Padding V 4px top · 6px bottom
Gap 16px
Alignment Center
Installation Planned API

iOS — Swift Package Manager

.package(url: "https://github.com/AY-Org/eb-ds-ios", from: "1.0.0")

Android — Gradle (Kotlin DSL)

implementation("com.eastblue.ds:countdown:1.0.0")

Import

import EastBlueDS
import com.eastblue.ds.countdown.*

Planned API — the native library does not exist yet. The artifact is the Countdown family, not this component: Countdown, Unit and Promo all ship in <code>com.eastblue.ds:countdown</code> and import <code>com.eastblue.ds.countdown.*</code>.

Property Mapping

Nine properties, in the order the Figma property panel lists them. State is the one that is not a parameter: Figma exposes it as a variant because a designer has to pick one to see it, but the platform works it out from the deadline — which is also what the Usage Guidelines ask for. The four unit booleans are independent flags, not a list: switching off hasDays alone is a real combination.

Figma PropertySwiftUICompose
Style — Per, Pill, One style: EBCountdownStyle style: EBCountdownStyle
State — Default, Expiring, Expired not a parameter — computed from until against an expiring threshold not a parameter — computed the same way
Surface — onColor, onLight surface: EBCountdownSurface = .onColor surface: EBCountdownSurface = OnColor
⤷ LeadingIcon (slot) icon: Image? — Pill only icon: @Composable (() -> Unit)? — Pill only
hasSeconds — true, false hasSeconds: Bool = true hasSeconds: Boolean = true
hasMinutes — true, false hasMinutes: Bool = true hasMinutes: Boolean = true
hasHours — true, false hasHours: Bool = true hasHours: Boolean = true
hasDays — true, false hasDays: Bool = true hasDays: Boolean = true
hasLeadingIcon — true, false hasLeadingIcon: Bool = true — Pill only hasLeadingIcon: Boolean = true — Pill only
Usage Snippets Planned API
Per — one box per unit
EBCountdown(
    until: saleEnds,
    style: .per,
    surface: .onColor
)
EBCountdown(
    until = saleEnds,
    style = EBCountdownStyle.Per,
    surface = EBCountdownSurface.OnColor
)
Pill — compact, behind a leading icon
EBCountdown(
    until: saleEnds,
    style: .pill,
    surface: .onColor
)
EBCountdown(
    until = saleEnds,
    style = EBCountdownStyle.Pill,
    surface = EBCountdownSurface.OnColor
)
One — a single bar
EBCountdown(
    until: saleEnds,
    style: .one,
    surface: .onColor
)
EBCountdown(
    until = saleEnds,
    style = EBCountdownStyle.One,
    surface = EBCountdownSurface.OnColor
)
Accessibility
RequirementiOSAndroid
The whole timer is one label .accessibilityElement(children: .ignore) with a formatted duration Modifier.clearAndSetSemantics { contentDescription = … }
Updates are polite, not per-second Re-announce at meaningful thresholds, not on every tick LiveRegionMode.Polite on a throttled value
Expiring is not signalled by colour alone Include the urgency in the spoken label Same, via stateDescription
Expired is announced as finished "Offer ended" rather than a row of zeros Same
Usage Guidelines

Do

Derive State from the deadline so it flips to Expiring and Expired on its own.

Don't

Don't set State by hand — a timer that never changes state is worse than no timer.

Do

Pick Surface by what the countdown sits on: onLight for a white page, onColor for a coloured card.

Don't

Don't wire Surface to the system theme — it is about the backdrop, not about dark mode.

Do

Use Pill where space is tight and Per where the urgency is the point.

Don't

Don't put Per inside a card that already has a heading and a button; it will fight them.

Criteria Scorecard
ID Criterion Status Notes
C1 Layer Structure & Naming Ready All three layouts read days / hrs / mins / secs with colon-separator siblings. Every unit is an instance.
C2 Variant & Property Naming Ready Style, State and Surface each carry one meaning, with semantic values throughout.
C3 Token Coverage Ready Every colour is bound and confirmed by design — five roles across three states and two surfaces, including the four warning steps the Expiring state uses. Colour still reaches the units as instance overrides rather than through a property; that is the accepted mechanism, and the Design Recommendations carry the case for driving it by property instead.
C4 Native Mappability Ready Maps to a formatted duration view on both platforms. Units are not fixed at four — hasDays, hasHours, hasMinutes and hasSeconds switch each one independently. State is the one property that is not a parameter: the platform computes it from the deadline, which is what the guidelines ask for.
C5 Interaction State Coverage Not Applicable Display only — confirmed by design. Default, Expiring and Expired are time states, not interaction states.
C6 Asset & Icon Quality Ready The pill's icon is a slot holding a vector instance. No rasters anywhere.
Variants Inventory (18 total)

3 Style × 3 State × 2 Surface = 18 variants, complete with no gaps. The four unit booleans, hasLeadingIcon and the icon slot are content settings and do not multiply the set.

StyleSizeUnit layoutCount
Per329 × 50Stacked, one box per unit6
Pill198 × 30Inline, with a leading icon slot6
One237 × 44Stacked, in a shared bar6
View full Style × State × Surface breakdown (18 rows)
StyleStateSurfaceSizeNode
PerDefaultonColor329 × 505630:36049
PerDefaultonLight329 × 505630:36051
PerExpiringonColor329 × 505642:37087
PerExpiringonLight329 × 505642:37069
PerExpiredonColor329 × 505656:42337
PerExpiredonLight329 × 505656:42319
PillDefaultonColor198 × 305630:36053
PillDefaultonLight198 × 305630:36054
PillExpiringonColor198 × 305642:37367
PillExpiringonLight198 × 305642:37383
PillExpiredonColor198 × 305656:42383
PillExpiredonLight198 × 305656:42399
OneDefaultonColor237 × 445630:36048
OneDefaultonLight237 × 445630:36050
OneExpiringonColor237 × 445642:36616
OneExpiringonLight237 × 445642:36876
OneExpiredonColor237 × 445656:42355
OneExpiredonLight237 × 445656:42369
2.0.2 — September 2026Patch
Surface token corrected — node 5630:36052
The Surface was documented against the wrong token. #EEF2F9 was recorded as bg/color-bg-secondary on all three cards and in the colours table; it is bg/color-bg-inverse-weaker. The hex was always right — only the name was wrong, which is the worse of the two failures, because a developer implements the name. Caught while reviewing Select Item, whose badge is genuinely bg/color-bg-secondary at #E5F1FF: one token cannot be two values, and comparing the two pages is what surfaced it.
C3 resolved
2.0.1 — September 2026Patch
Style and Code tabs rebuilt, six properties documented — node 5630:36052
Six of the nine properties were undocumented. The page described Style, State and Surface; the Figma panel also carries hasSeconds, hasMinutes, hasHours, hasDays, hasLeadingIcon and the LeadingIcon slot. Nothing changed in Figma — they were always there. get_node_info cannot read property definitions, so a node-tree reading alone will keep missing them; this is the second component in the family where the property panel held more than the tree could show.
C2 resolved
Four of the eighteen variants rendered the wrong surface. .eb-preview-cd--onlight and .eb-preview-cd--expiring carry the same specificity, so the state rule won on source order and Surface was ignored whenever State was not Default. Expiring and Expired on the brand surface drew the pale onColor palette — #FCF0CA for #F7D96E, #F6F9FD for #E5F1FF — across all three styles.
Docs
The separator is a per-style decision, not a per-state one. On the brand surface Per keeps #9BC5FD, One lifts to #69A6FC and Pill goes white; at Expiring the Pill tracks its own text colour while Per and One use bg/color-bg-warning. The preview applied one rule to all three. Per's is the weakest of the three against its own fill.
C3 resolved
Expiring runs four warning steps, not one. text/color-text-warning-strongest for the number on the brand surface, -strong for it on the pale one and for the unit label on both, -stronger for the Pill on both, and bg/color-bg-warning for the Per and One separator. The page had a single value.
C3 resolved
Colour is set by layer opacity in two places, which the node tree does not report. get_node_info returns a fill's own opacity — which is how the 80% unit label on the brand surface was found — but not the layer's. The 70% on the Expiring and Expired unit label only appeared in the Figma Appearance panel, and the preview was resetting it to full strength.
C3 resolved
The 80% unit label on the brand fill is deliberate. It carries over from the Sticker Sheet design and is the next token up that moved the contrast closer to 4.5:1 without changing the palette. It still falls short, so raising it stays a design recommendation rather than a defect — and this is where Countdown - Unit's open recommendation about that label actually lands, since the unit paints no surface of its own.
C3 resolved
One card became three. Style is the driving property, so Per, Pill and One each get a card — in Figma's panel order, which runs Per, Pill, One rather than the Per, One, Pill the page had. The six content settings are controls on every card, with hasLeadingIcon on Pill alone.
Docs
Typography was three font specs, one of which was not type. "Label wording": "days · hrs · mins · secs" sat in the Typography section. The rows now name the styles the Unit instances resolve to: Primary/Headlines/Segment over Primary/Label/Light/Tiny for Per and One, Primary/Label/Light/Base for Pill.
C3 resolved
Layout had none of the seven keys, and two of its values were wrong. Per's unit box has 8px padding, not the 0 the page implied — the row has none, but the box the unit sits in does. Pill's gap is Auto across a fixed 198px, not a flat 8px; it resolves to 8 only at full content and would diverge the moment a unit is switched off. All twelve values are now read from the auto-layout panel rather than derived from bounding boxes.
Docs
Three preview geometry fixes. One rendered its units centred instead of 4px from the top; Pill had no vertical padding at all and its inline separator gap was 6px against Figma's 4, which made the row overflow its own width; and the card declared font-family: inherit, so it drew in the documentation font rather than Proxima Soft.
Docs
The install block pointed at coordinates that will never exist. gcash/east-blue-ios and com.gcash.eastblue:components:1.0.0, with no Import line. It now cites the Countdown family artifact com.eastblue.ds:countdown:1.0.0 and imports com.eastblue.ds.countdown.*, matching Countdown - Unit.
Docs
Property Mapping collapsed four properties into one and missed two. A single days / hrs / mins / secs row mapped to units: [TimeUnit] — a list cannot express "days off, hours on", which the four independent booleans can. hasLeadingIcon was absent, and the types read CountdownStyle rather than EBCountdownStyle. Nine rows now, in panel order.
C4 resolved
State is documented as something the platform works out, not something it is passed. Figma exposes it as a variant because a designer has to pick one to see it; natively it follows from until against an expiring threshold, which is what the Usage Guidelines already asked for. The third shape difference in this family, after hasWeek6 → weeks: Int and UnitLabel → EBTimeUnit.
C4 resolved
Usage Snippets were keyed to placement, not to a property. They read "Inline above a call to action", "As a badge inside a card" and "On a coloured card". One per Style now, built from the same definition as the spec cards and the live preview.
Docs
The variants breakdown described half the matrix. Nine rows collapsed both surfaces into one cell for eighteen variants, and none carried a node. Eighteen rows now, each with its State, Surface and node ID, behind the existing grouped summary.
Docs
C4 was held open on a claim that had stopped being true. Its note read "units are fixed at four" — the four booleans mean they are not. C3's note asked for a main/countdown/* namespace that is no longer needed now every colour is bound and confirmed. Both move to Ready, and Code Connect is emptied.
C4 resolved
Two recommendations close, two stay open on their merits. The unit-selection ask is applied, though as four booleans rather than the array it proposed, so each traces 1:1 to a Figma property. The accessibility ask is applied for both platforms. Still open: the behaviour contract — Figma cannot say what interval the timer ticks on, what happens at zero, or whether it runs in the background — and making the separator a component, which this run strengthened by finding its colour differs three ways across the styles.
Docs
2.0.0 — August 2026Major
Restructured into a primitive plus recipes — node 5630:36052
Rebuilt in the 2026 Working File. The previous component at 4076:9090 in Sticker Sheets v2 is deprecated.
Family
The promo card split out to Countdown Promo, leaving only the timer here.
Family
Every unit is a Countdown - Unit instance rather than type drawn per layout.
C1 resolved
C2 — the settings resolved. State stopped mixing in layout, the White/Blue Style values became Per/One/Pill, and the unit-subset versions were removed.
C2 resolved
C5 — State=Expired added across all three layouts and both surfaces.
C5 resolved
C6 — the pill's icon became ⤷ LeadingIcon, no longer one fixed feature-specific glyph.
C6 resolved
C1 — all three layouts unified. Units are named days / hrs / mins / secs with colon-separator siblings, replacing three different internal structures.
C1 resolved
C2 — Theme = Light | Dark became Surface = onLight | onColor, with the values swapped, since the old names described the component's own colour rather than its backdrop.
C2 resolved
Surface remains a version rather than a resolved token. Known limitation — GCash has no dark theme today and the app is built on white, so baking it in is the practical choice.
C4 partial