A comprehensive template for building Telegram Mini Apps using Nuxt 4, Vue 3, TypeScript, and Tailwind CSS, ready for deployment on Netlify.
- 🔧 Complete Nuxt 4 Setup - Latest Nuxt with TypeScript support
- 📱 Telegram WebApp SDK - Full integration with Telegram Mini App APIs
- 🎨 Tailwind CSS - Utility-first CSS framework with Telegram theme integration
- ⚡ SPA Mode - Optimized for Telegram Mini App deployment
- 🌐 Netlify Ready - Pre-configured for seamless deployment
- TgButton - Telegram-styled buttons with haptic feedback
- TgCell - List cells with navigation and interaction support
- TgContent - Main content wrapper with proper spacing
- TgNav - Bottom navigation bar with up to 4 menu options and icons
- TgSection - Content sections with proper styling
- Hero - Header component for pages
- 🎮 Haptic Feedback - Impact, notification, and selection feedback
- 🔄 Main Button - Configurable main action button
- ⬅️ Back Button - Navigation back button control
- 👤 User Data - Access to Telegram user information
- 🎨 Theme Integration - Automatic Telegram theme colors
- 📐 Viewport Control - Responsive viewport management
- 🔗 Deep Linking - External and Telegram link handling
- 📤 Sharing - Built-in sharing functionality
- ⚡ Vitest - Fast unit testing framework
- 🧪 Component Tests - Comprehensive test coverage
- 🔍 TypeScript - Full type safety
- 📋 Type Definitions - Complete Telegram WebApp types
- Node.js 18+
- npm or yarn or pnpm
Click the "Use this template" button on GitHub or clone the repository:
git clone https://github.com/patricktobias86/nuxt-telegram-mini-app.git my-telegram-app
cd my-telegram-appnpm install
# or
yarn install
# or
pnpm installnpm run devOpen http://localhost:3000 in your browser to see the demo.
npm run generate
├── app/ # Nuxt app directory
│ ├── assets/ # CSS and static assets
│ │ └── css/
│ │ ├── main.css # Global styles
│ │ └── tailwind.css # Tailwind imports
│ ├── components/ # Vue components
│ │ ├── Hero.vue # Hero component
│ │ ├── ErrorBoundary.vue
│ │ └── tg/ # Telegram components
│ │ ├── Button.vue # Telegram button
│ │ ├── Cell.vue # List cell
│ │ ├── Content.vue # Content wrapper
│ │ ├── Nav.vue # Bottom navigation
│ │ └── Section.vue # Content section
│ ├── composables/ # Vue composables
│ │ └── telegram.ts # Telegram SDK integration
│ ├── pages/ # App pages
│ │ ├── index.vue # Home page with SDK demo
│ │ ├── components.vue # Components showcase
│ │ ├── utilities.vue # Utilities demo
│ │ └── functions.vue # Functions page
│ ├── types/ # TypeScript definitions
│ │ └── telegram-webapp.ts
│ └── utils/ # Utility functions
│ └── color.ts # Color conversion utilities
├── server/ # Nuxt server API
│ └── api/
│ └── verify-telegram-data.post.ts # Telegram data verification
├── public/ # Static assets
├── tests/ # Test files
│ └── telegram.spec.ts
├── nuxt.config.ts # Nuxt configuration
├── tailwind.config.ts # Tailwind configuration
└── vitest.config.ts # Test configuration
- Create a new page in
app/pages/:
<!-- app/pages/my-page.vue -->
<template>
<TgContent>
<Hero
title="My Page"
subtitle="Description of my page"
image-src="/img/hero-user.svg"
/>
<TgSection title="My Section" inset>
<TgCell
title="My Cell"
subtitle="Cell description"
icon="i-heroicons-star-20-solid"
/>
</TgSection>
</TgContent>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const goHome = () => router.push('/')
</script><script setup lang="ts">
import {
useMainButton,
useBackButton,
useHapticFeedback,
useInitData,
useThemeParams
} from '~/composables/telegram'
const main = useMainButton()
const back = useBackButton()
const haptic = useHapticFeedback()
const init = useInitData()
const theme = useThemeParams()
// Configure main button
onMounted(() => {
main.mount()
main.setParams({
is_visible: true,
is_active: true,
text: 'My Action'
})
const off = main.onClick(() => {
haptic.impactOccurred('medium')
// Your action here
})
onBeforeUnmounted(() => off?.())
})
// Access user data
const userName = computed(() => init.user.value?.first_name || 'Guest')
// Use theme colors
const bgColor = computed(() => theme.backgroundColor.value)
</script>The template includes pre-built Telegram-styled components:
<!-- Buttons -->
<TgButton title="Primary" status="primary" haptic @click="handleClick" />
<TgButton title="Secondary" status="secondary" haptic="impact-light" />
<TgButton title="Outline" status="outline" />
<TgButton title="Destructive" status="destructive" />
<!-- Cells -->
<TgCell title="Basic Cell" subtitle="With subtitle" />
<TgCell title="With Icon" icon="i-heroicons-star-20-solid" />
<TgCell title="Navigable" to="/target-page" />
<TgCell title="Interactive" @click="handleCellClick" />
<!-- Sections -->
<TgSection title="My Section" inset>
<!-- Section content -->
</TgSection>This template is compatible with Nuxt Layers, allowing you to use it as a reusable layer in other Nuxt projects or extend it with additional layers.
Nuxt Layers provide a way to extend and customize Nuxt applications by sharing configurations, components, pages, composables, and more across multiple projects. Layers can be local directories or published npm packages.
To use this Telegram Mini App template as a layer in another Nuxt project, install it as an NPM package:
-
Install the NPM package:
npm install nuxt-telegram-mini-app # or yarn add nuxt-telegram-mini-app # or pnpm add nuxt-telegram-mini-app
-
Configure your Nuxt project to extend this layer:
// nuxt.config.ts export default defineNuxtConfig({ extends: [ 'nuxt-telegram-mini-app' ], // Your custom config here })
Alternatively, if you prefer to use a local clone or git dependency:
# Clone as a subdirectory git clone https://github.com/patricktobias86/nuxt-telegram-mini-app.git layers/telegram-app # Or add as git dependency in package.json: # "dependencies": { # "nuxt-telegram-mini-app": "github:patricktobias86/nuxt-telegram-mini-app" # }
Then configure:
// nuxt.config.ts export default defineNuxtConfig({ extends: [ './layers/telegram-app' // or 'nuxt-telegram-mini-app' for git dependency ], // Your custom config here })
-
Access layer features in your project:
- Components: Use
<TgButton>,<TgCell>, etc. in your pages - Composables: Import
useMainButton,useHapticFeedback, etc. - Pages: Extend or override existing pages
- Styles: Inherit Tailwind and Telegram theme integration
- Components: Use
This template includes an extends: [] configuration, allowing you to further extend it with additional layers:
// nuxt.config.ts in this template
export default defineNuxtConfig({
extends: [
// Add your custom layers here
// './layers/my-custom-layer',
// 'my-published-layer'
],
// ... rest of config
})The template follows Nuxt's layer conventions:
app/directory contains all extendable contentnuxt.config.tsat root level defines layer configuration- Components, pages, and composables are automatically merged
- Override selectively: Only override what you need to customize
- Maintain compatibility: Keep Telegram SDK integration intact
- Test thoroughly: Ensure Telegram features work in your extended app
- Version control: Pin layer versions for stability
- Build the project:
npm run generate- Deploy the
distfolder to your hosting provider
- This project uses Vue Router history mode to preserve Telegram's
#tgWebAppDatahash. - Netlify requires a SPA redirect so deep links resolve to
index.html. - Included file:
public/_redirectswith/* /index.html 200. - If you deploy elsewhere, add an equivalent history fallback rule.
Set these in your deployment platform:
ENV=productionRun tests with:
# Run tests once
npm run test
# Run tests in watch mode
npm run test:watchThe project includes:
- Unit tests for Telegram composables
- Component tests for UI components
- Integration tests for user flows
The template automatically inherits Telegram's theme colors. Customize in app/assets/css/main.css:
:root {
/* Telegram theme variables are automatically set */
--custom-color: #your-color;
}Extend Tailwind in tailwind.config.ts:
export default {
theme: {
extend: {
colors: {
'custom': '#your-color',
}
}
}
}- Create component in
app/components/ - Follow the existing patterns for styling and props
- Add TypeScript interfaces for props
- Include tests in
tests/
webApp- WebApp instanceisReady- Ready stateisAvailable- Availability check
setParams(params)- Configure buttononClick(callback)- Handle clicksvisible- Visibility state
show()/hide()- Control visibilityonClick(callback)- Handle clicks
impactOccurred(style)- Trigger impactnotificationOccurred(type)- Trigger notificationselectionChanged()- Trigger selection
user- User informationqueryId- Query IDstartParam- Start parameter
backgroundColor- Theme backgroundtextColor- Theme text colorbuttonColor- Theme button color- And more theme colors...
Props-driven styling so you don’t need extra Tailwind classes.
<TgButton
title="Label"
status="primary|secondary|outline|danger|destructive"
size="sm|md|lg"
:block="true"
:loading="false"
:disabled="false"
icon="i-heroicons-star-20-solid"
icon-position="left|right"
elevated
uppercase
haptic="selection|impact-light|impact-medium|impact-heavy|notification-success|notification-warning|notification-error"
@click="handleClick"
/>TgButton props
| Prop | Required | Default | Description |
|---|---|---|---|
title |
yes | — | Button label text |
status |
no | primary |
Visual style variant |
size |
no | md |
Size of the button |
block |
no | true |
Full width when true |
loading |
no | false |
Shows spinner and disables |
disabled |
no | false |
Disables interaction |
icon |
no | — | Icon name for @nuxt/icon |
icon-position |
no | left |
Icon placement relative to text |
elevated |
no | false |
Adds a subtle shadow |
uppercase |
no | false |
Uppercase label |
to |
no | — | Internal route, uses NuxtLink |
href |
no | — | External link, uses <a> |
share-url |
no | — | Triggers Telegram share on click |
haptic |
no | false |
Haptic feedback type or boolean |
Notes:
- Prefer
to(router) for internal navigation to avoid conflicts with bottom Nav. smallis still supported butsizeis preferred.
<TgCell
title="Cell Title"
subtitle="Cell Subtitle"
:description="dynamicDescription"
icon="i-heroicons-star-20-solid"
color="var(--tg-theme-link-color)"
icon-color="#888"
tone="default|secondary"
:border="true"
:clickable="false"
:chevron="undefined|true|false"
to="/navigation-target"
href="https://example.com"
@click="handleClick"
/>TgCell props
| Prop | Required | Default | Description |
|---|---|---|---|
title |
no | '' |
Title text |
subtitle |
no | — | Subtitle text |
description |
no | — | Description text |
icon |
no | — | Icon name for @nuxt/icon |
color |
no | — | Title color override |
icon-color |
no | — | Icon color override |
line-clamp |
no | 0 |
Clamp lines for text (0 = none) |
border |
no | true |
Bottom divider line |
tone |
no | default |
Background tone |
clickable |
no | false |
Hover style even without link |
chevron |
no | auto |
Force chevron visibility |
to |
no | — | Internal route, uses NuxtLink |
href |
no | — | External link, uses <a> |
<TgContent as="main|section|div" />Behavior
- Automatically adds bottom safe-area padding when a
<TgNav>exists on the page. - Default container styles:
max-w-2xl,p-4, andspace-y-6.
TgContent props
| Prop | Required | Default | Description |
|---|---|---|---|
as |
no | main |
Render element |
max-width-class |
no | — | Optional override for container max width |
class |
no | '' |
Extra classes to merge |
<TgSection title="Section" inset tone="default|secondary" :append-border="true" />Behavior
- Rounded corners by default; larger rounding when
inset. - No outer border around the body.
TgSection props
| Prop | Required | Default | Description |
|---|---|---|---|
title |
no | — | Optional section header |
inset |
no | false |
Indented, iOS-like style |
tone |
no | default |
Background tone for body |
append-border |
no | true |
Thin border above append slot |
class |
no | '' |
Extra classes on wrapper |
<TgNav
:items="navItems"
:model-value="activeKey"
tone="default|secondary"
:border="true"
height="12|14"
:safe-area="true"
root-class="custom-nav-class"
@select="handleSelect"
@update:model-value="handleActiveChange"
/>TgNav props
| Prop | Required | Default | Description |
|---|---|---|---|
items |
yes | — | List of items { key, label, icon?, to? } |
model-value |
no | — | Controlled active key |
tone |
no | default |
Background tone |
border |
no | true |
Top border visibility |
height |
no | 14 |
Item height (Tailwind number) |
safe-area |
no | true |
Adds bottom safe area spacer |
root-class |
no | '' |
Extra classes on root nav |
Navigation items structure:
interface TgNavItem {
key: string // Unique identifier
label: string // Display text
icon?: string // Icon name (optional)
to?: string // Route path (optional)
}Routing guidance
- Use
to(router) for internal navigation to avoid conflicts with the fixed<TgNav>. - Reserve
hreffor external links.
Key settings in nuxt.config.ts:
export default defineNuxtConfig({
ssr: false, // SPA mode for Telegram
srcDir: 'app', // App source directory
router: {
options: {
// Use history mode so Telegram's #tgWebAppData is not rewritten to a route
hashMode: false
}
},
modules: [
'@nuxt/icon', // Icon support
'@nuxtjs/tailwindcss', // Tailwind CSS
],
app: {
head: {
script: [{
src: 'https://telegram.org/js/telegram-web-app.js?58',
defer: true
}]
}
}
})The template automatically includes the Telegram WebApp script. The version can be updated in the Nuxt config.
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new features
- Submit a pull request
MIT License - see LICENSE file for details.
- 📖 Documentation: Check this README and inline code comments
- 🐛 Issues: Report bugs via GitHub Issues
- 💡 Discussions: Use GitHub Discussions for questions
- 📚 Telegram Docs: Telegram Mini Apps Documentation
- Nuxt 4 - The Vue.js Framework
- Vue 3 - The Progressive JavaScript Framework
- TypeScript - JavaScript with syntax for types
- Tailwind CSS - A utility-first CSS framework
- Vitest - A blazing fast unit testing framework
- Telegram WebApp API - Telegram Mini Apps Platform
Made with ❤️ using Nuxt 4