Missions list
A set of things to do, and what each is worth. Challenges, quests, weekly goals — the pattern that drives specific behaviour rather than repetition.
Install
Copy this folder alongside tokens.css.
The first composite
Every other component in the kit renders one state. This renders a list whose items each carry their own state, their own progress and their own action, and two things follow from that.
Campaign status and mission state are different axes. The campaign
can be over while a mission is complete; a mission can be locked while
the campaign is live. Collapsing them into one union would have made half
the real combinations unsayable, so status stays the kit's usual six
and mission.state is its own vocabulary.
The tile is not the control. The scratch card's panel and the coupon's cover are both buttons, because each is a single surface with a single action. A mission tile carries a title, a description, a bar and a button — making the whole thing a button would swallow the text into the button's accessible name and leave the real action ambiguous.
Mission states
Six, and each renders differently because each means something different to the member. Server truth, always.
state |
Means | Renders |
|---|---|---|
locked |
A prerequisite is unmet | Dimmed, no progress. Shown rather than hidden — a mission you cannot start yet is a reason to come back |
available |
Startable, untouched | Full strength, empty progress |
in-progress |
Begun | Full strength, partial progress |
complete |
Finished and claimable | Emphasised border, and the only state with an action |
claimed |
Finished and taken | Half-faded, full progress, its reward in the success tone, and a "claimed" chip where the claim button was. Kept on the list — one that empties as you succeed erases the evidence |
expired |
The window closed | Dimmed, full progress, reward muted |
Claimed is not expired
It is tempting to dim both identically, on the argument that what a success and a loss have in common is that there is nothing left to do. The argument is true and it is the wrong thing to design from: a member scanning the list could not tell the reward they won from the one they missed. An identical grey row says neither.
Three differences now, and each is doing a distinct job:
- The claim control's position carries the answer. A claimed mission
renders a quiet chip in the claim control's position — the eye is already
going there to look for something to press, and what it finds is that
you already did. The word is
labels.state("claimed"), the same string the row already speaks, so there is no second piece of copy to write. - The reward line changes tone rather than disappearing. Claimed shows it in the success colour, because that is what you got. Expired shows it muted, because advertising an unearned "+50 pts" at full strength reads as if it had been earned.
- Claimed is only half-faded. Expired is over and nothing about it should draw the eye; claimed is a settled success and stays legible.
The row's own emphasis rule is unchanged: complete is still the only
state that takes the action colour, because it is still the only one with
something to press. What claimed gains is content emphasis, not an
affordance.
State beats arithmetic
progress and target drive a bar. state decides what the mission
is. They are allowed to disagree, and when they do, state wins.
A mission at 5/5 that the server still calls in-progress is a mission
whose completion has not settled yet, and the honest render is a full bar
with no claim button. A component that inferred completion from
arithmetic would offer a reward the backend has not granted — the same
class of mistake as deciding a prize in the browser.
Progress
tracker |
Draws |
|---|---|
auto (default) |
Per mission: steps when countable, a bar when a bare fraction, nothing when there is no progress |
steps |
Pips wherever the mission is countable; a fraction mission keeps its bar — pips need whole things to count |
bar |
A continuous fill everywhere progress exists |
none |
No progress at all |
auto is the default because a list where one mission is countable and
another is not should not force the same picture onto both. Pips beat a
percentage when the unit is countable — "3/5 friends invited" is the
number the member is actually tracking — and a bar is right when progress
is a fraction of something with no natural steps.
A mission is countable when it has a target. With progress alone,
the value is treated as a fraction 0..1.
The fill takes --lk-action, so progress reads in the same colour as
every control in the kit.
Props
| Prop | Type | Notes |
|---|---|---|
missions |
Mission[] |
In reading order. Capped at ten — past that it is a backlog, and a member scrolling a backlog of chores closes the tab. |
labels |
{ list, claim, progress(ctx), state(s), ready(title), claimed(title) } |
Required; all copy, screen-reader announcements included. state("claimed") is also the visible chip on a claimed row. |
tracker |
"auto" | "bar" | "steps" | "none" |
Default auto. |
surface |
"card" | "none" |
Whether each mission sits on a tile. Default card. |
icons |
boolean |
The icon column. Default true; missions without artwork get a neutral placeholder so a mixed list still aligns. |
renderIcon |
({ mission, index }) => ReactNode |
Replaces a mission's artwork. |
renderBadge |
({ mission }) => ReactNode |
Replaces the badge. |
renderAction |
({ mission, claim }) => ReactNode |
Replaces the claim control. |
renderMission |
({ mission, index }) => ReactNode |
Replaces a whole tile — the seam for a layout this component does not have. |
tiles |
MissionTileConfig |
color, border, radius, fill, badgeColor, badgeTextColor, rewardColor, iconSize. Every value accepts any CSS value including var(--your-token). |
status |
the kit's six | Host-provided campaign state. |
renderLocked |
(status) => ReactNode |
Renders alongside the list. |
header / footer |
ReactNode |
Above/below: the season and a countdown; how missions refresh. |
terms |
ReactNode |
Compliance slot. |
onClaim |
(mission) => void |
Fires when the default claim control is pressed. |
Mission
| Field | Type | Notes |
|---|---|---|
id |
string |
Stable across renders. What your backend calls it, and what announcements key on. |
title |
string |
|
description |
string |
One supporting line. |
state |
MissionState |
See above. Server truth. |
progress |
number |
Steps done, or a 0..1 fraction when there is no target. |
target |
number |
Steps needed. Its presence is what makes a mission countable. |
badge |
string |
"New", "Ends today", "2x points". Text, not an icon — a badge nobody can read is decoration. |
reward |
string |
What it pays. |
icon |
ReactNode |
Artwork. The kit ships none. |
There is a default claim control, which is why onClaim exists
alongside renderAction: a component that ships a control must ship the
event that control fires, or the button does nothing until the adopter
discovers it needs replacing.
Campaign status
| Your campaign | status |
Member sees |
|---|---|---|
| Running | available |
The live list |
| Season closed | ended |
The list, dimmed, with your copy |
| Not enrolled | not-eligible |
The list, dimmed, with your copy |
| All rewards taken | already-played |
The list, dimmed, with your copy |
| Weekly claim cap hit | attempts-exhausted |
The list, with your come-back-later copy |
| Season not open | not-started |
The list, with an opens-on line |
Tokens
| Token | Default | Purpose |
|---|---|---|
--lk-mission-surface |
--lk-surface |
Tile fill. |
--lk-mission-border |
--lk-border |
Tile outline. |
--lk-mission-radius |
--lk-radius |
Tile corners. |
--lk-mission-padding |
calc(--lk-space * 3.5) |
Tile padding. |
--lk-mission-gap |
calc(--lk-space * 2) |
Space between tiles. |
--lk-mission-fill |
--lk-action |
The progress fill. |
--lk-mission-bar-height |
6px |
Bar and pip thickness. |
--lk-mission-badge |
color-mix(--lk-action 14%) |
Badge background. |
--lk-mission-badge-text |
--lk-action |
Badge text. |
--lk-mission-reward |
--lk-reward |
What it pays. |
--lk-mission-claimed |
--lk-success |
A claimed row's chip and reward line. Coloured by meaning, like the tier ladder's qualified line. |
--lk-mission-icon-size |
40px |
The icon well. |
--lk-mission-icon-radius |
--lk-radius-sm |
Its corners. |
The list adds nothing to tokens.css.
Accessibility
A real <ul>, so a screen reader announces how many missions there
are before reading them — the one fact a member most wants from a set of
chores, and the one a stack of divs cannot give.
One progressbar per mission, with the bar or pips hidden beneath it.
aria-valuetext carries words, because "2" inside a list of six missions
is not a standing.
Every state is spoken. A row distinguished by colour alone says
nothing to a screen reader and nothing at all to anyone who cannot see
it, so labels.state names each one in the accessible tree.
Announcements key on id, not index. A list that reorders or drops a
finished mission would otherwise announce whatever slid into the slot.
The claimable animation and every transition are suppressed under reduced motion. There is no gesture here, so WCAG 2.5.1 does not apply.
When to use it / when not to
Use it when the actions differ from each other — invite a friend, try a category, spend in-store. Prefer the streak tracker when the same action repeats and losing the run is the motivation, and the stamp card when it repeats safely.
Don't use it as a to-do list for the business. Ten missions is the cap because a member reading a backlog of chores stops reading; if you have more, the answer is rotation, not scroll.
Common mistakes
Inferring completion from progress; hiding locked missions; removing claimed ones; dimming a claimed mission and an expired one identically; badging every row so the list becomes bunting; keying announcements on index; and colouring states without naming them.