The screen-level entry into an order: a search icon, the destination prompt in two centred lines, and the service image — one full-width row assembled from customButton.
Overview
DestinationButton is the control a rider taps to start an order. It sits at the top of the ride flow as a full-width row: the search icon on the left, the question in two centred lines in the middle, and the service the prompt is about on the right. It is a locked configuration of customButton — the base stays open, this preset fixes the height, the fill, the typography and both side slots.
-
1
Standard
-
2
Skeleton
States
The state list is complete: the row rests, it is held, or it is still loading.
-
1
Standard The rest state. var(--surface-on-white) fill, var(--text-and-icon-primary) title and var(--text-and-icon-secondary) subtitle, both side slots shown.
-
2
Pressed While the row is held. Scale steps 100% → 95% on var(--component-push-button-press) and returns on var(--component-push-button-release); the fill and both text rows keep their tokens.
-
3
Skeleton The shape alone while the order data loads. The content is dropped and the whole shape is handed to the Skeleton component at radius var(--sp-s20).
There is no Disabled form. The Figma set's State axis carries Standard | Skeleton only — a screen that cannot start an order shows the Skeleton while the data is missing rather than a dimmed row a rider could still tap.
Anatomy
DestinationButton is built out of slots named with the DS-wide StartSlot / StartText / EndSlot semantics. Both side slots are swap slots: they hold a default component and accept another in its place, and the two text rows carry defaults rather than fixed strings.
Row
-
1
DestinationButton .destination-button — the root and the only tap target. Full width of its container x var(--sp-s64), radius var(--sp-s20), the three parts on one row with space-between. Required.
-
2
StartSlot .destination-button__start-slot — var(--sp-s64) x var(--sp-s56) swap slot, default a modified-size IconContainer: the box is stretched to the slot, the glyph inside keeps its var(--sp-s32) box. Required.
-
3
EndSlot .destination-button__end-slot — var(--sp-s64) x var(--sp-s56) swap slot, default the 3D service image filling the box, mirrored so the vehicle faces the text. Required.
Text column
-
4
StartText .destination-button__start-text — the two rows in a column, var(--sp-s2) gap, centred. Required.
-
5
Title row .destination-button__title-row — Heading 2. The question itself. Single line: overflowing text truncates to an ellipsis rather than wrapping. Required.
-
6
Subtitle row .destination-button__subtitle-row — Compact Body in var(--text-and-icon-secondary). Single line, truncates to an ellipsis. Optional, on by default; it is what tells a rider the next screen also quotes a price.
Layout
The row is fluid: it takes the width of its container and keeps every other value fixed.
Vertical
-
General var(--sp-s64) — height, the one size. Width is 100% of the container; there is no size ladder.var(--sp-s20) — roundness, all four corners.No padding, any side. The side slots centre in the row's height and supply the inset themselves — there is nothing to band.
-
1
The box is stretched, the glyph is not Both side slots are var(--sp-s56) tall in the var(--sp-s64) row and centre in it — the 4 of air above and below each is centring arithmetic (64 − 56), not a declared padding. Inside the leading one the glyph keeps its var(--sp-s32) box, centred on both axes: it is the box that was modified, never the icon.
Horizontal — at minimum width
-
General var(--sp-s12) — gap, both sides of the text column. It is the minimum, not the rendered value: space-between hands the leftover width to the two gaps, which is why the row is redlined here at the width where there is none.var(--sp-s64) — both side slots. The service image fills the trailing one; in the leading one the var(--sp-s32) glyph is centred, which is where the leading air comes from.278 — the width below which the text column starts to truncate. Annotation arithmetic, not a token: 64 + 12 + 126 + 12 + 64.
Animation
Motion follows the motion system § 6.1, which names the button family as the consumer of pushButton. The press is the component's only motion of its own.
Press
-
General var(--component-push-button-press) — press, scale 100% → 95%.var(--component-push-button-release) — release, back to 100%.Only transform takes part, so the press stays on the compositor. The fill and both text rows keep their tokens through the press — nothing recolours.
-
Reduced motion Handled at the token: motion.css zeroes every --component-* inside prefers-reduced-motion: reduce, so the press becomes instant and this component carries no override of its own.
-
Skeleton The shimmer is not this component's animation. The row hands its whole shape to the Skeleton component, and the sweep comes with it.
Usage
Under construction — the usage guidance for DestinationButton is being reworked and will land in a later version.
Accessibility
Full spec: destination-button-a11y.md. Both rows below are real, so Tab reaches the first one and shows its actual ring.
-
1
One Tap Target The row is one control, not a composition of tappable parts. Its name is the two text rows read in order, joined by the name computation with a space — "Where to? And for how much?" — so the Standard state needs no aria-label. The icon and the service image carry aria-hidden="true": the image repeats what the prompt says, and the icon repeats what the button trait already carries.
-
2
Skeleton Is Out Of The Tree Not focusable, no tabindex. aria-hidden="true" takes it out of TalkBack's swipe order and out of VoiceOver's rotor, so both move from the element before it to the element after it; the screen announces its own loading through the region's aria-busy. There is no Disabled form on this component, so neither platform ever announces DISABLED or .dimmed for it.
-
The Subtitle Is Part Of The Name "And for how much?" changes what the control promises — a rider who hears only "Where to?" does not know the next screen also quotes a price. It belongs in the name, which is why the two rows are joined rather than split across aria-describedby.
-
Focus :focus-visible — var(--sp-s2) in var(--text-and-icon-primary) at radius var(--sp-s20), offset inward. Inward because the row spans its container: an outward ring would be drawn past that container's edge and clipped.
-
Keyboard Tab / Shift+Tab move onto and off the row — one stop for the whole thing.Enter and Space both activate, which a native <button> carries on its own.The trap: a <div> standing in for the row — tempting, because it holds an image and two lines and reads like a card — loses the role, the focus stop and both keys at once. It needs role="button", tabindex="0" and a handler for each key. And neither text row may become interactive: a link in the title makes two controls where the design has one, and focus then stops twice on the same row.
-
Contrast Title on var(--surface-on-white) — 15.69:1 light, 13.20:1 dark. AA and AAA both pass.Subtitle in var(--text-and-icon-secondary) — 4.64:1 light, 6.13:1 dark. AA passes at 14px; AAA does not.The light-theme margin over AA is 0.14, the narrowest number on the component. It leaves no room to darken the fill or lighten the subtitle — a subtitle that needs headroom moves to var(--text-and-icon-primary) rather than to a new colour.
-
Target Size var(--sp-s64) tall and the full width of its container, against a 44 x 44 bar. The whole row is the target, icon and image included — nothing inside it invites a separate tap, and nothing inside it has one.
-
Long Strings The label is read in full even when the visible text truncates — the name comes from the text content, not from the rendered line. A product string that is too long for the row is still announced whole.
-
Rotor By Heading The title is Heading 2 by style, not by semantics: a <span> inside a button, so it does not enter VoiceOver's heading rotor. That is correct — the control is an action, not a section title.
-
RTL The row mirrors as a whole from the document's dir: the icon moves to the trailing edge, the service image to the leading one, and the centred text column needs no change. Nothing flips in place — the default search glyph is not on the directional list in icon-container.md § 3.5, so the component carries no directional flip of its own. The illustration is the exception, and not a directional one: it ships scaleX(-1) in every variation, which belongs to the artwork and stays put whichever way the row runs. In Figma the mirrored composition is a variant of the set (RTL: Off | On).