// Lifestyle app UI
Dog Care App UI Design in Expo
Dog Assistant is a bright, neo-brutalist pet app UI for owners who want to track their dog and meet other owners at local events.
- Screens
- 3
- Built with
- Expo SDK 58
- Fonts
- Outfit
- License
- Free (MIT)

Dog Assistant app screens

Welcome
A cut-out pug on coral blobs, a big title, a loose hand-drawn squiggle with sparkles and a Let's go button.

Events
An outdoor events card with a jumping dog, a photo card with a sticker badge and Join action, and an outlined tab bar.

Dog tracking
An aerial map with a live route and pins, zoom controls, distance to the dog and transport times.
Copy the Dog Assistant 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
# Dog Assistant: design artifact
Dog Assistant is a bright, neo-brutalist pet app UI for owners who want to track their dog and meet other owners at local events.
- Type: Lifestyle mobile app UI
- Screens: 3
- Artboard: 390 × 844
- Target: Expo SDK 58, Expo Router, TypeScript, expo-image, expo-linear-gradient, react-native-svg
- Live page: https://developermill.com/app-designs/dog-assistant-app-ui-design
## Design direction
Playful neo-brutalism with thick black outlines, hard shadows, lime and coral colour blocking and cut-out dog photography.
- Neo-brutalist outlines and hard shadows
- Map tracking with a live route and pins
- Event cards with cut-out photography
- Transport options with travel times
## Foundations
### Colour
| Token | Hex |
| --- | --- |
| Pale lime | `#EEF7BC` |
| Neon lime | `#C8F25A` |
| Coral | `#F26B5B` |
| Pine | `#1F4A35` |
| Outline | `#141413` |
| Sky | `#9FC7E6` |
### Typography
- Typefaces: Outfit
- Common sizes (px): 13, 15, 16, 17, 24, 26, 30, 48
### Shape and depth
- Common corner radii (px): 3, 22, 24, 26, 28, 32
- Shadows: `0 4px 0 #141413`, `0 3px 0 #141413`
## Screens
### 1. Welcome
A cut-out pug on coral blobs, a big title, a loose hand-drawn squiggle with sparkles and a Let's go button.
Preview: https://developermill.com/app-designs/dog-assistant-app-ui-design/welcome.webp
Copy on this screen:
- Dog
- Assistant
- Helping owners track their dogs and organise events to meet up for networking
- Let’s go
### 2. Events
An outdoor events card with a jumping dog, a photo card with a sticker badge and Join action, and an outlined tab bar.
Preview: https://developermill.com/app-designs/dog-assistant-app-ui-design/events.webp
Copy on this screen:
- Events
- What is new?
- Outdoor
- events
- Perfect chance to enrich your social circle
- How does
- your dog
- feel?
- New
- event!
- Join
### 3. Dog tracking
An aerial map with a live route and pins, zoom controls, distance to the dog and transport times.
Preview: https://developermill.com/app-designs/dog-assistant-app-ui-design/tracking.webp
Copy on this screen:
- Dog tracking
- Spike
- Downing St, 794
- 3.7 km
- Transports
- Wrap
- Find others
- Create event
## Full source
The complete design source for each screen. Images point at hosted demo files.
### Main
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Dog Assistant — Welcome</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=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body{margin:0}
a{color:#1F4A35}a:hover{color:#14301f}
</style>
</helmet>
<div style="width: 390px; height: 844px; box-sizing: border-box; background: {{ground}}; font-family: 'Outfit', system-ui, sans-serif; color: #141413; position: relative; overflow: hidden; display: flex; flex-direction: column; padding: 64px 24px 36px">
<div style="position: relative; height: 330px; margin: 0 -8px">
<svg viewBox="0 0 360 330" width="360" height="330" aria-hidden="true" style="position: absolute; left: 0; top: 0">
<g transform="rotate(-8 180 165)">
<ellipse cx="185" cy="98" rx="150" ry="62" fill="{{accent}}" stroke="#141413" stroke-width="3"></ellipse>
<ellipse cx="175" cy="170" rx="160" ry="66" fill="{{accent}}" stroke="#141413" stroke-width="3"></ellipse>
<ellipse cx="185" cy="242" rx="150" ry="62" fill="{{accent}}" stroke="#141413" stroke-width="3"></ellipse>
</g>
</svg>
<img src="https://developermill.com/app-designs/dog-assistant-app-ui-design/assets/53dc736f77.webp" alt="Black pug in a grey knitted sweater" style="position: absolute; left: 50px; top: 0px; width: 262px; height: 312px; object-fit: contain; filter: grayscale(1) contrast(1.08)"></div>
<div style="display: flex; flex-direction: column; gap: 14px; margin-top: 18px">
<h1 style="margin: 0; font-size: 48px; line-height: 1.02; font-weight: 700; letter-spacing: -1.5px">Dog<br>Assistant</h1>
<p style="margin: 0; font-size: 15px; line-height: 1.4; color: #2E3A22; max-width: 260px">Helping owners track their dogs and organise events to meet up for networking</p>
</div>
<div style="flex: 1; position: relative">
<svg viewBox="0 0 340 150" width="340" height="150" aria-hidden="true" style="position: absolute; left: 0; top: 4px">
<path d="M40 10 c-30 30 30 50 0 80 c-20 20 -10 35 10 45" fill="none" stroke="#141413" stroke-width="2.5" stroke-linecap="round"></path>
<path d="M44 128 l8 9 l-12 2" fill="none" stroke="#141413" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M215 70 l5 13 l13 5 l-13 5 l-5 13 l-5 -13 l-13 -5 l13 -5 Z" fill="#1F4A35"></path>
<path d="M300 40 l7 18 l18 7 l-18 7 l-7 18 l-7 -18 l-18 -7 l18 -7 Z" fill="{{accent}}"></path>
<path d="M260 20 l3 8 l8 3 l-8 3 l-3 8 l-3 -8 l-8 -3 l8 -3 Z" fill="#B7D94A"></path>
<path d="M190 112 l2 6 l6 2 l-6 2 l-2 6 l-2 -6 l-6 -2 l6 -2 Z" fill="{{accent}}"></path>
<path d="M250 118 l2 5 l5 2 l-5 2 l-2 5 l-2 -5 l-5 -2 l5 -2 Z" fill="#1F4A35"></path>
</svg>
</div>
<a href="Events.dc.html" style="display: flex; align-items: center; justify-content: center; height: 56px; border-radius: 28px; background: #1F4A35; color: #F4FBD8; font-size: 17px; font-weight: 500; text-decoration: none; border: 2.5px solid #141413; box-shadow: 0 4px 0 #141413">Let’s go</a>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props='{"accent":{"editor":"color","default":"#F26B5B","options":["#F26B5B","#FF9F43","#7C6CF2","#3BA7F2"]},"ground":{"editor":"color","default":"#EEF7BC","options":["#EEF7BC","#FFF4E0","#E8F0FF"]},"$preview":{"width":390,"height":844}}'>
class Component extends DCLogic {
renderVals() {
return { accent: this.props.accent ?? '#F26B5B', ground: this.props.ground ?? '#EEF7BC' };
}
}
</script>
</body>
</html>
```
### Events
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Dog Assistant — Events</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=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body{margin:0}
a{color:#1F4A35}a:hover{color:#14301f}
</style>
</helmet>
<div style="width: 390px; height: 844px; box-sizing: border-box; background: #EEF7BC; font-family: 'Outfit', system-ui, sans-serif; color: #141413; position: relative; overflow: hidden; display: flex; flex-direction: column; gap: 16px; padding: 60px 20px 28px">
<div style="display: flex; align-items: center; justify-content: space-between">
<a href="Main.dc.html" aria-label="Back" style="width: 44px; height: 44px; border-radius: 22px; background: #C8F25A; border: 2.5px solid #141413; display: flex; align-items: center; justify-content: center; box-sizing: border-box">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#141413" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"></path></svg>
</a>
<div style="font-size: 17px; font-weight: 600">Events</div>
<button type="button" aria-label="Settings" style="width: 44px; height: 44px; border-radius: 22px; background: #C8F25A; border: 2.5px solid #141413; display: flex; align-items: center; justify-content: center; padding: 0; cursor: pointer">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#141413" stroke-width="2.2" 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; align-items: center; justify-content: space-between; margin-top: 6px">
<h1 style="margin: 0; font-size: 30px; font-weight: 600; letter-spacing: -0.5px">What is new?</h1>
<svg width="40" height="40" viewBox="0 0 40 40" aria-hidden="true"><path d="M8 32 c10 -2 4 -16 14 -20 c6 -2 10 4 10 -6" fill="none" stroke="#F26B5B" stroke-width="2.5" stroke-linecap="round"></path></svg>
</div>
<div style="position: relative; height: 196px; border-radius: 26px; background: #1F4A35; border: 2.5px solid #141413; box-shadow: 0 4px 0 #141413; padding: 20px; box-sizing: border-box; color: #F4FBD8; overflow: visible">
<div style="font-size: 24px; font-weight: 500; line-height: 1.1">Outdoor<br>events</div>
<p style="margin: 10px 0 0; font-size: 13px; line-height: 1.35; color: #C9DDB5; max-width: 150px">Perfect chance to enrich your social circle</p>
<div style="position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%); display: flex; gap: 6px">
<span style="width: 18px; height: 6px; border-radius: 3px; background: #F26B5B"></span>
<span style="width: 6px; height: 6px; border-radius: 3px; background: #F4FBD8"></span>
<span style="width: 6px; height: 6px; border-radius: 3px; background: #F4FBD8"></span>
<span style="width: 6px; height: 6px; border-radius: 3px; background: #F4FBD8"></span>
</div>
<svg viewBox="0 0 80 40" width="80" height="40" aria-hidden="true" style="position: absolute; right: 96px; top: -14px"><ellipse cx="40" cy="20" rx="30" ry="11" fill="#F26B5B" stroke="#141413" stroke-width="2.5" transform="rotate(-14 40 20)"></ellipse></svg>
<img src="https://developermill.com/app-designs/dog-assistant-app-ui-design/assets/05d181ed31.webp" alt="Happy terrier leaping through the air" style="position: absolute; right: -22px; top: 6px; width: 200px; height: 152px; object-fit: contain; transform: rotate(-12deg)"><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" style="position: absolute; right: 18px; top: 8px"><path d="M10 0 l3 7 l7 3 l-7 3 l-3 7 l-3 -7 l-7 -3 l7 -3 Z" fill="#C8F25A"></path></svg>
</div>
<div style="position: relative; flex: 1; border-radius: 26px; background: #9FC7E6; border: 2.5px solid #141413; box-shadow: 0 4px 0 #141413; overflow: hidden; padding: 20px; box-sizing: border-box; display: flex; flex-direction: column; justify-content: space-between">
<img src="https://developermill.com/app-designs/dog-assistant-app-ui-design/assets/217fada78a.webp" alt="Cocker spaniel with ears flying, head out of a car window" style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; object-fit: cover; object-position: 70% 30%"><div style="position: relative; font-size: 24px; font-weight: 500; line-height: 1.1; color: #141413">How does<br>your dog<br>feel?</div>
<div style="position: absolute; right: 18px; top: 16px; width: 70px; height: 70px">
<svg viewBox="0 0 70 70" width="70" height="70" aria-hidden="true" style="position: absolute; left: 0; top: 0"><path d="M35 2 l7 10 l12 -4 l1 12 l12 3 l-6 11 l8 9 l-11 5 l1 12 l-12 -1 l-5 11 l-9 -8 l-9 8 l-5 -11 l-12 1 l1 -12 l-11 -5 l8 -9 l-6 -11 l12 -3 l1 -12 l12 4 Z" fill="#F26B5B" stroke="#141413" stroke-width="2"></path></svg>
<div style="position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; text-align: center; font-size: 11px; font-weight: 600; line-height: 1.1; transform: rotate(-12deg); color: #141413">New<br>event!</div>
</div>
<div style="position: relative; display: flex; align-items: center; justify-content: space-between">
<button type="button" style="width: 140px; height: 48px; border-radius: 24px; background: #C8F25A; border: 2.5px solid #141413; font-family: inherit; font-size: 16px; font-weight: 500; color: #141413; cursor: pointer">Join</button>
<button type="button" aria-label="Open event" style="width: 44px; height: 44px; border-radius: 22px; background: #1F4A35; border: 2.5px solid #141413; display: flex; align-items: center; justify-content: center; padding: 0; cursor: pointer">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#F4FBD8" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17L17 7M8 7h9v9"></path></svg>
</button>
</div>
</div>
<nav aria-label="Main" style="height: 64px; border-radius: 32px; background: #F26B5B; border: 2.5px solid #141413; display: flex; align-items: center; justify-content: space-around; box-sizing: border-box">
<a href="Events.dc.html" aria-label="Home" style="width: 44px; height: 44px; display: flex; align-items: center; justify-content: center"><svg width="24" height="24" viewBox="0 0 24 24" fill="#141413" aria-hidden="true"><path d="M3 11l9-7 9 7v9a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z"></path></svg></a>
<a href="Events.dc.html" aria-label="Weather" style="width: 44px; height: 44px; display: flex; align-items: center; justify-content: center"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#141413" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M7 18h10a4 4 0 0 0 0-8 6 6 0 0 0-11.5 1.5A3.3 3.3 0 0 0 7 18z"></path></svg></a>
<a href="Tracking.dc.html" aria-label="Tracking" style="width: 44px; height: 44px; display: flex; align-items: center; justify-content: center"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#141413" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 21s7-6.2 7-12a7 7 0 0 0-14 0c0 5.8 7 12 7 12z"></path><circle cx="12" cy="9" r="2.5"></circle></svg></a>
<a href="Main.dc.html" aria-label="Profile" style="width: 44px; height: 44px; display: flex; align-items: center; justify-content: center"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#141413" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="8" r="4"></circle><path d="M4 21c1.5-4 4.5-6 8-6s6.5 2 8 6"></path></svg></a>
</nav>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props='{"$preview":{"width":390,"height":844}}'>
class Component extends DCLogic {
renderVals() { return {}; }
}
</script>
</body>
</html>
```
### Tracking
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Dog Assistant — Tracking</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=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body{margin:0}
a{color:#1F4A35}a:hover{color:#14301f}
</style>
</helmet>
<div style="width: 390px; height: 844px; box-sizing: border-box; background: #EEF7BC; font-family: 'Outfit', system-ui, sans-serif; color: #141413; position: relative; overflow: hidden">
<div style="position: absolute; left: 0; top: 0; width: 390px; height: 470px; background: #6E7F5C">
<img src="https://developermill.com/app-designs/dog-assistant-app-ui-design/assets/5306f632e1.webp" alt="Aerial map of the neighbourhood around Spike" style="position: absolute; left: 0; top: -40px; width: 390px; height: 585px; object-fit: cover"><div style="position: absolute; left: 0; top: 0; width: 390px; height: 130px; background: linear-gradient(rgba(0,0,0,0.35), rgba(0,0,0,0))"></div>
<svg viewBox="0 0 390 470" width="390" height="470" aria-hidden="true" style="position: absolute; left: 0; top: 0">
<path d="M58 290 C110 270 130 230 170 236 C210 242 220 200 250 166" fill="none" stroke="#141413" stroke-width="7" stroke-linecap="round" opacity="0.5"></path>
<path d="M58 290 C110 270 130 230 170 236 C210 242 220 200 250 166" fill="none" stroke="#C8F25A" stroke-width="4" stroke-linecap="round"></path>
</svg>
<div style="position: absolute; left: 30px; top: 252px; width: 54px; height: 66px">
<svg viewBox="0 0 54 66" width="54" height="66" aria-hidden="true"><ellipse cx="27" cy="60" rx="14" ry="5" fill="#141413" opacity="0.35"></ellipse><path d="M27 58 L18 44 A20 20 0 1 1 36 44 Z" fill="#F4FBD8" stroke="#141413" stroke-width="2.5"></path><circle cx="27" cy="27" r="14" fill="#D9934D"></circle><circle cx="22" cy="25" r="2" fill="#141413"></circle><circle cx="32" cy="25" r="2" fill="#141413"></circle><ellipse cx="27" cy="31" rx="3" ry="2" fill="#141413"></ellipse></svg>
</div>
<div style="position: absolute; left: 224px; top: 120px; width: 54px; height: 66px">
<svg viewBox="0 0 54 66" width="54" height="66" aria-hidden="true"><ellipse cx="27" cy="60" rx="14" ry="5" fill="#141413" opacity="0.35"></ellipse><path d="M27 58 L18 44 A20 20 0 1 1 36 44 Z" fill="#F26B5B" stroke="#141413" stroke-width="2.5"></path><circle cx="27" cy="27" r="14" fill="#F4FBD8"></circle><circle cx="27" cy="23" r="5" fill="#1F4A35"></circle><path d="M18 36 a9 7 0 0 1 18 0" fill="#1F4A35"></path></svg>
</div>
</div>
<div style="position: absolute; left: 20px; right: 20px; top: 60px; display: flex; align-items: center; justify-content: space-between">
<a href="Events.dc.html" aria-label="Back" style="width: 44px; height: 44px; border-radius: 22px; background: #C8F25A; border: 2.5px solid #141413; display: flex; align-items: center; justify-content: center; box-sizing: border-box">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#141413" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"></path></svg>
</a>
<div style="font-size: 17px; font-weight: 600; color: #FFFFFF; text-shadow: 0 1px 4px rgba(0,0,0,0.5)">Dog tracking</div>
<button type="button" aria-label="Settings" style="width: 44px; height: 44px; border-radius: 22px; background: #C8F25A; border: 2.5px solid #141413; display: flex; align-items: center; justify-content: center; padding: 0; cursor: pointer">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#141413" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="3"></circle><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M4.9 19.1L7 17M17 7l2.1-2.1"></path></svg>
</button>
</div>
<div style="position: absolute; right: 20px; top: 270px; display: flex; flex-direction: column; gap: 10px">
<button type="button" aria-label="Zoom in" style="width: 44px; height: 44px; border-radius: 22px; background: #C8F25A; border: 2.5px solid #141413; display: flex; align-items: center; justify-content: center; padding: 0; cursor: pointer"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#141413" stroke-width="2.6" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"></path></svg></button>
<button type="button" aria-label="Zoom out" style="width: 44px; height: 44px; border-radius: 22px; background: #C8F25A; border: 2.5px solid #141413; display: flex; align-items: center; justify-content: center; padding: 0; cursor: pointer"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#141413" stroke-width="2.6" stroke-linecap="round" aria-hidden="true"><path d="M5 12h14"></path></svg></button>
</div>
<div style="position: absolute; left: 12px; right: 12px; top: 438px; bottom: 24px; display: flex; flex-direction: column; gap: 0">
<div style="background: #EEF7BC; border: 2.5px solid #141413; border-radius: 26px 26px 0 0; border-bottom: none; padding: 16px 18px; display: flex; align-items: center; gap: 12px">
<div style="width: 44px; height: 44px; border-radius: 22px; background: #F26B5B; border: 2px solid #141413; display: flex; align-items: center; justify-content: center; box-sizing: border-box">
<svg width="30" height="30" viewBox="0 0 30 30" aria-hidden="true"><circle cx="15" cy="16" r="10" fill="#D9934D"></circle><path d="M6 10 C2 12 3 20 7 22 L9 14 Z M24 10 C28 12 27 20 23 22 L21 14 Z" fill="#A9652A"></path><circle cx="12" cy="15" r="1.6" fill="#141413"></circle><circle cx="18" cy="15" r="1.6" fill="#141413"></circle><ellipse cx="15" cy="19" rx="2.2" ry="1.6" fill="#141413"></ellipse></svg>
</div>
<div style="flex: 1; display: flex; flex-direction: column; gap: 2px">
<div style="font-size: 16px; font-weight: 600">Spike</div>
<div style="font-size: 13px; color: #4A5638">Downing St, 794</div>
</div>
<div style="font-size: 26px; font-weight: 600; letter-spacing: -0.5px">3.7 km</div>
</div>
<div style="flex: 1; background: {{accent}}; border: 2.5px solid #141413; border-radius: 26px; padding: 18px; display: flex; flex-direction: column; gap: 18px; box-shadow: 0 4px 0 #141413; margin-top: -10px">
<div style="display: flex; align-items: center; justify-content: space-between">
<h2 style="margin: 0; font-size: 26px; font-weight: 500; color: #FFFFFF">Transports</h2>
<button type="button" style="background: none; border: none; font-family: inherit; font-size: 13px; color: #FFFFFF; cursor: pointer; min-height: 44px; padding: 0 4px">Wrap</button>
</div>
<div style="display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px">
<sc-for list="{{modes}}" as="m" hint-placeholder-count="4">
<button type="button" aria-label="{{m.label}}" style="background: none; border: none; padding: 0; font-family: inherit; display: flex; flex-direction: column; align-items: center; gap: 8px; cursor: pointer">
<span style="width: 58px; height: 58px; border-radius: 29px; background: #FFFFFF; border: 2.5px solid #141413; box-shadow: 0 3px 0 #141413; display: flex; align-items: center; justify-content: center">
<sc-if value="{{m.car}}" hint-placeholder-val="{{ false }}"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#141413" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 16V11l2-5h10l2 5v5"></path><rect x="3" y="11" width="18" height="6" rx="2"></rect><circle cx="7.5" cy="14" r="1"></circle><circle cx="16.5" cy="14" r="1"></circle><path d="M6 17v2M18 17v2"></path></svg></sc-if>
<sc-if value="{{m.bus}}" hint-placeholder-val="{{ false }}"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#141413" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="3" width="14" height="15" rx="3"></rect><path d="M5 11h14M8 18v2M16 18v2"></path><circle cx="8.5" cy="14.5" r="1"></circle><circle cx="15.5" cy="14.5" r="1"></circle></svg></sc-if>
<sc-if value="{{m.bike}}" hint-placeholder-val="{{ false }}"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#141413" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="6" cy="16" r="3.5"></circle><circle cx="18" cy="16" r="3.5"></circle><path d="M6 16l4-8h5l3 8M10 8l3 8h-7M14 5h2"></path></svg></sc-if>
<sc-if value="{{m.walk}}" hint-placeholder-val="{{ false }}"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#141413" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="13" cy="4" r="2"></circle><path d="M9 21l2-6 3 3v3M11 15l1-6 3 3 3 1M12 9l-4 2-1 4"></path></svg></sc-if>
</span>
<span style="font-size: 12px; color: #FFFFFF">{{m.time}}</span>
</button>
</sc-for>
</div>
<div style="flex: 1"></div>
<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px">
<button type="button" style="height: 52px; border-radius: 26px; background: #C8F25A; border: 2.5px solid #141413; font-family: inherit; font-size: 16px; font-weight: 500; color: #141413; cursor: pointer">Find others</button>
<button type="button" style="height: 52px; border-radius: 26px; background: #1F4A35; border: 2.5px solid #141413; font-family: inherit; font-size: 16px; font-weight: 500; color: #F4FBD8; cursor: pointer">Create event</button>
</div>
</div>
</div>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props='{"accent":{"editor":"color","default":"#E85A4A","options":["#E85A4A","#FF9F43","#7C6CF2","#3BA7F2"]},"$preview":{"width":390,"height":844}}'>
class Component extends DCLogic {
renderVals() {
return {
accent: this.props.accent ?? '#E85A4A',
modes: [
{ label: 'Car', time: '15 min', car: true },
{ label: 'Bus', time: '25 min', bus: true },
{ label: 'Bike', time: '40 min', bike: true },
{ label: 'Walk', time: '1 h', walk: true }
]
};
}
}
</script>
</body>
</html>
```SKILL.md
Save as .claude/skills/dog-assistant-app-ui/SKILL.md
--- name: dog-assistant-app-ui description: Build the Dog Assistant mobile app UI in Expo React Native. Use when asked to create a dog app UI or a new screen in this visual style. --- # Dog Assistant app UI Build this UI in an Expo SDK 58 app with Expo Router and strict TypeScript. Match the spec below rather than inventing a new look. The finished reference lives at https://developermill.com/app-designs/dog-assistant-app-ui-design ## Visual direction Playful neo-brutalism with thick black outlines, hard shadows, lime and coral colour blocking and cut-out dog photography. Keep these signature details: - Neo-brutalist outlines and hard shadows - Map tracking with a live route and pins - Event cards with cut-out photography - Transport options with travel times ## Set up ```bash npx expo install expo-image expo-linear-gradient react-native-svg @expo-google-fonts/outfit expo-font ``` ## Tokens | Token | Hex | | --- | --- | | Pale lime | `#EEF7BC` | | Neon lime | `#C8F25A` | | Coral | `#F26B5B` | | Pine | `#1F4A35` | | Outline | `#141413` | | Sky | `#9FC7E6` | - Typefaces: Outfit - Common sizes (px): 13, 15, 16, 17, 24, 26, 30, 48 - Common corner radii (px): 3, 22, 24, 26, 28, 32 Define these once and reuse them. Do not add new colours. ## Screens Build one Expo Router route per screen, in this order: 1. **Welcome**: A cut-out pug on coral blobs, a big title, a loose hand-drawn squiggle with sparkles and a Let's go button. 2. **Events**: An outdoor events card with a jumping dog, a photo card with a sticker badge and Join action, and an outlined tab bar. 3. **Dog tracking**: An aerial map with a live route and pins, zoom controls, distance to the dog and transport times. ## Build rules - Design on a 390 × 844 artboard, then scale to the device width so layouts match on every phone. - Load each custom font weight by its own family name with expo-font. Do not rely on `fontWeight` with custom fonts. - Draw icons, charts and illustrations with react-native-svg rather than image files or chart libraries. - Use `Pressable` with an `accessibilityLabel` for every icon-only control, and keep touch targets at 44 px or larger. - Wire navigation with `router.navigate`, and keep sample data in constants at the top of each file so it is easy to replace. - Use expo-image for photos, with `contentFit` set explicitly. ## Done when - Every screen above exists as a route and they link together. - Colours, type and radii match the tokens exactly. - `npx tsc --noEmit` and `npx expo lint` both pass. - Layouts hold up on small and large phones.
Set up the project
npx create-expo-app@latest dog-assistant-app --template default cd dog-assistant-app rm -rf src npx expo install expo-image expo-linear-gradient react-native-svg @expo-google-fonts/outfit expo-font
src/app/_layout.tsx
import {
Outfit_400Regular,
Outfit_500Medium,
Outfit_600SemiBold,
Outfit_700Bold,
} from '@expo-google-fonts/outfit';
import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
export default function RootLayout() {
const [fontsLoaded] = useFonts({
Outfit_400Regular,
Outfit_500Medium,
Outfit_600SemiBold,
Outfit_700Bold,
});
if (!fontsLoaded) return null;
return <Stack screenOptions={{ headerShown: false }} />;
}src/app/events.tsx
import { Image } from 'expo-image';
import { router } from 'expo-router';
import { Pressable, Text, View } from 'react-native';
import Svg, { Circle, Ellipse, Path } from 'react-native-svg';
import { Screen } from '@/components/screen';
import { asset } from '@/lib/assets';
export default function EventsScreen() {
return (
<Screen background="#EEF7BC" statusBar="dark">
<View style={{ gap: 16, paddingTop: 60, paddingBottom: 28, paddingHorizontal: 20, flex: 1 }}>
<View
style={{ alignItems: 'center', justifyContent: 'space-between', flexDirection: 'row' }}>
<Pressable
accessibilityRole="link"
accessibilityLabel="Back"
onPress={() => router.navigate('/')}
style={{
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: '#C8F25A',
borderWidth: 2.5,
borderColor: '#141413',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
}}>
<Svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="#141413"
strokeWidth="2.6"
strokeLinecap="round"
strokeLinejoin="round">
<Path d="M15 18l-6-6 6-6" />
</Svg>
</Pressable>
<Text
style={{
flexShrink: 1,
fontFamily: 'Outfit_600SemiBold',
fontSize: 17,
color: '#141413',
}}>
Events
</Text>
<Pressable
accessibilityRole="button"
accessibilityLabel="Settings"
style={{
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: '#C8F25A',
borderWidth: 2.5,
borderColor: '#141413',
alignItems: 'center',
justifyContent: 'center',
padding: 0,
flexDirection: 'row',
}}>
<Svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="#141413"
strokeWidth="2.2"
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={{
alignItems: 'center',
justifyContent: 'space-between',
marginTop: 6,
flexDirection: 'row',
}}>
<Text
style={{
margin: 0,
flexShrink: 1,
fontFamily: 'Outfit_600SemiBold',
fontSize: 30,
color: '#141413',
letterSpacing: -0.5,
}}>
What is new?
</Text>
<Svg width="40" height="40" viewBox="0 0 40 40">
<Path
d="M8 32 c10 -2 4 -16 14 -20 c6 -2 10 4 10 -6"
fill="none"
stroke="#F26B5B"
strokeWidth="2.5"
strokeLinecap="round"
/>
</Svg>
</View>
<View
style={{
height: 196,
borderRadius: 26,
backgroundColor: '#1F4A35',
borderWidth: 2.5,
borderColor: '#141413',
boxShadow: '0 4px 0 #141413',
padding: 20,
}}>
<Text
style={{
fontFamily: 'Outfit_500Medium',
fontSize: 24,
color: '#F4FBD8',
lineHeight: 26.4,
}}>
Outdoor{'\n'}events
</Text>
<Text
style={{
marginTop: 10,
marginBottom: 0,
marginHorizontal: 0,
maxWidth: 150,
fontFamily: 'Outfit_400Regular',
fontSize: 13,
color: '#C9DDB5',
lineHeight: 17.55,
}}>
Perfect chance to enrich your social circle
</Text>
<View
style={{
position: 'absolute',
left: '50%',
bottom: 16,
transform: [{ translateX: '-50%' }],
gap: 6,
flexDirection: 'row',
}}>
<View style={{ width: 18, height: 6, borderRadius: 3, backgroundColor: '#F26B5B' }} />
<View style={{ width: 6, height: 6, borderRadius: 3, backgroundColor: '#F4FBD8' }} />
<View style={{ width: 6, height: 6, borderRadius: 3, backgroundColor: '#F4FBD8' }} />
<View style={{ width: 6, height: 6, borderRadius: 3, backgroundColor: '#F4FBD8' }} />
</View>
<Svg
viewBox="0 0 80 40"
width="80"
height="40"
style={{ position: 'absolute', right: 96, top: -14 }}>
<Ellipse
cx="40"
cy="20"
rx="30"
ry="11"
fill="#F26B5B"
stroke="#141413"
strokeWidth="2.5"
transform="rotate(-14 40 20)"
/>
</Svg>
<Image
source={{ uri: asset('05d181ed31.webp') }}
contentFit="contain"
accessibilityLabel="Happy terrier leaping through the air"
style={{
position: 'absolute',
right: -22,
top: 6,
width: 200,
height: 152,
transform: [{ rotate: '-12deg' }],
}}
/>
<Svg
viewBox="0 0 20 20"
width="20"
height="20"
style={{ position: 'absolute', right: 18, top: 8 }}>
<Path d="M10 0 l3 7 l7 3 l-7 3 l-3 7 l-3 -7 l-7 -3 l7 -3 Z" fill="#C8F25A" />
</Svg>
</View>
<View
style={{
flex: 1,
borderRadius: 26,
backgroundColor: '#9FC7E6',
borderWidth: 2.5,
borderColor: '#141413',
boxShadow: '0 4px 0 #141413',
overflow: 'hidden',
padding: 20,
justifyContent: 'space-between',
}}>
<Image
source={{ uri: asset('217fada78a.webp') }}
contentFit="cover"
contentPosition={{ left: '70%', top: '30%' }}
accessibilityLabel="Cocker spaniel with ears flying, head out of a car window"
style={{ position: 'absolute', left: 0, top: 0, width: '100%', height: '100%' }}
/>
<Text
style={{
fontFamily: 'Outfit_500Medium',
fontSize: 24,
color: '#141413',
lineHeight: 26.4,
}}>
How does{'\n'}your dog{'\n'}feel?
</Text>
<View style={{ position: 'absolute', right: 18, top: 16, width: 70, height: 70 }}>
<Svg
viewBox="0 0 70 70"
width="70"
height="70"
style={{ position: 'absolute', left: 0, top: 0 }}>
<Path
d="M35 2 l7 10 l12 -4 l1 12 l12 3 l-6 11 l8 9 l-11 5 l1 12 l-12 -1 l-5 11 l-9 -8 l-9 8 l-5 -11 l-12 1 l1 -12 l-11 -5 l8 -9 l-6 -11 l12 -3 l1 -12 l12 4 Z"
fill="#F26B5B"
stroke="#141413"
strokeWidth="2"
/>
</Svg>
<View
style={{
position: 'absolute',
bottom: 0,
right: 0,
left: 0,
top: 0,
alignItems: 'center',
justifyContent: 'center',
transform: [{ rotate: '-12deg' }],
flexDirection: 'row',
}}>
<Text
style={{
fontFamily: 'Outfit_600SemiBold',
fontSize: 11,
color: '#141413',
lineHeight: 12.1,
textAlign: 'center',
flexShrink: 1,
}}>
New
</Text>
<View style={{ flexShrink: 1 }} />
<Text
style={{
fontFamily: 'Outfit_600SemiBold',
fontSize: 11,
color: '#141413',
lineHeight: 12.1,
textAlign: 'center',
flexShrink: 1,
}}>
event!
</Text>
</View>
</View>
<View
style={{ alignItems: 'center', justifyContent: 'space-between', flexDirection: 'row' }}>
<Pressable
accessibilityRole="button"
style={{
width: 140,
height: 48,
borderRadius: 24,
backgroundColor: '#C8F25A',
borderWidth: 2.5,
borderColor: '#141413',
alignItems: 'center',
justifyContent: 'center',
}}>
<Text
style={{
fontFamily: 'Outfit_500Medium',
fontSize: 16,
color: '#141413',
textAlign: 'center',
}}>
Join
</Text>
</Pressable>
<Pressable
accessibilityRole="button"
accessibilityLabel="Open event"
style={{
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: '#1F4A35',
borderWidth: 2.5,
borderColor: '#141413',
alignItems: 'center',
justifyContent: 'center',
padding: 0,
flexDirection: 'row',
}}>
<Svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="#F4FBD8"
strokeWidth="2.4"
strokeLinecap="round"
strokeLinejoin="round">
<Path d="M7 17L17 7M8 7h9v9" />
</Svg>
</Pressable>
</View>
</View>
<View
style={{
height: 64,
borderRadius: 32,
backgroundColor: '#F26B5B',
borderWidth: 2.5,
borderColor: '#141413',
alignItems: 'center',
justifyContent: 'space-around',
flexDirection: 'row',
}}>
<Pressable
accessibilityRole="link"
accessibilityLabel="Home"
onPress={() => router.navigate('/events')}
style={{
width: 44,
height: 44,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
}}>
<Svg width="24" height="24" viewBox="0 0 24 24" fill="#141413">
<Path d="M3 11l9-7 9 7v9a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z" />
</Svg>
</Pressable>
<Pressable
accessibilityRole="link"
accessibilityLabel="Weather"
onPress={() => router.navigate('/events')}
style={{
width: 44,
height: 44,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
}}>
<Svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#141413"
strokeWidth="2"
strokeLinecap="round">
<Path d="M7 18h10a4 4 0 0 0 0-8 6 6 0 0 0-11.5 1.5A3.3 3.3 0 0 0 7 18z" />
</Svg>
</Pressable>
<Pressable
accessibilityRole="link"
accessibilityLabel="Tracking"
onPress={() => router.navigate('/tracking')}
style={{
width: 44,
height: 44,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
}}>
<Svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#141413"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round">
<Path d="M12 21s7-6.2 7-12a7 7 0 0 0-14 0c0 5.8 7 12 7 12z" />
<Circle cx="12" cy="9" r="2.5" />
</Svg>
</Pressable>
<Pressable
accessibilityRole="link"
accessibilityLabel="Profile"
onPress={() => router.navigate('/')}
style={{
width: 44,
height: 44,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
}}>
<Svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#141413"
strokeWidth="2"
strokeLinecap="round">
<Circle cx="12" cy="8" r="4" />
<Path d="M4 21c1.5-4 4.5-6 8-6s6.5 2 8 6" />
</Svg>
</Pressable>
</View>
</View>
</Screen>
);
}src/app/index.tsx
import { Image } from 'expo-image';
import { router } from 'expo-router';
import { Pressable, Text, View } from 'react-native';
import Svg, { Ellipse, G, Path } from 'react-native-svg';
import { Screen } from '@/components/screen';
import { asset } from '@/lib/assets';
const accent = '#F26B5B';
const ground = '#EEF7BC';
export default function MainScreen() {
return (
<Screen background={ground} statusBar="dark">
<View style={{ paddingTop: 64, paddingBottom: 36, paddingHorizontal: 24, flex: 1 }}>
<View style={{ height: 330, marginVertical: 0, marginHorizontal: -8 }}>
<Svg
viewBox="0 0 360 330"
width="360"
height="330"
style={{ position: 'absolute', left: 0, top: 0 }}>
<G transform="rotate(-8 180 165)">
<Ellipse
cx="185"
cy="98"
rx="150"
ry="62"
fill={accent}
stroke="#141413"
strokeWidth="3"
/>
<Ellipse
cx="175"
cy="170"
rx="160"
ry="66"
fill={accent}
stroke="#141413"
strokeWidth="3"
/>
<Ellipse
cx="185"
cy="242"
rx="150"
ry="62"
fill={accent}
stroke="#141413"
strokeWidth="3"
/>
</G>
</Svg>
<Image
source={{ uri: asset('53dc736f77.webp') }}
contentFit="contain"
accessibilityLabel="Black pug in a grey knitted sweater"
style={{
position: 'absolute',
left: 50,
top: 0,
width: 262,
height: 312,
filter: 'grayscale(1) contrast(1.08)',
}}
/>
</View>
<View style={{ gap: 14, marginTop: 18 }}>
<Text
style={{
margin: 0,
fontFamily: 'Outfit_700Bold',
fontSize: 48,
color: '#141413',
letterSpacing: -1.5,
lineHeight: 48.96,
}}>
Dog{'\n'}Assistant
</Text>
<Text
style={{
margin: 0,
maxWidth: 260,
fontFamily: 'Outfit_400Regular',
fontSize: 15,
color: '#2E3A22',
lineHeight: 21,
}}>
Helping owners track their dogs and organise events to meet up for networking
</Text>
</View>
<View style={{ flex: 1 }}>
<Svg
viewBox="0 0 340 150"
width="340"
height="150"
style={{ position: 'absolute', left: 0, top: 4 }}>
<Path
d="M40 10 c-30 30 30 50 0 80 c-20 20 -10 35 10 45"
fill="none"
stroke="#141413"
strokeWidth="2.5"
strokeLinecap="round"
/>
<Path
d="M44 128 l8 9 l-12 2"
fill="none"
stroke="#141413"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path d="M215 70 l5 13 l13 5 l-13 5 l-5 13 l-5 -13 l-13 -5 l13 -5 Z" fill="#1F4A35" />
<Path d="M300 40 l7 18 l18 7 l-18 7 l-7 18 l-7 -18 l-18 -7 l18 -7 Z" fill={accent} />
<Path d="M260 20 l3 8 l8 3 l-8 3 l-3 8 l-3 -8 l-8 -3 l8 -3 Z" fill="#B7D94A" />
<Path d="M190 112 l2 6 l6 2 l-6 2 l-2 6 l-2 -6 l-6 -2 l6 -2 Z" fill={accent} />
<Path d="M250 118 l2 5 l5 2 l-5 2 l-2 5 l-2 -5 l-5 -2 l5 -2 Z" fill="#1F4A35" />
</Svg>
</View>
<Pressable
accessibilityRole="link"
onPress={() => router.navigate('/events')}
style={{
alignItems: 'center',
justifyContent: 'center',
height: 61,
borderRadius: 28,
backgroundColor: '#1F4A35',
borderWidth: 2.5,
borderColor: '#141413',
boxShadow: '0 4px 0 #141413',
flexDirection: 'row',
}}>
<Text
style={{
fontFamily: 'Outfit_500Medium',
fontSize: 17,
color: '#F4FBD8',
flexShrink: 1,
}}>
Let’s go
</Text>
</Pressable>
</View>
</Screen>
);
}src/app/tracking.tsx
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import { router } from 'expo-router';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import Svg, { Circle, Ellipse, Path, Rect } from 'react-native-svg';
import { Screen } from '@/components/screen';
import { asset } from '@/lib/assets';
const accent = '#E85A4A';
const modes = [
{ label: 'Car', time: '15 min', car: true },
{ label: 'Bus', time: '25 min', bus: true },
{ label: 'Bike', time: '40 min', bike: true },
{ label: 'Walk', time: '1 h', walk: true },
];
export default function TrackingScreen() {
return (
<Screen background="#EEF7BC" statusBar="dark">
<View style={{ flex: 1 }}>
<View
style={{
position: 'absolute',
left: 0,
top: 0,
width: 390,
height: 470,
backgroundColor: '#6E7F5C',
}}>
<Image
source={{ uri: asset('5306f632e1.webp') }}
contentFit="cover"
accessibilityLabel="Aerial map of the neighbourhood around Spike"
style={{ position: 'absolute', left: 0, top: -40, width: 390, height: 585 }}
/>
<View style={{ position: 'absolute', left: 0, top: 0, width: 390, height: 130 }}>
<LinearGradient
colors={['rgba(0,0,0,0.35)', 'rgba(0,0,0,0)']}
locations={[0, 1]}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 1 }}
style={StyleSheet.absoluteFill}
/>
</View>
<Svg
viewBox="0 0 390 470"
width="390"
height="470"
style={{ position: 'absolute', left: 0, top: 0 }}>
<Path
d="M58 290 C110 270 130 230 170 236 C210 242 220 200 250 166"
fill="none"
stroke="#141413"
strokeWidth="7"
strokeLinecap="round"
opacity="0.5"
/>
<Path
d="M58 290 C110 270 130 230 170 236 C210 242 220 200 250 166"
fill="none"
stroke="#C8F25A"
strokeWidth="4"
strokeLinecap="round"
/>
</Svg>
<View style={{ position: 'absolute', left: 30, top: 252, width: 54, height: 66 }}>
<Svg viewBox="0 0 54 66" width="54" height="66">
<Ellipse cx="27" cy="60" rx="14" ry="5" fill="#141413" opacity="0.35" />
<Path
d="M27 58 L18 44 A20 20 0 1 1 36 44 Z"
fill="#F4FBD8"
stroke="#141413"
strokeWidth="2.5"
/>
<Circle cx="27" cy="27" r="14" fill="#D9934D" />
<Circle cx="22" cy="25" r="2" fill="#141413" />
<Circle cx="32" cy="25" r="2" fill="#141413" />
<Ellipse cx="27" cy="31" rx="3" ry="2" fill="#141413" />
</Svg>
</View>
<View style={{ position: 'absolute', left: 224, top: 120, width: 54, height: 66 }}>
<Svg viewBox="0 0 54 66" width="54" height="66">
<Ellipse cx="27" cy="60" rx="14" ry="5" fill="#141413" opacity="0.35" />
<Path
d="M27 58 L18 44 A20 20 0 1 1 36 44 Z"
fill="#F26B5B"
stroke="#141413"
strokeWidth="2.5"
/>
<Circle cx="27" cy="27" r="14" fill="#F4FBD8" />
<Circle cx="27" cy="23" r="5" fill="#1F4A35" />
<Path d="M18 36 a9 7 0 0 1 18 0" fill="#1F4A35" />
</Svg>
</View>
</View>
<View
style={{
position: 'absolute',
left: 20,
right: 20,
top: 60,
alignItems: 'center',
justifyContent: 'space-between',
flexDirection: 'row',
}}>
<Pressable
accessibilityRole="link"
accessibilityLabel="Back"
onPress={() => router.navigate('/events')}
style={{
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: '#C8F25A',
borderWidth: 2.5,
borderColor: '#141413',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
}}>
<Svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="#141413"
strokeWidth="2.6"
strokeLinecap="round"
strokeLinejoin="round">
<Path d="M15 18l-6-6 6-6" />
</Svg>
</Pressable>
<Text
style={{
flexShrink: 1,
fontFamily: 'Outfit_600SemiBold',
fontSize: 17,
color: '#FFFFFF',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 4,
textShadowColor: 'rgba(0,0,0,0.5)',
}}>
Dog tracking
</Text>
<Pressable
accessibilityRole="button"
accessibilityLabel="Settings"
style={{
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: '#C8F25A',
borderWidth: 2.5,
borderColor: '#141413',
alignItems: 'center',
justifyContent: 'center',
padding: 0,
flexDirection: 'row',
}}>
<Svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="#141413"
strokeWidth="2.2"
strokeLinecap="round">
<Circle cx="12" cy="12" r="3" />
<Path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M4.9 19.1L7 17M17 7l2.1-2.1" />
</Svg>
</Pressable>
</View>
<View style={{ position: 'absolute', right: 20, top: 270, gap: 10 }}>
<Pressable
accessibilityRole="button"
accessibilityLabel="Zoom in"
style={{
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: '#C8F25A',
borderWidth: 2.5,
borderColor: '#141413',
alignItems: 'center',
justifyContent: 'center',
padding: 0,
flexDirection: 'row',
}}>
<Svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="#141413"
strokeWidth="2.6"
strokeLinecap="round">
<Path d="M12 5v14M5 12h14" />
</Svg>
</Pressable>
<Pressable
accessibilityRole="button"
accessibilityLabel="Zoom out"
style={{
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: '#C8F25A',
borderWidth: 2.5,
borderColor: '#141413',
alignItems: 'center',
justifyContent: 'center',
padding: 0,
flexDirection: 'row',
}}>
<Svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="#141413"
strokeWidth="2.6"
strokeLinecap="round">
<Path d="M5 12h14" />
</Svg>
</Pressable>
</View>
<View style={{ position: 'absolute', left: 12, right: 12, top: 438, bottom: 24, gap: 0 }}>
<View
style={{
backgroundColor: '#EEF7BC',
borderWidth: 2.5,
borderColor: '#141413',
borderTopLeftRadius: 26,
borderTopRightRadius: 26,
borderBottomRightRadius: 0,
borderBottomLeftRadius: 0,
borderBottomWidth: 0,
paddingVertical: 16,
paddingHorizontal: 18,
alignItems: 'center',
gap: 12,
flexDirection: 'row',
}}>
<View
style={{
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: '#F26B5B',
borderWidth: 2,
borderColor: '#141413',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
}}>
<Svg width="30" height="30" viewBox="0 0 30 30">
<Circle cx="15" cy="16" r="10" fill="#D9934D" />
<Path
d="M6 10 C2 12 3 20 7 22 L9 14 Z M24 10 C28 12 27 20 23 22 L21 14 Z"
fill="#A9652A"
/>
<Circle cx="12" cy="15" r="1.6" fill="#141413" />
<Circle cx="18" cy="15" r="1.6" fill="#141413" />
<Ellipse cx="15" cy="19" rx="2.2" ry="1.6" fill="#141413" />
</Svg>
</View>
<View style={{ flex: 1, gap: 2 }}>
<Text style={{ fontFamily: 'Outfit_600SemiBold', fontSize: 16, color: '#141413' }}>
Spike
</Text>
<Text style={{ fontFamily: 'Outfit_400Regular', fontSize: 13, color: '#4A5638' }}>
Downing St, 794
</Text>
</View>
<Text
style={{
flexShrink: 1,
fontFamily: 'Outfit_600SemiBold',
fontSize: 26,
color: '#141413',
letterSpacing: -0.5,
}}>
3.7 km
</Text>
</View>
<View
style={{
flex: 1,
backgroundColor: accent,
borderWidth: 2.5,
borderColor: '#141413',
borderRadius: 26,
padding: 18,
gap: 18,
boxShadow: '0 4px 0 #141413',
marginTop: -10,
}}>
<View
style={{
alignItems: 'center',
justifyContent: 'space-between',
flexDirection: 'row',
}}>
<Text
style={{
margin: 0,
flexShrink: 1,
fontFamily: 'Outfit_500Medium',
fontSize: 26,
color: '#FFFFFF',
}}>
Transports
</Text>
<Pressable
accessibilityRole="button"
style={{
borderWidth: 0,
minHeight: 44,
paddingVertical: 0,
paddingHorizontal: 4,
flexShrink: 1,
alignItems: 'center',
justifyContent: 'center',
}}>
<Text
style={{
fontFamily: 'Outfit_400Regular',
fontSize: 13,
color: '#FFFFFF',
textAlign: 'center',
}}>
Wrap
</Text>
</Pressable>
</View>
<View style={{ flexDirection: 'row', gap: 8 }}>
{modes.map((m, index) => (
<Pressable
key={index}
accessibilityRole="button"
accessibilityLabel={m.label}
style={{ borderWidth: 0, padding: 0, alignItems: 'center', gap: 8, flex: 1 }}>
<View
style={{
width: 63,
height: 63,
borderRadius: 29,
backgroundColor: '#FFFFFF',
borderWidth: 2.5,
borderColor: '#141413',
boxShadow: '0 3px 0 #141413',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
}}>
{m.car && (
<Svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#141413"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round">
<Path d="M5 16V11l2-5h10l2 5v5" />
<Rect x="3" y="11" width="18" height="6" rx="2" />
<Circle cx="7.5" cy="14" r="1" />
<Circle cx="16.5" cy="14" r="1" />
<Path d="M6 17v2M18 17v2" />
</Svg>
)}
{m.bus && (
<Svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#141413"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round">
<Rect x="5" y="3" width="14" height="15" rx="3" />
<Path d="M5 11h14M8 18v2M16 18v2" />
<Circle cx="8.5" cy="14.5" r="1" />
<Circle cx="15.5" cy="14.5" r="1" />
</Svg>
)}
{m.bike && (
<Svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#141413"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round">
<Circle cx="6" cy="16" r="3.5" />
<Circle cx="18" cy="16" r="3.5" />
<Path d="M6 16l4-8h5l3 8M10 8l3 8h-7M14 5h2" />
</Svg>
)}
{m.walk && (
<Svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#141413"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round">
<Circle cx="13" cy="4" r="2" />
<Path d="M9 21l2-6 3 3v3M11 15l1-6 3 3 3 1M12 9l-4 2-1 4" />
</Svg>
)}
</View>
<Text style={{ fontFamily: 'Outfit_400Regular', fontSize: 12, color: '#FFFFFF' }}>
{m.time}
</Text>
</Pressable>
))}
</View>
<View style={{ flex: 1 }} />
<View style={{ flexDirection: 'row', gap: 10 }}>
<Pressable
accessibilityRole="button"
style={{
height: 52,
borderRadius: 26,
backgroundColor: '#C8F25A',
borderWidth: 2.5,
borderColor: '#141413',
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}>
<Text
style={{
fontFamily: 'Outfit_500Medium',
fontSize: 16,
color: '#141413',
textAlign: 'center',
}}>
Find others
</Text>
</Pressable>
<Pressable
accessibilityRole="button"
style={{
height: 52,
borderRadius: 26,
backgroundColor: '#1F4A35',
borderWidth: 2.5,
borderColor: '#141413',
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}>
<Text
style={{
fontFamily: 'Outfit_500Medium',
fontSize: 16,
color: '#F4FBD8',
textAlign: 'center',
}}>
Create event
</Text>
</Pressable>
</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 x 844 artboard, then scaled to the device width.
const ARTBOARD_WIDTH = 390;
const ARTBOARD_HEIGHT = 844;
type Props = {
background: string;
statusBar: 'light' | 'dark';
children: ReactNode;
};
export function Screen({ background, statusBar, 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(ARTBOARD_HEIGHT * scale, height) }}>
<StatusBar style={statusBar} />
<View
style={{
width: ARTBOARD_WIDTH,
height: ARTBOARD_HEIGHT,
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/dog-assistant-app-ui-design/assets';
export const asset = (name: string) => `${ASSET_BASE}/${name}`;About this design
Thick outlines and hard shadows give it plenty of character, and the tracking screen pairs a live route on a map with arrival times for each way of getting there.
Playful neo-brutalism with thick black outlines, hard shadows, lime and coral colour blocking and cut-out dog photography.
- Neo-brutalist outlines and hard shadows
- Map tracking with a live route and pins
- Event cards with cut-out photography
- Transport options with travel times
Colour palette
Pale lime
#EEF7BC
Neon lime
#C8F25A
Coral
#F26B5B
Pine
#1F4A35
Outline
#141413
Sky
#9FC7E6
How to use this design
- 01
Pick what to copy
Copy the Dog Assistant 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
Is the tracking map live?
The map is a static image with an SVG route, so the screen works without API keys. Swap it for react-native-maps when you connect real location data.
Is the Dog Assistant 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.



