Template · Landing pages
SaaS Landing
Complete Vue 3 SaaS landing page template with a drawn product UI, features, pricing toggle, testimonials and FAQ. Tailwind CSS 4, dark mode, typed.
Vue 3 landing page for a mobile app with a CSS-drawn phone mockup, feature highlights, a screenshot strip and download buttons. Tailwind CSS, dark mode.
by Vueplayv1.0.0Updated 2 hours ago
A one-page site for promoting a mobile app: a hero with a phone mockup and download buttons, six feature highlights, a strip of four drawn app screens, a closing download banner and a compact footer.
The phone is a reusable PhoneMockup component drawn entirely with Tailwind. It ships with four example screens and a default slot, so you can drop your own markup (or a real screenshot image) inside the frame. The screenshot strip scrolls horizontally with snap points on phones and becomes a staggered row on desktop.
The download buttons are deliberately generic. When you launch, replace them with the official badges from each app store's marketing guidelines; everything else is plain copy you can edit in place.
<script setup lang="ts">
import AppNav from './components/AppNav.vue'
import AppHero from './components/AppHero.vue'
import FeatureHighlights from './components/FeatureHighlights.vue'
import ScreenshotStrip from './components/ScreenshotStrip.vue'
import DownloadCta from './components/DownloadCta.vue'
import AppFooter from './components/AppFooter.vue'
</script>
<template>
<div class="min-h-screen bg-white text-gray-900 antialiased dark:bg-gray-950 dark:text-white">
<AppNav />
<main>
<AppHero />
<FeatureHighlights />
<ScreenshotStrip />
<DownloadCta />
</main>
<AppFooter />
</div>
</template>
<script setup lang="ts">
const year = new Date().getFullYear()
const links = ['Help', 'Privacy', 'Terms', 'Press', 'Contact']
</script>
<template>
<footer class="border-t border-gray-200 dark:border-white/10">
<div class="mx-auto flex max-w-6xl flex-col items-center gap-6 px-4 py-10 sm:flex-row sm:justify-between sm:px-6">
<p class="text-sm text-gray-500 dark:text-gray-400">© {{ year }} Loopa Apps. A fictional product.</p>
<nav aria-label="Footer">
<ul class="flex flex-wrap justify-center gap-x-6 gap-y-2">
<li v-for="l in links" :key="l">
<a href="#" class="text-sm text-gray-600 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-rose-500 dark:text-gray-400 dark:hover:text-white">{{ l }}</a>
</li>
</ul>
</nav>
</div>
</footer>
</template>
<script setup lang="ts">
import PhoneMockup from './PhoneMockup.vue'
import StoreButtons from './StoreButtons.vue'
</script>
<template>
<section class="relative overflow-hidden">
<div class="mx-auto grid max-w-6xl items-center gap-14 px-4 pb-20 pt-10 sm:px-6 lg:grid-cols-2 lg:pb-28 lg:pt-16">
<div class="text-center lg:text-left">
<p class="inline-flex items-center gap-2 rounded-full bg-rose-100 px-3 py-1 text-xs font-semibold text-rose-700 dark:bg-rose-500/15 dark:text-rose-300">
<span class="size-1.5 rounded-full bg-rose-500" /> Version 3.0 · now with reflections
</p>
<h1 class="mt-5 text-4xl font-extrabold tracking-tight text-balance text-gray-900 sm:text-6xl dark:text-white">
Small habits,
<span class="bg-gradient-to-r from-rose-500 to-orange-400 bg-clip-text text-transparent">kept daily.</span>
</h1>
<p class="mx-auto mt-6 max-w-lg text-lg text-pretty text-gray-600 lg:mx-0 dark:text-gray-300">
Loopa is a calm habit tracker. Check off what matters in one tap, see your streaks grow, and get a gentle nudge only when you need one.
</p>
<div class="mt-8 flex justify-center lg:justify-start">
<StoreButtons />
</div>
<div class="mt-8 flex items-center justify-center gap-4 lg:justify-start">
<div class="flex -space-x-1.5" aria-hidden="true">
<span v-for="(i, idx) in ['KM', 'JD', 'AR', 'LT']" :key="i" class="grid size-9 place-items-center rounded-full text-[11px] font-semibold text-white ring-2 ring-white dark:ring-gray-950" :class="['bg-rose-500', 'bg-amber-500', 'bg-sky-500', 'bg-violet-500'][idx]">{{ i }}</span>
</div>
<p class="text-left text-sm text-gray-600 dark:text-gray-400">
<span class="font-semibold text-gray-900 dark:text-white">4.8 average rating</span><br>from 18,000+ reviews
</p>
</div>
</div>
<div class="relative flex justify-center">
<div aria-hidden="true" class="absolute left-1/2 top-1/2 size-[26rem] -translate-x-1/2 -translate-y-1/2 rounded-full bg-gradient-to-br from-rose-300/60 to-orange-200/60 blur-3xl dark:from-rose-600/25 dark:to-orange-500/15" />
<div aria-hidden="true" class="absolute left-1/2 top-1/2 size-[22rem] -translate-x-1/2 -translate-y-1/2 rounded-full border border-rose-200 sm:size-[30rem] dark:border-rose-500/20" />
<div class="relative w-64 sm:w-72">
<PhoneMockup screen="today" label="Loopa's Today screen: a progress ring and four habits, two of them checked off" />
<!-- floating chips -->
<div aria-hidden="true" class="absolute -left-16 top-24 hidden rounded-2xl bg-white px-3 py-2 shadow-xl ring-1 ring-gray-900/5 sm:block dark:bg-gray-800 dark:ring-white/10">
<p class="text-[10px] font-medium text-gray-500 dark:text-gray-400">Streak</p>
<p class="text-sm font-bold text-gray-900 dark:text-white">12 days in a row</p>
</div>
<div aria-hidden="true" class="absolute -right-14 bottom-28 hidden rounded-2xl bg-white px-3 py-2 shadow-xl ring-1 ring-gray-900/5 sm:block dark:bg-gray-800 dark:ring-white/10">
<p class="text-[10px] font-medium text-gray-500 dark:text-gray-400">This week</p>
<p class="text-sm font-bold text-emerald-600 dark:text-emerald-400">+9% done</p>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, useTemplateRef } from 'vue'
const links = [
{ label: 'Features', href: '#features' },
{ label: 'Screens', href: '#screens' },
{ label: 'Download', href: '#download' },
]
const open = ref(false)
const toggle = useTemplateRef<HTMLButtonElement>('toggle')
function onKeydown(e: KeyboardEvent) {
if (e.key === 'Escape' && open.value) {
open.value = false
toggle.value?.focus()
}
}
onMounted(() => window.addEventListener('keydown', onKeydown))
onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown))
</script>
<template>
<header class="relative z-30">
<nav class="mx-auto flex h-16 max-w-6xl items-center justify-between px-4 sm:px-6" aria-label="Main">
<a href="#" class="flex items-center gap-2 rounded-lg text-lg font-bold tracking-tight text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-rose-500 dark:text-white">
<span class="grid size-8 place-items-center rounded-xl bg-gradient-to-br from-rose-500 to-orange-400 text-white shadow-md shadow-rose-500/30" aria-hidden="true">
<svg viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M7 12a5 5 0 1 0 5-5" /><path d="M12 4v3h3" /></svg>
</span>
Loopa
</a>
<ul class="hidden items-center gap-8 sm:flex">
<li v-for="l in links" :key="l.href">
<a :href="l.href" class="text-sm font-medium text-gray-600 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-rose-500 dark:text-gray-300 dark:hover:text-white">{{ l.label }}</a>
</li>
</ul>
<a href="#download" class="hidden rounded-full bg-gray-900 px-4 py-2 text-sm font-semibold text-white hover:bg-gray-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rose-500 sm:inline-block dark:bg-white dark:text-gray-900 dark:hover:bg-gray-200">Get the app</a>
<button
ref="toggle"
type="button"
class="grid size-10 place-items-center rounded-lg text-gray-700 hover:bg-gray-900/5 focus-visible:outline-2 focus-visible:outline-rose-500 sm:hidden dark:text-gray-200 dark:hover:bg-white/10"
:aria-expanded="open"
aria-controls="app-mobile-menu"
@click="open = !open"
>
<span class="sr-only">{{ open ? 'Close menu' : 'Open menu' }}</span>
<svg viewBox="0 0 24 24" class="size-6" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" aria-hidden="true">
<path :d="open ? 'M6 6l12 12M18 6L6 18' : 'M4 7h16M4 12h16M4 17h16'" />
</svg>
</button>
</nav>
<div v-if="open" id="app-mobile-menu" class="absolute inset-x-4 top-16 rounded-2xl border border-gray-200 bg-white p-3 shadow-xl sm:hidden dark:border-white/10 dark:bg-gray-900">
<a v-for="l in links" :key="l.href" :href="l.href" class="block rounded-lg px-3 py-2.5 font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-white/5" @click="open = false">{{ l.label }}</a>
<a href="#download" class="mt-2 block rounded-lg bg-rose-600 px-3 py-2.5 text-center font-semibold text-white" @click="open = false">Get the app</a>
</div>
</header>
</template>
<script setup lang="ts">
import StoreButtons from './StoreButtons.vue'
</script>
<template>
<section id="download" aria-labelledby="download-title" class="scroll-mt-8 px-4 py-20 sm:px-6 sm:py-28">
<div class="relative mx-auto max-w-5xl overflow-hidden rounded-[2rem] bg-gradient-to-br from-rose-600 to-orange-500 px-6 py-14 text-center sm:px-12 sm:py-16">
<svg aria-hidden="true" viewBox="0 0 400 400" class="absolute -right-24 -top-24 size-96 text-white/10" fill="none" stroke="currentColor" stroke-width="28"><circle cx="200" cy="200" r="150" /></svg>
<svg aria-hidden="true" viewBox="0 0 400 400" class="absolute -bottom-32 -left-20 size-80 text-white/10" fill="none" stroke="currentColor" stroke-width="28"><circle cx="200" cy="200" r="150" /></svg>
<h2 id="download-title" class="relative text-3xl font-bold tracking-tight text-balance text-white sm:text-4xl">Start with one habit today</h2>
<p class="relative mx-auto mt-4 max-w-md text-rose-50">Free to download. Loopa Plus unlocks unlimited habits and sync for $2.99 a month.</p>
<div class="relative mt-8 flex justify-center">
<StoreButtons tone="light" />
</div>
</div>
</section>
</template>
<script setup lang="ts">
const features = [
{ title: 'One-tap check-ins', body: 'Mark a habit done from the home screen widget or your watch. No app to open, no form to fill.', icon: 'M5 12.5l4.5 4.5L19 7.5' },
{ title: 'Streaks that forgive', body: 'Set rest days and a weekly target instead of all-or-nothing. Miss a day and your streak bends, not breaks.', icon: 'M12 3c1 3.5 5 5.5 5 10.5A5 5 0 0 1 7 13.5c0-2 1-3.5 2-4.5 0 2 1 3 2 3 0-3-1-5 1-9z' },
{ title: 'Gentle reminders', body: 'Nudges arrive at the time you usually succeed, learned from your own history. Quiet hours are respected.', icon: 'M6 16V11a6 6 0 1 1 12 0v5l2 2H4l2-2zM10 21h4' },
{ title: 'Weekly insights', body: 'A short Sunday summary shows what worked, what slipped and one small suggestion for next week.', icon: 'M4 20V10M10 20V4M16 20v-7M22 20H2' },
{ title: 'Private journal', body: 'Add a mood and a line of reflection to any day. Entries stay on your device unless you turn on sync.', icon: 'M5 4h11l3 3v13H5V4zM9 9h6M9 13h6M9 17h3' },
{ title: 'Works offline', body: 'Everything is stored locally first and syncs when you are back online. No account required to start.', icon: 'M3 12a9 9 0 0 1 15-6.7M21 12a9 9 0 0 1-15 6.7M3 3l18 18' },
]
</script>
<template>
<section id="features" aria-labelledby="features-title" class="scroll-mt-8 py-20 sm:py-28">
<div class="mx-auto max-w-6xl px-4 sm:px-6">
<div class="mx-auto max-w-2xl text-center">
<p class="text-sm font-semibold text-rose-600 dark:text-rose-400">Why Loopa</p>
<h2 id="features-title" class="mt-2 text-3xl font-bold tracking-tight text-balance text-gray-900 sm:text-4xl dark:text-white">Designed for the days you don't feel like it</h2>
<p class="mt-4 text-lg text-gray-600 dark:text-gray-400">Most habit apps reward perfect weeks. Loopa is built for real ones.</p>
</div>
<ul class="mt-14 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<li
v-for="f in features"
:key="f.title"
class="rounded-3xl border border-gray-200 bg-white p-7 transition hover:-translate-y-0.5 hover:shadow-lg hover:shadow-rose-500/5 dark:border-white/10 dark:bg-white/[0.03]"
>
<span class="grid size-11 place-items-center rounded-2xl bg-gradient-to-br from-rose-500 to-orange-400 text-white shadow-md shadow-rose-500/25">
<svg viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path :d="f.icon" /></svg>
</span>
<h3 class="mt-5 font-semibold text-gray-900 dark:text-white">{{ f.title }}</h3>
<p class="mt-2 text-sm leading-6 text-gray-600 dark:text-gray-400">{{ f.body }}</p>
</li>
</ul>
</div>
</section>
</template>
<script setup lang="ts">
// A phone drawn with CSS. Pick one of the built-in screens, or pass your own via the default slot.
withDefaults(defineProps<{
screen?: 'today' | 'stats' | 'streak' | 'journal'
/** Accessible description of what the screen shows. */
label?: string
}>(), { screen: 'today', label: 'App screenshot' })
const habits = [
{ name: 'Morning walk', meta: '20 min', done: true, color: 'bg-rose-500' },
{ name: 'Read 10 pages', meta: '10 / 10', done: true, color: 'bg-amber-500' },
{ name: 'Drink water', meta: '5 / 8 glasses', done: false, color: 'bg-sky-500' },
{ name: 'No phone after 10pm', meta: 'Evening', done: false, color: 'bg-violet-500' },
]
const week = [62, 80, 45, 90, 100, 72, 88]
const days = ['M', 'T', 'W', 'T', 'F', 'S', 'S']
</script>
<template>
<div role="img" :aria-label="label" class="relative mx-auto aspect-[9/19] w-full max-w-[280px] rounded-[2.75rem] bg-gray-900 p-2.5 shadow-2xl shadow-rose-900/20 ring-1 ring-gray-900/10 dark:bg-gray-800 dark:shadow-black/50 dark:ring-white/10">
<!-- side buttons -->
<span aria-hidden="true" class="absolute -left-0.5 top-24 h-10 w-1 rounded-l bg-gray-700" />
<span aria-hidden="true" class="absolute -right-0.5 top-32 h-16 w-1 rounded-r bg-gray-700" />
<div class="relative flex h-full flex-col overflow-hidden rounded-[2.2rem] bg-rose-50 text-gray-900 dark:bg-gray-950 dark:text-white">
<!-- status bar + island -->
<div class="flex items-center justify-between px-6 pt-3 text-[10px] font-semibold">
<span>9:41</span>
<span class="absolute left-1/2 top-2 h-5 w-20 -translate-x-1/2 rounded-full bg-gray-900 dark:bg-black" />
<span class="flex items-center gap-1">
<span class="flex items-end gap-px"><span class="h-1 w-0.5 rounded-sm bg-current" /><span class="h-1.5 w-0.5 rounded-sm bg-current" /><span class="h-2 w-0.5 rounded-sm bg-current" /></span>
<span class="h-2 w-4 rounded-sm border border-current p-px"><span class="block h-full w-2/3 rounded-[1px] bg-current" /></span>
</span>
</div>
<slot>
<!-- TODAY -->
<div v-if="screen === 'today'" class="flex flex-1 flex-col px-4 pt-6">
<p class="text-[10px] font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Tuesday, 14 May</p>
<p class="mt-0.5 text-lg font-bold">Good morning, Sam</p>
<div class="mt-4 flex items-center gap-3 rounded-2xl bg-gradient-to-br from-rose-500 to-orange-400 p-3 text-white shadow-lg shadow-rose-500/30">
<svg viewBox="0 0 36 36" class="size-12 -rotate-90"><circle cx="18" cy="18" r="15" fill="none" stroke="white" stroke-opacity=".3" stroke-width="4" /><circle cx="18" cy="18" r="15" fill="none" stroke="white" stroke-width="4" stroke-linecap="round" stroke-dasharray="94.2" stroke-dashoffset="47" /></svg>
<div>
<p class="text-xs font-semibold">2 of 4 done</p>
<p class="text-[10px] opacity-90">Keep your 12-day streak going</p>
</div>
</div>
<div class="mt-4 space-y-2">
<div v-for="h in habits" :key="h.name" class="flex items-center gap-2.5 rounded-xl bg-white p-2.5 shadow-sm dark:bg-white/5">
<span class="size-2 rounded-full" :class="h.color" />
<div class="min-w-0 flex-1">
<p class="truncate text-[11px] font-semibold" :class="h.done ? 'text-gray-400 line-through dark:text-gray-500' : ''">{{ h.name }}</p>
<p class="text-[9px] text-gray-500 dark:text-gray-400">{{ h.meta }}</p>
</div>
<span class="grid size-5 place-items-center rounded-full" :class="h.done ? 'bg-rose-500 text-white' : 'border border-gray-300 dark:border-white/20'">
<svg v-if="h.done" viewBox="0 0 20 20" class="size-3" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 10.5l3.5 3.5L15 7" /></svg>
</span>
</div>
</div>
</div>
<!-- STATS -->
<div v-else-if="screen === 'stats'" class="flex flex-1 flex-col px-4 pt-6">
<p class="text-lg font-bold">This week</p>
<p class="text-[10px] text-gray-500 dark:text-gray-400">77% completion, up 9% from last week</p>
<div class="mt-4 rounded-2xl bg-white p-3 shadow-sm dark:bg-white/5">
<div class="flex h-28 items-end justify-between gap-1.5">
<div v-for="(v, i) in week" :key="i" class="flex h-full flex-1 flex-col items-center gap-1"><div class="flex w-full flex-1 items-end">
<div class="w-full rounded-md" :class="i === 4 ? 'bg-rose-500' : 'bg-rose-200 dark:bg-rose-500/30'" :style="{ height: `${v}%` }" /></div>
<span class="text-[8px] text-gray-400">{{ days[i] }}</span>
</div>
</div>
</div>
<div class="mt-3 grid grid-cols-2 gap-2">
<div class="rounded-xl bg-white p-2.5 shadow-sm dark:bg-white/5"><p class="text-[9px] text-gray-500 dark:text-gray-400">Best streak</p><p class="text-base font-bold">31 days</p></div>
<div class="rounded-xl bg-white p-2.5 shadow-sm dark:bg-white/5"><p class="text-[9px] text-gray-500 dark:text-gray-400">Habits done</p><p class="text-base font-bold">142</p></div>
<div class="col-span-2 rounded-xl bg-white p-2.5 shadow-sm dark:bg-white/5">
<p class="text-[9px] text-gray-500 dark:text-gray-400">Most consistent</p>
<p class="text-xs font-semibold">Morning walk · 6 of 7 days</p>
</div>
</div>
</div>
<!-- STREAK -->
<div v-else-if="screen === 'streak'" class="flex flex-1 flex-col items-center px-4 pt-10 text-center">
<div class="grid size-24 place-items-center rounded-full bg-gradient-to-br from-rose-500 to-orange-400 shadow-xl shadow-rose-500/40">
<svg viewBox="0 0 24 24" class="size-12 text-white" fill="currentColor"><path d="M12 2c1 3.5 5 5.5 5 10.5A5 5 0 0 1 7 12.5c0-2 1-3.5 2-4.5 0 2 1 3 2 3 0-3-1-5 1-9z" /></svg>
</div>
<p class="mt-5 text-3xl font-extrabold">12 days</p>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Your longest streak this year</p>
<div class="mt-5 grid w-full grid-cols-7 gap-1.5">
<span v-for="n in 28" :key="n" class="aspect-square rounded-md" :class="n > 16 ? 'bg-rose-500' : n % 5 === 0 ? 'bg-gray-200 dark:bg-white/10' : 'bg-rose-200 dark:bg-rose-500/30'" />
</div>
<span class="mt-5 rounded-full bg-gray-900 px-4 py-2 text-[11px] font-semibold text-white dark:bg-white dark:text-gray-900">Share milestone</span>
</div>
<!-- JOURNAL -->
<div v-else class="flex flex-1 flex-col px-4 pt-6">
<p class="text-lg font-bold">Reflect</p>
<p class="text-[10px] text-gray-500 dark:text-gray-400">How did today feel?</p>
<div class="mt-4 flex justify-between">
<span v-for="(m, i) in ['bg-gray-200 dark:bg-white/10', 'bg-gray-200 dark:bg-white/10', 'bg-amber-300', 'bg-rose-400 ring-4 ring-rose-200 dark:ring-rose-500/30', 'bg-gray-200 dark:bg-white/10']" :key="i" class="size-8 rounded-full" :class="m" />
</div>
<div class="mt-5 flex-1 space-y-2 rounded-2xl bg-white p-3 shadow-sm dark:bg-white/5">
<p class="text-[11px] font-semibold">Tuesday notes</p>
<p class="text-[10px] leading-4 text-gray-600 dark:text-gray-300">Walked before work and it made the whole morning calmer. Skipped the water goal again, try a bottle on the desk.</p>
<div class="flex gap-1 pt-1">
<span class="rounded-full bg-rose-100 px-2 py-0.5 text-[9px] font-medium text-rose-700 dark:bg-rose-500/15 dark:text-rose-300">calm</span>
<span class="rounded-full bg-amber-100 px-2 py-0.5 text-[9px] font-medium text-amber-700 dark:bg-amber-500/15 dark:text-amber-300">focused</span>
</div>
</div>
<div class="h-6" />
</div>
</slot>
<!-- tab bar -->
<div class="mt-auto flex items-center justify-around border-t border-gray-200/70 bg-white/80 px-4 pb-4 pt-2.5 dark:border-white/10 dark:bg-white/5">
<span v-for="n in 4" :key="n" class="size-5 rounded-md" :class="n === 1 ? 'bg-rose-500' : 'bg-gray-200 dark:bg-white/15'" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import PhoneMockup from './PhoneMockup.vue'
const shots = [
{ screen: 'today', caption: 'Today at a glance', label: 'Today screen with a progress ring and habit checklist' },
{ screen: 'stats', caption: 'Honest weekly stats', label: 'Stats screen with a bar chart of daily completion' },
{ screen: 'streak', caption: 'Streaks worth sharing', label: 'Streak screen showing a 12 day streak and a calendar grid' },
{ screen: 'journal', caption: 'A line a day', label: 'Journal screen with a mood picker and a short note' },
] as const
</script>
<template>
<section id="screens" aria-labelledby="screens-title" class="scroll-mt-8 overflow-hidden bg-gradient-to-b from-rose-50 to-white py-20 sm:py-28 dark:from-rose-950/20 dark:to-gray-950">
<div class="mx-auto max-w-6xl px-4 sm:px-6">
<div class="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-end">
<div>
<p class="text-sm font-semibold text-rose-600 dark:text-rose-400">Take a look</p>
<h2 id="screens-title" class="mt-2 text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl dark:text-white">Four screens. That's the whole app.</h2>
</div>
<p class="max-w-sm text-gray-600 dark:text-gray-400">No feeds, no badges to grind. Just the tools that help a habit stick.</p>
</div>
</div>
<!-- Horizontally scrollable on small screens, a neat row on large ones -->
<ul
class="mx-auto mt-12 flex max-w-6xl snap-x snap-mandatory gap-6 overflow-x-auto px-4 pb-6 sm:px-6 lg:grid lg:grid-cols-4 lg:overflow-visible"
aria-label="App screenshots"
tabindex="0"
>
<li v-for="(s, i) in shots" :key="s.screen" class="w-56 shrink-0 snap-center lg:w-auto" :class="i % 2 === 1 ? 'lg:translate-y-8' : ''">
<figure>
<PhoneMockup :screen="s.screen" :label="s.label" />
<figcaption class="mt-5 text-center text-sm font-semibold text-gray-700 dark:text-gray-200">{{ s.caption }}</figcaption>
</figure>
</li>
</ul>
</section>
</template>
<script setup lang="ts">
// Generic download buttons. Swap in the official store badges from each store's
// marketing guidelines when you ship; these are intentionally unbranded.
withDefaults(defineProps<{ tone?: 'solid' | 'light' }>(), { tone: 'solid' })
</script>
<template>
<div class="flex flex-wrap gap-3">
<a
v-for="store in [
{ top: 'Download for', name: 'iPhone & iPad', icon: 'M8 2.5h8A1.5 1.5 0 0 1 17.5 4v16a1.5 1.5 0 0 1-1.5 1.5H8A1.5 1.5 0 0 1 6.5 20V4A1.5 1.5 0 0 1 8 2.5zM11 18.5h2' },
{ top: 'Get it for', name: 'Android', icon: 'M12 3v12M7 10l5 5 5-5M5 20h14' },
]"
:key="store.name"
href="#"
class="inline-flex min-w-40 items-center gap-3 rounded-xl px-4 py-2.5 transition focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rose-500"
:class="tone === 'solid'
? 'bg-gray-900 text-white hover:bg-gray-700 dark:bg-white dark:text-gray-900 dark:hover:bg-gray-200'
: 'bg-white text-gray-900 hover:bg-rose-50'"
>
<svg viewBox="0 0 24 24" class="size-7 shrink-0" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path :d="store.icon" /></svg>
<span class="text-left leading-tight">
<span class="block text-[11px] opacity-75">{{ store.top }}</span>
<span class="block text-base font-semibold">{{ store.name }}</span>
</span>
</a>
</div>
</template>
Template · Landing pages
Complete Vue 3 SaaS landing page template with a drawn product UI, features, pricing toggle, testimonials and FAQ. Tailwind CSS 4, dark mode, typed.
Template · Landing pages
Arctic is a sleek and minimal Vue Play template inspired by the calm and clarity of icy landscapes. With clean layouts, cool tones, and a modern design system, it’s perfect for projects that need a fresh, elegant, and professional look. Whether you’re building portfolios, dashboards, or landing pages, Arctic provides a crisp foundation that puts your content in focus.
Template · Error pages
Friendly Vue 3 404 page with a drawn compass illustration, live search suggestions and helpful links, plus a compact 500 error variant. Tailwind CSS.
Template · Dashboards
A complete Vue 3 admin dashboard template with a collapsible sidebar, hand-drawn SVG charts, a sortable customers table and settings. Tailwind CSS, dark mode.