// Transport app UI
Electric Bike App UI Design in Expo
This electric bike app UI lets riders choose, check and ride a bike. Large product cut-outs lead each screen, with battery, usage and distance beside them.
- Screens
- 3
- Built with
- Expo SDK 58
- Fonts
- Inter
- License
- Free (MIT)

Volta E-Bike app screens

Bike detail
A large bike photo, battery meter, usage and distance tiles and a swipe to book control.

Bike list
Rider profile, search, category chips and bike cards with usage, battery and a booking control.

Ride navigation
A road photo with a route overlay, a map mode switch and a sheet with the next turn and ride stats.
Copy the Volta E-Bike kit
Use the artifact as a design brief, the skill to generate the UI with an AI agent, or the code to run it right away.
Design artifact
The full design: tokens, screens and source
# Volta E-Bike: design artifact
This electric bike app UI lets riders choose, check and ride a bike. Large product cut-outs lead each screen, with battery, usage and distance beside them.
- Type: Transport mobile app UI
- Screens: 3
- Artboard: 390 × 844
- Target: Expo SDK 58, Expo Router, TypeScript, expo-image, react-native-svg
- Live page: https://developermill.com/app-designs/electric-bike-app-ui-design
## Design direction
Clean white and light grey with a sunny yellow accent, oversized product photos that break the card edges and dark stat tiles.
- Oversized bike photos that overflow their cards
- Vertical battery meter
- Category chips with selected state
- Road view with next turn and ride stats
## Foundations
### Colour
| Token | Hex |
| --- | --- |
| Volta yellow | `#FFC93C` |
| Ink | `#111111` |
| Lime | `#C6F05A` |
| Orange | `#FF8A4C` |
| Fog | `#F2F2F2` |
| Steel | `#6B6B6B` |
### Typography
- Typefaces: Inter
- Common sizes (px): 12, 13, 14, 15, 17, 18, 26, 40
### Shape and depth
- Common corner radii (px): 17, 20, 22, 24, 25, 32
- Shadows: `0 2px 8px rgba(0,0,0,0.06)`
## Screens
### 1. Bike detail
A large bike photo, battery meter, usage and distance tiles and a swipe to book control.
Preview: https://developermill.com/app-designs/electric-bike-app-ui-design/bike.webp
Copy on this screen:
- VOLTA S50
- VOLTA S50 is a sleek, powerful, and versatile electric bike.
- 89%
- Usage Time
- 21h 12m
- Distance
- 165Km
- Swipe To Book
- ›››
### 2. Bike list
Rider profile, search, category chips and bike cards with usage, battery and a booking control.
Preview: https://developermill.com/app-designs/electric-bike-app-ui-design/home.webp
Copy on this screen:
- Max Cole
- Premium
- VOLTA S50
- Usage Time
- 21h 22m
- 89%
- Swipe To Book
- ›››
- VOLTA D50
- 23h 18m
- 79%
### 3. Ride navigation
A road photo with a route overlay, a map mode switch and a sheet with the next turn and ride stats.
Preview: https://developermill.com/app-designs/electric-bike-app-ui-design/navigate.webp
Copy on this screen:
- Rocky Ridge Colorado
- Highway (Route 67)
- In 32m, Turn Left
- Time Left
- 00:19:29
- Calories
- 629 Kcal
- Distance
- 19 Km
## Full source
The original artboards as HTML. Image URLs point at the hosted demo assets.
### Bike detail (Main)
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Volta S50 – bike detail</title>
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body{margin:0}
a{color:inherit;text-decoration:none}a:hover{color:inherit}
</style>
</helmet>
<div style="width: 390px; height: 844px; box-sizing: border-box; position: relative; overflow: hidden; background: #FFFFFF; font-family: Inter, system-ui, sans-serif; color: #111111; display: flex; flex-direction: column; padding: 58px 22px 30px">
<div style="display: flex; justify-content: space-between; align-items: center">
<a href="Home.dc.html" aria-label="Back" style="width: 48px; height: 48px; border-radius: 24px; background: #F2F2F2; display: flex; align-items: center; justify-content: center">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#111111" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"></path></svg>
</a>
<button type="button" aria-label="Settings" style="width: 48px; height: 48px; border-radius: 24px; background: #F2F2F2; border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#111111" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"></path></svg>
</button>
</div>
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-top: 22px; gap: 16px">
<div style="display: flex; flex-direction: column; gap: 10px; max-width: 250px">
<h1 style="margin: 0; font-size: 40px; font-weight: 400; letter-spacing: -0.5px; line-height: 1.1">VOLTA S50</h1>
<p style="margin: 0; font-size: 15px; line-height: 1.5; color: #6B6B6B">VOLTA S50 is a sleek, powerful, and versatile electric bike.</p>
</div>
<div style="width: 54px; border-radius: 27px; border: 1px solid #ECECEC; background: #FFFFFF; display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 12px 0 12px; flex-shrink: 0">
<svg width="20" height="20" viewBox="0 0 24 24" fill="#111111" aria-hidden="true"><path d="M13 2L4 14h7l-1 8 9-12h-7l1-8z"></path></svg>
<div style="width: 34px; height: 92px; border-radius: 17px; background: #F2F2F2; display: flex; align-items: flex-end; overflow: hidden">
<div style="width: 34px; height: 82px; border-radius: 17px; background: {{accent}}"></div>
</div>
<span style="font-size: 13px; font-weight: 600">89%</span>
</div>
</div>
<div style="flex-grow: 1; position: relative">
<img src="https://developermill.com/app-designs/electric-bike-app-ui-design/assets/71a9755ef5.png" alt="Volta S50 electric bike in sage green" style="position: absolute; left: -150px; top: 4px; width: 530px; height: auto">
</div>
<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px">
<div style="background: {{accent}}; border-radius: 22px; padding: 18px 16px; display: flex; flex-direction: column; gap: 14px; height: 116px; box-sizing: border-box">
<span style="font-size: 14px; font-weight: 500">Usage Time</span>
<div style="display: flex; justify-content: space-between; align-items: center">
<span style="font-size: 26px; font-weight: 600; letter-spacing: -0.3px">21h 12m</span>
<span style="width: 40px; height: 40px; border-radius: 20px; background: #111111; display: flex; align-items: center; justify-content: center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17L17 7M8 7h9v9"></path></svg>
</span>
</div>
</div>
<div style="background: #111111; color: #FFFFFF; border-radius: 22px; padding: 18px 16px; display: flex; flex-direction: column; gap: 14px; height: 116px; box-sizing: border-box">
<span style="font-size: 14px; font-weight: 500">Distance</span>
<div style="display: flex; justify-content: space-between; align-items: center">
<span style="font-size: 26px; font-weight: 600; letter-spacing: -0.3px">165Km</span>
<span style="width: 40px; height: 40px; border-radius: 20px; background: #FFFFFF; display: flex; align-items: center; justify-content: center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#111111" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17L17 7M8 7h9v9"></path></svg>
</span>
</div>
</div>
</div>
<a href="Navigate.dc.html" style="margin-top: 12px; height: 64px; border-radius: 32px; background: #F2F2F2; display: flex; align-items: center; padding: 0 8px; gap: 12px">
<span style="width: 50px; height: 50px; border-radius: 25px; background: #FFFFFF; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 8px rgba(0,0,0,0.06)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#111111" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"></rect><path d="M8 11V7a4 4 0 0 1 8 0v4"></path></svg>
</span>
<span style="flex-grow: 1; text-align: center; font-size: 15px; color: #6B6B6B">Swipe To Book</span>
<span style="font-size: 15px; letter-spacing: 2px; color: #9A9A9A; padding-right: 14px">›››</span>
</a>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props='{"accent":{"editor":"color","default":"#FFC93C","options":["#FFC93C","#C6F05A","#FF8A4C"]},"$preview":{"width":390,"height":844}}'>
class Component extends DCLogic {
renderVals() {
return { accent: this.props.accent ?? '#FFC93C' };
}
}
</script>
</body>
</html>
```
### Bike list (Home)
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Volta – bike list</title>
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body{margin:0}
a{color:inherit;text-decoration:none}a:hover{color:inherit}
input::placeholder{color:#8A8A8A}
</style>
</helmet>
<div style="width: 390px; height: 844px; box-sizing: border-box; position: relative; overflow: hidden; background: #F4F4F4; font-family: Inter, system-ui, sans-serif; color: #111111; display: flex; flex-direction: column; gap: 16px; padding: 58px 18px 0">
<div style="display: flex; justify-content: space-between; align-items: center">
<button type="button" aria-label="Menu" style="width: 48px; height: 48px; border-radius: 24px; background: #FFFFFF; border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#111111" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"></path></svg>
</button>
<div style="display: flex; align-items: center; gap: 10px; background: #FFFFFF; border-radius: 30px; padding: 5px 16px 5px 5px">
<img src="https://developermill.com/app-designs/electric-bike-app-ui-design/assets/25d2a7a6f3.jpg" alt="Max Cole" style="width: 42px; height: 42px; border-radius: 21px; object-fit: cover">
<div style="display: flex; flex-direction: column; gap: 1px">
<span style="font-size: 15px; font-weight: 600">Max Cole</span>
<span style="font-size: 12px; color: #6B6B6B">Premium</span>
</div>
</div>
</div>
<label style="height: 50px; border-radius: 25px; background: #FFFFFF; display: flex; align-items: center; gap: 10px; padding: 0 18px">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#6B6B6B" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="M20 20l-3.5-3.5"></path></svg>
<input type="search" placeholder="Search" aria-label="Search bikes" style="border: none; outline: none; background: transparent; font-family: inherit; font-size: 15px; color: #111111; flex-grow: 1">
</label>
<div style="display: flex; gap: 8px; margin-right: -18px; overflow: hidden">
<sc-for list="{{chips}}" as="chip" hint-placeholder-count="4">
<button type="button" onClick="{{chip.pick}}" style="{{chip.style}}">{{chip.label}}</button>
</sc-for>
</div>
<a href="Main.dc.html" style="position: relative; height: 330px; border-radius: 28px; background: #FFFFFF; overflow: hidden; flex-shrink: 0; display: block">
<div style="position: absolute; left: 20px; top: 22px; display: flex; flex-direction: column; gap: 6px">
<span style="font-size: 26px; font-weight: 500; letter-spacing: -0.3px">VOLTA S50</span>
<span style="font-size: 13px; color: #6B6B6B; margin-top: 12px">Usage Time</span>
<span style="font-size: 26px; font-weight: 600; letter-spacing: -0.3px">21h 22m</span>
</div>
<div style="position: absolute; right: 16px; top: 22px; display: flex; align-items: center; gap: 6px">
<span style="width: 34px; height: 34px; border-radius: 17px; background: #111111; display: flex; align-items: center; justify-content: center">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="5.5" cy="16.5" r="3.5"></circle><circle cx="18.5" cy="16.5" r="3.5"></circle><path d="M5.5 16.5L9 9h6l3.5 7.5M9 9l3 7.5h-6.5M15 9l-1-3h-2.5"></path></svg>
</span>
<span style="font-size: 14px; font-weight: 600">89%</span>
</div>
<img src="https://developermill.com/app-designs/electric-bike-app-ui-design/assets/71a9755ef5.png" alt="Volta S50 electric bike in sage green" style="position: absolute; left: 120px; top: 70px; width: 340px; height: auto">
<span style="position: absolute; left: 14px; right: 14px; bottom: 14px; height: 58px; border-radius: 29px; background: rgba(240,240,240,0.82); display: flex; align-items: center; gap: 10px; padding: 0 6px">
<span style="width: 46px; height: 46px; border-radius: 23px; background: #FFFFFF; display: flex; align-items: center; justify-content: center">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#111111" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"></rect><path d="M8 11V7a4 4 0 0 1 8 0v4"></path></svg>
</span>
<span style="flex-grow: 1; text-align: center; font-size: 14px; color: #5E5E5E">Swipe To Book</span>
<span style="font-size: 14px; letter-spacing: 2px; color: #8A8A8A; padding-right: 12px">›››</span>
</span>
</a>
<div style="position: relative; height: 330px; border-radius: 28px; background: #FFFFFF; overflow: hidden; flex-shrink: 0">
<div style="position: absolute; left: 20px; top: 22px; display: flex; flex-direction: column; gap: 6px">
<span style="font-size: 26px; font-weight: 500; letter-spacing: -0.3px">VOLTA D50</span>
<span style="font-size: 13px; color: #6B6B6B; margin-top: 12px">Usage Time</span>
<span style="font-size: 26px; font-weight: 600; letter-spacing: -0.3px">23h 18m</span>
</div>
<div style="position: absolute; right: 16px; top: 22px; display: flex; align-items: center; gap: 6px">
<span style="width: 34px; height: 34px; border-radius: 17px; background: #111111; display: flex; align-items: center; justify-content: center">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="5.5" cy="16.5" r="3.5"></circle><circle cx="18.5" cy="16.5" r="3.5"></circle><path d="M5.5 16.5L9 9h6l3.5 7.5M9 9l3 7.5h-6.5M15 9l-1-3h-2.5"></path></svg>
</span>
<span style="font-size: 14px; font-weight: 600">79%</span>
</div>
<img src="https://developermill.com/app-designs/electric-bike-app-ui-design/assets/ac289eaeaa.png" alt="Volta D50 electric bike in yellow" style="position: absolute; left: 140px; top: 74px; width: 330px; height: auto">
</div>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props='{"accent":{"editor":"color","default":"#FFC93C","options":["#FFC93C","#C6F05A","#FF8A4C"]},"$preview":{"width":390,"height":844}}'>
class Component extends DCLogic {
constructor(props) {
super(props);
this.state = { picked: 'All' };
}
renderVals() {
const accent = this.props.accent ?? '#FFC93C';
const base = 'height: 44px; border-radius: 22px; padding: 0 20px; font-family: inherit; font-size: 14px; font-weight: 500; white-space: nowrap; cursor: pointer; flex-shrink: 0; color: #111111; ';
const chips = ['All', 'Trail Bike', 'Hybrid Bike', 'City Bike'].map((label) => ({
label,
style: base + (this.state.picked === label
? 'background: ' + accent + '; border: 1px solid ' + accent + ';'
: 'background: #FFFFFF; border: 1px solid #E4E4E4;'),
pick: () => this.setState({ picked: label })
}));
return { accent, chips };
}
}
</script>
</body>
</html>
```
### Ride navigation (Navigate)
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Volta – ride navigation</title>
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body{margin:0}
a{color:inherit;text-decoration:none}a:hover{color:inherit}
</style>
</helmet>
<div style="width: 390px; height: 844px; box-sizing: border-box; position: relative; overflow: hidden; background: #111111; font-family: Inter, system-ui, sans-serif; color: #111111">
<img src="https://developermill.com/app-designs/electric-bike-app-ui-design/assets/3d91eb8069.jpg" alt="Empty mountain highway with double yellow lines through a pine forest" style="position: absolute; left: 0; top: 0; width: 390px; height: 620px; object-fit: cover; object-position: center 40%">
<div style="position: absolute; left: 18px; right: 18px; top: 58px; display: flex; justify-content: space-between; align-items: center">
<a href="Main.dc.html" aria-label="Back" style="width: 48px; height: 48px; border-radius: 24px; background: rgba(255,255,255,0.3); display: flex; align-items: center; justify-content: center">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"></path></svg>
</a>
<div role="group" aria-label="Map type" style="display: flex; background: #FFFFFF; border-radius: 24px; padding: 4px; gap: 2px">
<sc-for list="{{modes}}" as="mode" hint-placeholder-count="2">
<button type="button" onClick="{{mode.pick}}" aria-pressed="{{mode.on}}" style="{{mode.style}}">{{mode.label}}</button>
</sc-for>
</div>
</div>
<div style="position: absolute; left: 0; right: 0; bottom: 0; background: #FFFFFF; border-radius: 32px 32px 0 0; padding: 12px 16px 30px; display: flex; flex-direction: column; gap: 12px">
<div style="width: 40px; height: 4px; border-radius: 2px; background: #DADADA; align-self: center"></div>
<div style="display: flex; align-items: center; gap: 12px">
<span style="width: 48px; height: 48px; border-radius: 24px; border: 1px solid #ECECEC; display: flex; align-items: center; justify-content: center; flex-shrink: 0">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#111111" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="5.5" cy="16.5" r="3.5"></circle><circle cx="18.5" cy="16.5" r="3.5"></circle><path d="M5.5 16.5L9 9h6l3.5 7.5M9 9l3 7.5h-6.5M15 9l-1-3h-2.5"></path></svg>
</span>
<div style="display: flex; flex-direction: column; gap: 2px; flex-grow: 1">
<span style="font-size: 17px; font-weight: 600">Rocky Ridge Colorado</span>
<span style="font-size: 13px; color: #6B6B6B">Highway (Route 67)</span>
</div>
<button type="button" aria-label="Recenter" style="width: 52px; height: 52px; border-radius: 26px; background: #111111; border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; flex-shrink: 0">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="6.5"></circle><circle cx="12" cy="12" r="2" fill="#FFFFFF"></circle><path d="M12 2v3M12 19v3M2 12h3M19 12h3"></path></svg>
</button>
</div>
<div style="background: #F4F4F4; border-radius: 24px; padding: 20px 18px 20px 22px; display: flex; align-items: center; justify-content: space-between; gap: 12px">
<span style="font-size: 26px; font-weight: 500; letter-spacing: -0.3px; line-height: 1.25">In 32m, Turn Left</span>
<span style="width: 64px; height: 64px; border-radius: 32px; background: {{accent}}; display: flex; align-items: center; justify-content: center; flex-shrink: 0">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#111111" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6L4 11l5 5"></path><path d="M4 11h9a5 5 0 0 1 5 5v4"></path></svg>
</span>
</div>
<div style="display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; padding: 6px 6px 0">
<div style="display: flex; flex-direction: column; gap: 4px">
<span style="font-size: 13px; color: #6B6B6B">Time Left</span>
<span style="font-size: 18px; font-weight: 600">00:19:29</span>
</div>
<div style="display: flex; flex-direction: column; gap: 4px; align-items: center">
<span style="font-size: 13px; color: #6B6B6B">Calories</span>
<span style="font-size: 18px; font-weight: 600">629 Kcal</span>
</div>
<div style="display: flex; flex-direction: column; gap: 4px; align-items: flex-end">
<span style="font-size: 13px; color: #6B6B6B">Distance</span>
<span style="font-size: 18px; font-weight: 600">19 Km</span>
</div>
</div>
</div>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props='{"accent":{"editor":"color","default":"#FFC93C","options":["#FFC93C","#C6F05A","#FF8A4C"]},"$preview":{"width":390,"height":844}}'>
class Component extends DCLogic {
constructor(props) {
super(props);
this.state = { mode: 'Road' };
}
renderVals() {
const base = 'height: 40px; border-radius: 20px; padding: 0 18px; border: none; font-family: inherit; font-size: 14px; font-weight: 500; cursor: pointer; ';
const modes = ['Road', 'Satellite'].map((label) => {
const on = this.state.mode === label;
return {
label,
on: on ? 'true' : 'false',
style: base + (on ? 'background: #111111; color: #FFFFFF;' : 'background: transparent; color: #111111;'),
pick: () => this.setState({ mode: label })
};
});
return { accent: this.props.accent ?? '#FFC93C', modes };
}
}
</script>
</body>
</html>
```SKILL.md
Save as .claude/skills/electric-bike-app-ui/SKILL.md
--- name: electric-bike-app-ui description: Build the Volta E-Bike mobile app UI in Expo React Native. Use when asked to create a electric bike app UI or a new screen in this visual style. --- # Volta E-Bike app UI Build this transport app UI in an Expo Router project with TypeScript. Reference: https://developermill.com/app-designs/electric-bike-app-ui-design ## Visual direction Clean white and light grey with a sunny yellow accent, oversized product photos that break the card edges and dark stat tiles. Keep these signature details: - Oversized bike photos that overflow their cards - Vertical battery meter - Category chips with selected state - Road view with next turn and ride stats ## Set up ```bash npx expo install expo-image react-native-svg @expo-google-fonts/inter expo-font ``` ## Tokens | Name | Hex | | --- | --- | | Volta yellow | `#FFC93C` | | Ink | `#111111` | | Lime | `#C6F05A` | | Orange | `#FF8A4C` | | Fog | `#F2F2F2` | | Steel | `#6B6B6B` | - Typefaces: Inter - Text sizes (px): 12, 13, 14, 15, 17, 18, 26, 40 - Corner radii (px): 17, 20, 22, 24, 25, 32 Define these once and reuse them. Do not add new colours. ## Screens 1. **Bike detail.** A large bike photo, battery meter, usage and distance tiles and a swipe to book control. 2. **Bike list.** Rider profile, search, category chips and bike cards with usage, battery and a booking control. 3. **Ride navigation.** A road photo with a route overlay, a map mode switch and a sheet with the next turn and ride stats. ## Build rules - Draw each screen on a 390 × 844 artboard and scale it to the device width. - Load each custom font weight by family name with expo-font. Do not set fontWeight with custom fonts. - Draw icons, charts and illustrations with react-native-svg. - Use Pressable with an accessibilityLabel for controls, with at least a 44px target. - Navigate with router.navigate and keep sample data as constants at the top of the file. - Use expo-image with contentFit for photos. ## Done when - All routes are linked and reachable. - Colours, type and radii match the tokens above. - `npx tsc --noEmit` and `npx expo lint` pass. - The layout holds up on small and large phones.
Set up the project
npx create-expo-app@latest electric-bike-app --template default cd electric-bike-app rm -rf src npx expo install expo-image react-native-svg @expo-google-fonts/inter expo-font
src/app/_layout.tsx
import { Inter_400Regular, Inter_500Medium, Inter_600SemiBold } from '@expo-google-fonts/inter';
import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
export default function RootLayout() {
const [fontsLoaded] = useFonts({ Inter_400Regular, Inter_500Medium, Inter_600SemiBold });
if (!fontsLoaded) return null;
return <Stack screenOptions={{ headerShown: false }} />;
}src/app/index.tsx
import { Image } from 'expo-image';
import { router } from 'expo-router';
import { Pressable, Text, View } from 'react-native';
import Svg, { Circle, Path, Rect } from 'react-native-svg';
import { Screen } from '@/components/screen';
import { asset } from '@/lib/assets';
const accent = '#FFC93C';
export default function MainScreen() {
return (
<Screen background="#FFFFFF" statusBar="dark">
<View style={{ paddingTop: 58, paddingBottom: 30, paddingHorizontal: 22, flex: 1 }}>
<View
style={{ justifyContent: 'space-between', alignItems: 'center', flexDirection: 'row' }}>
<Pressable
accessibilityRole="link"
accessibilityLabel="Back"
onPress={() => router.navigate('/home')}
style={{
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: '#F2F2F2',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
}}>
<Svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="#111111"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round">
<Path d="M15 18l-6-6 6-6" />
</Svg>
</Pressable>
<Pressable
accessibilityRole="button"
accessibilityLabel="Settings"
style={{
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: '#F2F2F2',
borderWidth: 0,
alignItems: 'center',
justifyContent: 'center',
padding: 0,
flexDirection: 'row',
}}>
<Svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="#111111"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round">
<Circle cx="12" cy="12" r="3" />
<Path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z" />
</Svg>
</Pressable>
</View>
<View
style={{
justifyContent: 'space-between',
alignItems: 'flex-start',
marginTop: 22,
gap: 16,
flexDirection: 'row',
}}>
<View style={{ gap: 10, maxWidth: 250, flexShrink: 1 }}>
<Text
style={{
margin: 0,
fontFamily: 'Inter_400Regular',
fontSize: 40,
color: '#111111',
letterSpacing: -0.5,
lineHeight: 44,
}}>
VOLTA S50
</Text>
<Text
style={{
margin: 0,
fontFamily: 'Inter_400Regular',
fontSize: 15,
color: '#6B6B6B',
lineHeight: 22.5,
}}>
VOLTA S50 is a sleek, powerful, and versatile electric bike.
</Text>
</View>
<View
style={{
width: 56,
borderRadius: 27,
borderWidth: 1,
borderColor: '#ECECEC',
backgroundColor: '#FFFFFF',
alignItems: 'center',
gap: 10,
paddingVertical: 12,
paddingHorizontal: 0,
flexShrink: 0,
}}>
<Svg width="20" height="20" viewBox="0 0 24 24" fill="#111111">
<Path d="M13 2L4 14h7l-1 8 9-12h-7l1-8z" />
</Svg>
<View
style={{
width: 34,
height: 92,
borderRadius: 17,
backgroundColor: '#F2F2F2',
alignItems: 'flex-end',
overflow: 'hidden',
flexDirection: 'row',
}}>
<View style={{ width: 34, height: 82, borderRadius: 17, backgroundColor: accent }} />
</View>
<Text style={{ fontFamily: 'Inter_600SemiBold', fontSize: 13, color: '#111111' }}>
89%
</Text>
</View>
</View>
<View style={{ flexGrow: 1 }}>
<Image
source={{ uri: asset('71a9755ef5.png') }}
contentFit="fill"
accessibilityLabel="Volta S50 electric bike in sage green"
style={{ position: 'absolute', left: -150, top: 4, width: 530, aspectRatio: 1.592 }}
/>
</View>
<View style={{ flexDirection: 'row', gap: 8 }}>
<View
style={{
backgroundColor: accent,
borderRadius: 22,
paddingVertical: 18,
paddingHorizontal: 16,
gap: 14,
height: 116,
flex: 1,
}}>
<Text style={{ fontFamily: 'Inter_500Medium', fontSize: 14, color: '#111111' }}>
Usage Time
</Text>
<View
style={{
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: 'row',
}}>
<Text
style={{
flexShrink: 1,
fontFamily: 'Inter_600SemiBold',
fontSize: 26,
color: '#111111',
letterSpacing: -0.3,
}}>
21h 12m
</Text>
<View
style={{
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#111111',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
}}>
<Svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="#FFFFFF"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round">
<Path d="M7 17L17 7M8 7h9v9" />
</Svg>
</View>
</View>
</View>
<View
style={{
backgroundColor: '#111111',
borderRadius: 22,
paddingVertical: 18,
paddingHorizontal: 16,
gap: 14,
height: 116,
flex: 1,
}}>
<Text style={{ fontFamily: 'Inter_500Medium', fontSize: 14, color: '#FFFFFF' }}>
Distance
</Text>
<View
style={{
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: 'row',
}}>
<Text
style={{
flexShrink: 1,
fontFamily: 'Inter_600SemiBold',
fontSize: 26,
color: '#FFFFFF',
letterSpacing: -0.3,
}}>
165Km
</Text>
<View
style={{
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#FFFFFF',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
}}>
<Svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="#111111"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round">
<Path d="M7 17L17 7M8 7h9v9" />
</Svg>
</View>
</View>
</View>
</View>
<Pressable
accessibilityRole="link"
onPress={() => router.navigate('/navigate')}
style={{
marginTop: 12,
height: 64,
borderRadius: 32,
backgroundColor: '#F2F2F2',
alignItems: 'center',
paddingVertical: 0,
paddingHorizontal: 8,
gap: 12,
flexDirection: 'row',
}}>
<View
style={{
width: 50,
height: 50,
borderRadius: 25,
backgroundColor: '#FFFFFF',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
flexDirection: 'row',
}}>
<Svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="#111111"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round">
<Rect x="5" y="11" width="14" height="10" rx="2" />
<Path d="M8 11V7a4 4 0 0 1 8 0v4" />
</Svg>
</View>
<Text
style={{
flexGrow: 1,
flexShrink: 1,
fontFamily: 'Inter_400Regular',
fontSize: 15,
color: '#6B6B6B',
textAlign: 'center',
}}>
Swipe To Book
</Text>
<Text
style={{
paddingRight: 14,
flexShrink: 1,
fontFamily: 'Inter_400Regular',
fontSize: 15,
color: '#9A9A9A',
letterSpacing: 2,
}}>
›››
</Text>
</Pressable>
</View>
</Screen>
);
}src/app/home.tsx
import { Image } from 'expo-image';
import { router } from 'expo-router';
import { useState } from 'react';
import { Pressable, Text, TextInput, View } from 'react-native';
import Svg, { Circle, Path, Rect } from 'react-native-svg';
import { Screen } from '@/components/screen';
import { asset } from '@/lib/assets';
import { css, cssText } from '@/lib/css';
// Screen state, and the values the layout below reads from it.
function useScreen() {
const [state, setScreenState] = useState<Record<string, any>>({ picked: 'All' });
const setState = (patch: Record<string, any>) =>
setScreenState((current) => ({ ...current, ...patch }));
const accent = '#FFC93C';
const base =
'height: 44px; border-radius: 22px; padding: 0 20px; font-family: inherit; font-size: 14px; font-weight: 500; white-space: nowrap; cursor: pointer; flex-shrink: 0; color: #111111; ';
const chips = ['All', 'Trail Bike', 'Hybrid Bike', 'City Bike'].map((label) => ({
label,
style:
base +
(state.picked === label
? 'background: ' + accent + '; border: 1px solid ' + accent + ';'
: 'background: #FFFFFF; border: 1px solid #E4E4E4;'),
pick: () => setState({ picked: label }),
}));
return { accent, chips };
}
export default function HomeScreen() {
const { chips } = useScreen();
return (
<Screen background="#F4F4F4" statusBar="dark">
<View style={{ gap: 16, paddingTop: 58, paddingBottom: 0, paddingHorizontal: 18, flex: 1 }}>
<View
style={{ justifyContent: 'space-between', alignItems: 'center', flexDirection: 'row' }}>
<Pressable
accessibilityRole="button"
accessibilityLabel="Menu"
style={{
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: '#FFFFFF',
borderWidth: 0,
alignItems: 'center',
justifyContent: 'center',
padding: 0,
flexDirection: 'row',
}}>
<Svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="#111111"
strokeWidth="2"
strokeLinecap="round">
<Path d="M4 7h16M4 12h16M4 17h16" />
</Svg>
</Pressable>
<View
style={{
alignItems: 'center',
gap: 10,
backgroundColor: '#FFFFFF',
borderRadius: 30,
paddingVertical: 5,
paddingLeft: 5,
paddingRight: 16,
flexShrink: 1,
flexDirection: 'row',
}}>
<Image
source={{ uri: asset('25d2a7a6f3.jpg') }}
contentFit="cover"
accessibilityLabel="Max Cole"
style={{ width: 42, height: 42, borderRadius: 21 }}
/>
<View style={{ gap: 1, flexShrink: 1 }}>
<Text style={{ fontFamily: 'Inter_600SemiBold', fontSize: 15, color: '#111111' }}>
Max Cole
</Text>
<Text style={{ fontFamily: 'Inter_400Regular', fontSize: 12, color: '#6B6B6B' }}>
Premium
</Text>
</View>
</View>
</View>
<View
style={{
height: 50,
borderRadius: 25,
backgroundColor: '#FFFFFF',
alignItems: 'center',
gap: 10,
paddingVertical: 0,
paddingHorizontal: 18,
flexDirection: 'row',
}}>
<Svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="#6B6B6B"
strokeWidth="2"
strokeLinecap="round">
<Circle cx="11" cy="11" r="7" />
<Path d="M20 20l-3.5-3.5" />
</Svg>
<TextInput
placeholder="Search"
placeholderTextColor="#8A8A8A"
accessibilityLabel="Search bikes"
style={{
borderWidth: 0,
flexGrow: 1,
flexShrink: 1,
fontFamily: 'Inter_400Regular',
fontSize: 15,
color: '#111111',
}}
/>
</View>
<View style={{ gap: 8, marginRight: -18, overflow: 'hidden', flexDirection: 'row' }}>
{chips.map((chip, index) => (
<Pressable
key={index}
accessibilityRole="button"
onPress={chip.pick}
style={{
flexShrink: 1,
alignItems: 'center',
justifyContent: 'center',
...css(chip.style),
}}>
<Text
style={[
{
fontFamily: 'Inter_400Regular',
fontSize: 13.33,
color: '#000000',
textAlign: 'center',
},
cssText(chip.style),
]}>
{chip.label}
</Text>
</Pressable>
))}
</View>
<Pressable
accessibilityRole="link"
onPress={() => router.navigate('/')}
style={{
height: 330,
borderRadius: 28,
backgroundColor: '#FFFFFF',
overflow: 'hidden',
flexShrink: 0,
}}>
<View style={{ position: 'absolute', left: 20, top: 22, gap: 6 }}>
<Text
style={{
fontFamily: 'Inter_500Medium',
fontSize: 26,
color: '#111111',
letterSpacing: -0.3,
}}>
VOLTA S50
</Text>
<Text
style={{
marginTop: 12,
fontFamily: 'Inter_400Regular',
fontSize: 13,
color: '#6B6B6B',
}}>
Usage Time
</Text>
<Text
style={{
fontFamily: 'Inter_600SemiBold',
fontSize: 26,
color: '#111111',
letterSpacing: -0.3,
}}>
21h 22m
</Text>
</View>
<View
style={{
position: 'absolute',
right: 16,
top: 22,
alignItems: 'center',
gap: 6,
flexDirection: 'row',
}}>
<View
style={{
width: 34,
height: 34,
borderRadius: 17,
backgroundColor: '#111111',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
}}>
<Svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="#FFFFFF"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round">
<Circle cx="5.5" cy="16.5" r="3.5" />
<Circle cx="18.5" cy="16.5" r="3.5" />
<Path d="M5.5 16.5L9 9h6l3.5 7.5M9 9l3 7.5h-6.5M15 9l-1-3h-2.5" />
</Svg>
</View>
<Text
style={{
flexShrink: 1,
fontFamily: 'Inter_600SemiBold',
fontSize: 14,
color: '#111111',
}}>
89%
</Text>
</View>
<Image
source={{ uri: asset('71a9755ef5.png') }}
contentFit="fill"
accessibilityLabel="Volta S50 electric bike in sage green"
style={{ position: 'absolute', left: 120, top: 70, width: 340, aspectRatio: 1.592 }}
/>
<View
style={{
position: 'absolute',
left: 14,
right: 14,
bottom: 14,
height: 58,
borderRadius: 29,
backgroundColor: 'rgba(240,240,240,0.82)',
alignItems: 'center',
gap: 10,
paddingVertical: 0,
paddingHorizontal: 6,
flexDirection: 'row',
}}>
<View
style={{
width: 46,
height: 46,
borderRadius: 23,
backgroundColor: '#FFFFFF',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
}}>
<Svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="#111111"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round">
<Rect x="5" y="11" width="14" height="10" rx="2" />
<Path d="M8 11V7a4 4 0 0 1 8 0v4" />
</Svg>
</View>
<Text
style={{
flexGrow: 1,
flexShrink: 1,
fontFamily: 'Inter_400Regular',
fontSize: 14,
color: '#5E5E5E',
textAlign: 'center',
}}>
Swipe To Book
</Text>
<Text
style={{
paddingRight: 12,
flexShrink: 1,
fontFamily: 'Inter_400Regular',
fontSize: 14,
color: '#8A8A8A',
letterSpacing: 2,
}}>
›››
</Text>
</View>
</Pressable>
<View
style={{
height: 330,
borderRadius: 28,
backgroundColor: '#FFFFFF',
overflow: 'hidden',
flexShrink: 0,
}}>
<View style={{ position: 'absolute', left: 20, top: 22, gap: 6 }}>
<Text
style={{
fontFamily: 'Inter_500Medium',
fontSize: 26,
color: '#111111',
letterSpacing: -0.3,
}}>
VOLTA D50
</Text>
<Text
style={{
marginTop: 12,
fontFamily: 'Inter_400Regular',
fontSize: 13,
color: '#6B6B6B',
}}>
Usage Time
</Text>
<Text
style={{
fontFamily: 'Inter_600SemiBold',
fontSize: 26,
color: '#111111',
letterSpacing: -0.3,
}}>
23h 18m
</Text>
</View>
<View
style={{
position: 'absolute',
right: 16,
top: 22,
alignItems: 'center',
gap: 6,
flexDirection: 'row',
}}>
<View
style={{
width: 34,
height: 34,
borderRadius: 17,
backgroundColor: '#111111',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
}}>
<Svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="#FFFFFF"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round">
<Circle cx="5.5" cy="16.5" r="3.5" />
<Circle cx="18.5" cy="16.5" r="3.5" />
<Path d="M5.5 16.5L9 9h6l3.5 7.5M9 9l3 7.5h-6.5M15 9l-1-3h-2.5" />
</Svg>
</View>
<Text
style={{
flexShrink: 1,
fontFamily: 'Inter_600SemiBold',
fontSize: 14,
color: '#111111',
}}>
79%
</Text>
</View>
<Image
source={{ uri: asset('ac289eaeaa.png') }}
contentFit="fill"
accessibilityLabel="Volta D50 electric bike in yellow"
style={{ position: 'absolute', left: 140, top: 74, width: 330, aspectRatio: 1.549 }}
/>
</View>
</View>
</Screen>
);
}src/app/navigate.tsx
import { Image } from 'expo-image';
import { router } from 'expo-router';
import { useState } from 'react';
import { Pressable, Text, View } from 'react-native';
import Svg, { Circle, Path } from 'react-native-svg';
import { Screen } from '@/components/screen';
import { asset } from '@/lib/assets';
import { css, cssText } from '@/lib/css';
// Screen state, and the values the layout below reads from it.
function useScreen() {
const [state, setScreenState] = useState<Record<string, any>>({ mode: 'Road' });
const setState = (patch: Record<string, any>) =>
setScreenState((current) => ({ ...current, ...patch }));
const base =
'height: 40px; border-radius: 20px; padding: 0 18px; border: none; font-family: inherit; font-size: 14px; font-weight: 500; cursor: pointer; ';
const modes = ['Road', 'Satellite'].map((label) => {
const on = state.mode === label;
return {
label,
on: on ? 'true' : 'false',
style:
base +
(on ? 'background: #111111; color: #FFFFFF;' : 'background: transparent; color: #111111;'),
pick: () => setState({ mode: label }),
};
});
return { accent: '#FFC93C', modes };
}
export default function NavigateScreen() {
const { accent, modes } = useScreen();
return (
<Screen background="#111111" statusBar="light">
<View style={{ flex: 1 }}>
<Image
source={{ uri: asset('3d91eb8069.jpg') }}
contentFit="cover"
contentPosition={{ left: 'center', top: '40%' }}
accessibilityLabel="Empty mountain highway with double yellow lines through a pine forest"
style={{ position: 'absolute', left: 0, top: 0, width: 390, height: 620 }}
/>
<View
style={{
position: 'absolute',
left: 18,
right: 18,
top: 58,
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: 'row',
}}>
<Pressable
accessibilityRole="link"
accessibilityLabel="Back"
onPress={() => router.navigate('/')}
style={{
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: 'rgba(255,255,255,0.3)',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
}}>
<Svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="#FFFFFF"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round">
<Path d="M15 18l-6-6 6-6" />
</Svg>
</Pressable>
<View
style={{
backgroundColor: '#FFFFFF',
borderRadius: 24,
padding: 4,
gap: 2,
flexShrink: 1,
flexDirection: 'row',
}}>
{modes.map((mode, index) => (
<Pressable
key={index}
accessibilityRole="button"
onPress={mode.pick}
style={{
flexShrink: 1,
alignItems: 'center',
justifyContent: 'center',
...css(mode.style),
}}>
<Text
style={[
{
fontFamily: 'Inter_400Regular',
fontSize: 13.33,
color: '#000000',
textAlign: 'center',
},
cssText(mode.style),
]}>
{mode.label}
</Text>
</Pressable>
))}
</View>
</View>
<View
style={{
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
backgroundColor: '#FFFFFF',
borderTopLeftRadius: 32,
borderTopRightRadius: 32,
borderBottomRightRadius: 0,
borderBottomLeftRadius: 0,
paddingTop: 12,
paddingBottom: 30,
paddingHorizontal: 16,
gap: 12,
}}>
<View
style={{
width: 40,
height: 4,
borderRadius: 2,
backgroundColor: '#DADADA',
alignSelf: 'center',
}}
/>
<View style={{ alignItems: 'center', gap: 12, flexDirection: 'row' }}>
<View
style={{
width: 50,
height: 50,
borderRadius: 24,
borderWidth: 1,
borderColor: '#ECECEC',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
flexDirection: 'row',
}}>
<Svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="#111111"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round">
<Circle cx="5.5" cy="16.5" r="3.5" />
<Circle cx="18.5" cy="16.5" r="3.5" />
<Path d="M5.5 16.5L9 9h6l3.5 7.5M9 9l3 7.5h-6.5M15 9l-1-3h-2.5" />
</Svg>
</View>
<View style={{ gap: 2, flexGrow: 1, flexShrink: 1 }}>
<Text style={{ fontFamily: 'Inter_600SemiBold', fontSize: 17, color: '#111111' }}>
Rocky Ridge Colorado
</Text>
<Text style={{ fontFamily: 'Inter_400Regular', fontSize: 13, color: '#6B6B6B' }}>
Highway (Route 67)
</Text>
</View>
<Pressable
accessibilityRole="button"
accessibilityLabel="Recenter"
style={{
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: '#111111',
borderWidth: 0,
alignItems: 'center',
justifyContent: 'center',
padding: 0,
flexShrink: 0,
flexDirection: 'row',
}}>
<Svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="#FFFFFF"
strokeWidth="1.8"
strokeLinecap="round">
<Circle cx="12" cy="12" r="6.5" />
<Circle cx="12" cy="12" r="2" fill="#FFFFFF" />
<Path d="M12 2v3M12 19v3M2 12h3M19 12h3" />
</Svg>
</Pressable>
</View>
<View
style={{
backgroundColor: '#F4F4F4',
borderRadius: 24,
paddingVertical: 20,
paddingLeft: 22,
paddingRight: 18,
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
flexDirection: 'row',
}}>
<Text
style={{
flexShrink: 1,
fontFamily: 'Inter_500Medium',
fontSize: 26,
color: '#111111',
letterSpacing: -0.3,
lineHeight: 32.5,
}}>
In 32m, Turn Left
</Text>
<View
style={{
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: accent,
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
flexDirection: 'row',
}}>
<Svg
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="#111111"
strokeWidth="2.4"
strokeLinecap="round"
strokeLinejoin="round">
<Path d="M9 6L4 11l5 5" />
<Path d="M4 11h9a5 5 0 0 1 5 5v4" />
</Svg>
</View>
</View>
<View
style={{
paddingTop: 6,
paddingBottom: 0,
paddingHorizontal: 6,
flexDirection: 'row',
gap: 8,
}}>
<View style={{ gap: 4, flex: 1 }}>
<Text style={{ fontFamily: 'Inter_400Regular', fontSize: 13, color: '#6B6B6B' }}>
Time Left
</Text>
<Text style={{ fontFamily: 'Inter_600SemiBold', fontSize: 18, color: '#111111' }}>
00:19:29
</Text>
</View>
<View style={{ gap: 4, alignItems: 'center', flex: 1 }}>
<Text style={{ fontFamily: 'Inter_400Regular', fontSize: 13, color: '#6B6B6B' }}>
Calories
</Text>
<Text style={{ fontFamily: 'Inter_600SemiBold', fontSize: 18, color: '#111111' }}>
629 Kcal
</Text>
</View>
<View style={{ gap: 4, alignItems: 'flex-end', flex: 1 }}>
<Text style={{ fontFamily: 'Inter_400Regular', fontSize: 13, color: '#6B6B6B' }}>
Distance
</Text>
<Text style={{ fontFamily: 'Inter_600SemiBold', fontSize: 18, color: '#111111' }}>
19 Km
</Text>
</View>
</View>
</View>
</View>
</Screen>
);
}src/components/screen.tsx
import { StatusBar } from 'expo-status-bar';
import type { ReactNode } from 'react';
import { ScrollView, useWindowDimensions, View } from 'react-native';
// Every screen is drawn on a 390 wide artboard, then scaled to the device width.
const ARTBOARD_WIDTH = 390;
type Props = {
background: string;
statusBar: 'light' | 'dark';
height?: number;
children: ReactNode;
};
export function Screen({ background, statusBar, height: artboardHeight = 844, children }: Props) {
const { width, height } = useWindowDimensions();
const scale = width / ARTBOARD_WIDTH;
return (
<ScrollView
bounces={false}
showsVerticalScrollIndicator={false}
style={{ flex: 1, backgroundColor: background }}
contentContainerStyle={{ height: Math.max(artboardHeight * scale, height) }}>
<StatusBar style={statusBar} />
<View
style={{
width: ARTBOARD_WIDTH,
height: artboardHeight,
overflow: 'hidden',
transformOrigin: 'left top',
transform: [{ scale }],
}}>
{children}
</View>
</ScrollView>
);
}src/lib/assets.ts
// Demo images are hosted by DeveloperMill. Point this at your own CDN, or swap to require().
const ASSET_BASE = 'https://developermill.com/app-designs/electric-bike-app-ui-design/assets';
export const asset = (name: string) => `${ASSET_BASE}/${name}`;src/lib/css.ts
import type { TextStyle, ViewStyle } from 'react-native';
// A few styles change with state, so they are kept as short CSS strings from the design.
const FACES: Record<number, string> = {400: 'Inter_400Regular', 500: 'Inter_500Medium', 600: 'Inter_600SemiBold', 700: 'Inter_700Bold', 800: 'Inter_800ExtraBold'};
const px = (value: string) => (value.endsWith('%') ? (value as `${number}%`) : parseFloat(value));
function declarations(source: string) {
return source
.split(';')
.map((rule) => rule.split(':').map((part) => part.trim()))
.filter(([name, value]) => name && value);
}
export function css(source: string): ViewStyle {
const style: Record<string, unknown> = {};
for (const [name, ...rest] of declarations(source)) {
const value = rest.join(':');
if (name === 'background' || name === 'background-color') {
style.backgroundColor = value;
} else if (name === 'border') {
const [width, , ...color] = value.split(' ');
if (value === 'none' || value === '0') style.borderWidth = 0;
else {
style.borderWidth = parseFloat(width);
style.borderColor = color.join(' ');
}
} else if (name === 'display' && value === 'flex') {
style.flexDirection = style.flexDirection ?? 'row';
} else if (name === 'flex-direction') {
style.flexDirection = value;
} else if (name === 'align-items') {
style.alignItems = value;
} else if (name === 'justify-content') {
style.justifyContent = value;
} else if (name === 'box-shadow') {
style.boxShadow = value;
} else if (name === 'position') {
style.position = value === 'absolute' ? 'absolute' : 'relative';
} else if (name === 'opacity') {
style.opacity = parseFloat(value);
} else if (name === 'gap' || name === 'width' || name === 'height' || name === 'left' || name === 'top') {
style[name] = px(value);
} else if (name === 'border-radius') {
style.borderRadius = px(value);
} else if (name === 'padding') {
const [vertical, horizontal = vertical] = value.split(' ');
style.paddingVertical = px(vertical);
style.paddingHorizontal = px(horizontal);
} else if (name === 'white-space' && value === 'nowrap') {
style.flexShrink = 0;
} else if (name === 'flex-shrink') {
style.flexShrink = parseFloat(value);
} else if (name === 'flex') {
if (value === 'none') style.flexGrow = 0;
else style.flex = parseFloat(value);
}
}
return style as ViewStyle;
}
export function cssText(source: string): TextStyle {
const style: Record<string, unknown> = {};
for (const [name, value] of declarations(source)) {
if (name === 'color') style.color = value;
if (name === 'font-size') style.fontSize = parseFloat(value);
if (name === 'font-weight' && FACES[Number(value)]) style.fontFamily = FACES[Number(value)];
if (name === 'text-decoration' && value.startsWith('line-through')) {
style.textDecorationLine = 'line-through';
}
}
return style as TextStyle;
}About this design
A swipe-to-book control sits on each bike card, and the navigation screen pairs a road view with a clear next turn. The layout fits e-bike rentals, scooters and connected vehicle companion apps.
Clean white and light grey with a sunny yellow accent, oversized product photos that break the card edges and dark stat tiles.
- Oversized bike photos that overflow their cards
- Vertical battery meter
- Category chips with selected state
- Road view with next turn and ride stats
Colour palette
Volta yellow
#FFC93C
Ink
#111111
Lime
#C6F05A
Orange
#FF8A4C
Fog
#F2F2F2
Steel
#6B6B6B
How to use this design
- 01
Pick what to copy
Copy the Volta E-Bike artifact, skill or code from this page.
- 02
Run it or generate it
Paste the code into a new Expo app and start it, or give the skill to your AI coding agent.
- 03
Make it yours
Replace the demo images and sample content, then connect your own data.
Frequently asked questions
Do the chips and map switch work?
Yes. Category chips and the Road and Satellite switch hold selected state. Filter your bike list or map style from those values.
Is the swipe to book control functional?
It is styled as a static control. Add a pan gesture with react-native-gesture-handler to make it slide.
Can it be used for scooters?
Yes. Replace the photos and stats. The list, detail and navigation screens stay the same.
Is the Volta E-Bike design free to use?
Yes. The design, skill and Expo code are free and MIT licensed, including for commercial apps. Replace the demo images and sample content with your own before you ship.
What is the difference between the artifact, skill and code?
The artifact is the full design: tokens, screens and source. The skill is a SKILL.md that teaches an AI coding agent to build the UI. The code is the finished Expo app you can paste in and run.
Which Expo version does the code use?
It targets Expo SDK 58 with Expo Router and TypeScript. It uses packages installed through npx expo install, with no custom native code.



