While working on VoteHub, a mobile absentee ballot solution for U.S. elections, I was tasked with designing and prototyping an interface for a relatively new election contest type, rapidly gaining attention and adoption, called Ranked Choice Voting (RCV).
By leveraging Expo’s Snack tool, React Native, and the react-native-reanimated animation library, I was able to deliver a fully-functional prototype in about two weeks’ time.
What is Ranked Choice Voting?
The Ranked Choice Voting contest type allows voters to mark preferences for a set of candidates in order of preference, rather than just a single candidate. Stated benefits of RCV are many, including eliminating the need for a runoff election in the case of a tie, and by some accounts a more accurate and thus representative model of the people’s voice.1
In this year’s U.S. general election, RCV is being used by a number of jurisdictions and cities (among them Portland, OR, and San Fransisco, CA), as well as the states of Alaska and Maine for both the presidential contest and to elect their members of Congress.2 Given its rate of adoption nationwide, it’s clear why supporting the contest type has been a priority for VoteHub.
There are a handful of different modalities voters may use to cast their vote, paper and a voting booth system being the most common two. For both, the surface area of a physical ballot or voting booth system’s screen is generally large enough or sufficient to handle the layout variations recommended for RCV. In VoteHub’s case, however, surface area is at a premium: the screen of my iPhone 12 mini is only about 8.6% the area of just one side of this year’s Washington State paper absentee ballot3. On the other hand, a paper ballot’s design is static, while a dynamic mobile app design, with some creative thinking, might allow for an equal or better voting experience. So, how to go about making RCV work on a phone?
Screen sizes and interaction best practices
An excerpt from the Center for Civic Design’s “Best Practices: Designing Ranked Choice Voting Ballots” (PDF, 2022), showing a printed RCV ballot example.
Two points of concern in creating an intuitive mobile interface for RCV were the small viewport sizes on which the app would be rendered, and honoring interaction requirements detailed by the Voluntary Voting System Guidelines (VVSG).
One of the trickiest user experience points related to RCV is a concept called “adjudication”: per the VVSG, and somewhat paraphrased, the voter must be in explicit control of their selections, or put differently, a voter’s selections (and the display order of candidates) cannot be changed or adjudicated by the system as a side effect of another action. The VVSG’s guidance against adjudication runs counter to a few tenets of product design, among them, to reduce a user’s interactions with a UI is to improve that user’s experience, and to display content in its most organized form is to increase the chances of it being understood. In an electoral context, however, equality and freedom of choice are paramount, and eclipse both principles: all voters should be presented with candidates in the same order, and all voters should be allowed to make as few or many selections as they desire in any given election, ballot measure or contest. To properly support these requirements, the RCV interface needed to have explicit controls for a voter to mark, revise and sort candidate ranks.
From the start (and as suspected), the Center for Civic Design (CCV) was an amazing resource on election mechanics in general, and on the topic of RCV in particular. They explained how the contest type works and how to design for it in print to adhere to election best practices, and also published research detailing a prototype they built for a digital voting booth system — a great starting point.
Whereas the most widely-accepted print version of an RCV design amounts to a table—with candidates in rows, ranks in columns, and cells to assign candidate ranks—a digital voting booth design couldn’t accommodate the surface area of a table, and needed to answer differently the design questions of displaying and modifying rank. Additionally, the manner in which a voter is able to navigate a digital voting booth’s interface is highly constrained, with really only two modalities available: touch screen interactions; and keyboard navigation by way of one or two directional (arrow) keys and a selection (“enter”/“return”) key, with screen reader assistance if needed.
An excerpt from CCV’s “Designing an accessible RCV ballot” (PDF, 2020), showing their RCV ballot design.
CCV’s solution took into account both interaction modalities by thinking through a keyboard-navigating voter’s focus path through the interface, putting elements into hierarchical and semantic order, and exposing essential actions at the global and candidate level. Their solution resulted in an interface listing candidates in print ballot order; a button to put candidates in voter-ranked order once ranked; a “rank space” on each candidate that when tapped assigns the next-available rank to (or removes the rank from) a candidate; and buttons on each candidate row to raise or lower that candidate’s rank.
The obvious benefit of CCV’s well-researched voting booth prototype was that, with modifications, it likely could operate at a mobile viewport size, it had been tested, and it already had already taken into account VVSG accessibility requirements and the general logic of a non-adjudicating UX. But how would their RCV interface work with VoteHub: How could we be fairly assured the interface would be intuitive for voters using our app? We needed to try out some ideas with a prototype, but first we needed to design what it would look like with this research in mind.
Designing a mobile RCV interface
I created a straightforward series of RCV ballot design options in Figma using our component library and based on the above research. Some options were very easy to rule out, the most obvious being the “adapted optical scan” variant, which essentially repeats the contest’s candidate list by the number of candidates present, resulting in a lot of screens and some arduous navigation. We settled on a design very similar to CCV’s, with some alterations: candidate name and rank area is clearly visually delineated as a button; rank adjustment controls are always visible on each candidate; the next-available rank is shown on all remaining unranked candidates, if the voter hasn’t ranked all candidates or met the contest criteria; when the contest criteria is met, additional unranked candidate actions are disabled; and the most exciting part, the user can update candidate ranks by way of drag-drop, which I’ll write more about in a bit.
An excerpt from our RCV interface options Figma file, showing the recommended (and accepted) RCV design option.
Picking the right prototyping tool
I needed a way to personally test out drag-drop behavior with our prototype, and we later would need to test its behavior with others, too, so the prototyping tool had to be able to animate list items, manage global and local component states, and hopefully allow for some kind of keyboard or screen reader navigation.
Figma, as of Spring 2023, was not a viable solution; variables had yet to be introduced later that year, and so manually configuring a prototype with, say, eight candidates would have taken 40,320 individual connections just to set candidate selection state, let alone assigning rank.
ProtoPie was a great contender—it could do everything we needed it to—but as I hadn’t used it before, there was a bit of a learning curve involved and not much time to get properly up to speed. At the point of considering it for the job, I had a conversation with another Nearform design engineer, who suggested that perhaps the most efficient way forward would be to create the prototype in code. And an engineer on the VoteHub team mentioned that the React Native platform Expo had an online editor called Snack, that allows one to quickly spin up a React Native + Expo project and test it on iOS and Android. Put together, the suggestions were a revelation: we’d have a coded prototype that, with some tweaks and additional work, could be dropped into the VoteHub app—which meant that design and development time each could be cut roughly in half—and we could share it as a standalone Snack project for testing.
I should note that in many prototyping cases, it just doesn’t make sense to go directly to code. With Figma’s introduction of variables—not to mention a host of other tools well-suited to the task of complex prototyping—it might be more efficient to use the design tool you’re most familiar with to get the job done.
React Native, Expo, and Reanimated
Scaffolding out the prototype in React Native with Expo was a relative breeze, given that the dataset was extremely lightweight, and RN’s native components (like View) make composing layouts a simple task: a fixed header, a scrolling main section, and a footer with actions required creating only five components in total. Expo’s Snack tool, it turned out, allowed me to download the work I’d done so far as a bootstrapped and ready-to-go project I could continue to edit with my IDE of choice. The real task, it turned out, was to dial in the logic around candidate ranking, bringing user interactions to life with Reanimated, and discovering through trial and error what were the most intuitive set of rank adjustment interactions.
Rank logic
The interface needed to perform the following actions on candidates:
- rank a candidate (by tapping the candidate button containing their name),
- raise or lower a candidate’s rank (by tapping arrows to the right of a candidate), and
- sort candidates into rank order (by tapping the footer’s “Put in rank order” button).
Pretty straightforward.
Once I’d wired up the above actions to the design and gave our interface a first pass, it became clear the mechanism of updating ranks would cause confusion. Take the following example:
A voter first taps the last candidate in the list, ranking them 1, then taps the first candidate in the list, ranking them 2. How does the voter swap these two candidates’ ranks? Using either the "up" arrow on the first candidate in the list, or the "down" arrow on the last candidate — but doesn’t “down” mean … down? Is this even the right button?
Since the list’s visual order and rank order were both (necessarily) being rendered along a single vertical axis, a voter might have trouble telling the two dimensions apart, and think that by pressing the “down” arrow the rank target would be the next, visually adjacent candidate in the list, rather than the next-lowest-ranked candidate.
I could think of two solutions to the "up"/"down" arrow issue: swap list positions of the candidates with adjacent ranks, and use animation to underscore the rank change—in other words, alter candidate list order and, despite it having been the result of an explicit user action, tread toward adjudicating on behalf of the voter; or leave the candidates in place and swap rank numbers.
The latter, arguably a cleaner and simpler solution, was surprisingly more confusing than the former. Seeing rank changes out of the corner of one’s eye on candidates other than the one whose arrows one tapped was both hard to visually track and, perhaps more importantly, verify as correct.
And so: enter Reanimated.
Animating the prototype with Reanimated
The Reanimated library provides a means to natively animate elements in React Native, meaning that the result runs smoothly regardless of device or operating system.
Styles in RN are applied by creating a stylesheet and applying classes defined there by way of an element’s style attribute, not too dissimilar in concept from basic HTML.
Reanimated exports an “animatable” Animated
component wrapper for RN built-ins (like View
) or Text
) that allow for defining and updating custom animations and transitions. They also have a number of hooks to help animate changes in one’s data. Two hooks, useSharedValue
and useAnimatedStyle
, were especially handy to interpolate color values and conditionally switch styles for, say, a button that’s either active or disabled. I specified some spring defaults and wrote a small wrapper hook to adjust multiple parameters off a single “shared value.” That took care of the prototype’s general styles.
I now needed to animate changes to list order. Reanimated allows for defining layout animations, accomplished as easily as switching FlatList
(used in RN to render, you guessed it, a list of elements) to Animated.FlatList
, and passing a few extra props, like itemLayoutAnimation
(which configures the specific layout transition applied when the list layout changes).
The working solution I came to was to fade out the candidate being dragged and render a placeholder rank in the drop target area, to indicate to the voter as much as possible that in dropping the candidate, the candidate’s rank would be updated to the rank indicated.
Conclusion
Tusk Philanthropies, when presented with the completed prototype, were delighted with it, and shared it with a number of folks in the elections space who also gave similarly positive feedback.
As with any design, creating a working version and testing with real people is the only true way to discover whether or not it works. I believe the RCV ballot contest type will soon be implemented and ready to test with real voters in the VoteHub app—an exciting prospect that I hope goes well for all involved!
Last but not least, this article wouldn’t be complete without the prototype itself, which you can view and interact with here (click or tap “Launch Snack” to start!).
If for whatever reason the embed isn’t working, you can find the Expo Snack project here. I’d recommend testing out the prototype using the virtual iOS or Android devices, which you can toggle in the upper right-hand corner; React Native Web with Expo version 48 doesn’t support the list animations you’d otherwise see in iOS/Android.
Footnotes
-
“Best Practices: Designing Ranked Choice Voting Voter Education” (PDF), Center for Civic Design, June 2022 ↩
-
“Fact Sheet: Ranked Choice Voting Headed For Biggest Election Day Yet,” FairVote, 23 Oct 2024 ↩
-
iPhone 12 mini is 5.18 x 2.53”, or 13.11 square inches; ballot is 8.5 x 18”, or 153 square inches; comparing the two (13.11 / 153), the phone screen is roughly 8.6% of the ballot’s surface area. ↩