LIFF App Development: LINE Front-end Framework Tutorial with React & Next.js 2025
Complete guide to building LIFF (LINE Front-end Framework) apps. Create web applications that run seamlessly inside LINE with user authentication, profile access, and messaging capabilities. React and Next.js examples included.

#What is LIFF?
LIFF (LINE Front-end Framework) is a platform for building web applications that run inside the LINE app. LIFF apps provide a native app-like experience while using standard web technologies.
#Key Features
- Seamless Authentication: Access user's LINE profile without login forms
- Message Sending: Send messages to chats directly from your app
- Share Target Picker: Let users share content to friends and groups
- QR Code Scanner: Built-in barcode/QR scanning capability
- Native Feel: Full-screen experience inside LINE
#Use Cases
| Use Case | Example | Benefits |
|---|---|---|
| E-commerce | Product catalog, checkout | No app download needed |
| Reservations | Restaurant booking, appointments | Quick access via LINE |
| Membership | Loyalty cards, point systems | Auto-authenticated |
| Forms | Surveys, registrations | Rich UX, pre-filled data |
| Games | Mini-games, quizzes | Viral sharing features |
Learn more about LINE app development services.
#Project Setup
#Step 1: Create LIFF App in LINE Console
- Go to LINE Developers Console
- Select your provider and channel
- Navigate to LIFF tab
- Click "Add LIFF app"
- Configure:
- LIFF app name: Your app name
- Size: Full, Tall, or Compact
- Endpoint URL: Your app URL (must be HTTPS)
- Scopes: profile, openid, chat_message.write
#Step 2: Set Up React/Next.js Project
npx create-next-app@latest my-liff-app --typescript
cd my-liff-app
npm install @line/liff
#Step 3: Initialize LIFF SDK
Create a LIFF utility file:
// lib/liff.ts
import liff from '@line/liff';
const LIFF_ID = process.env.NEXT_PUBLIC_LIFF_ID!;
export async function initLiff() {
try {
await liff.init({ liffId: LIFF_ID });
console.log('LIFF initialized successfully');
return true;
} catch (error) {
console.error('LIFF initialization failed:', error);
return false;
}
}
export function isLoggedIn() {
return liff.isLoggedIn();
}
export function login() {
liff.login();
}
export function logout() {
liff.logout();
}
export async function getProfile() {
if (!liff.isLoggedIn()) return null;
return liff.getProfile();
}
export function isInClient() {
return liff.isInClient();
}
export { liff };
#Step 4: Create LIFF Provider Component
// components/LiffProvider.tsx
'use client';
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { initLiff, liff } from '@/lib/liff';
interface LiffContextType {
isReady: boolean;
isLoggedIn: boolean;
isInClient: boolean;
profile: {
userId: string;
displayName: string;
pictureUrl?: string;
} | null;
login: () => void;
logout: () => void;
}
const LiffContext = createContext<LiffContextType | null>(null);
export function LiffProvider({ children }: { children: ReactNode }) {
const [isReady, setIsReady] = useState(false);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [isInClient, setIsInClient] = useState(false);
const [profile, setProfile] = useState<LiffContextType['profile']>(null);
useEffect(() => {
async function init() {
const success = await initLiff();
if (success) {
setIsReady(true);
setIsLoggedIn(liff.isLoggedIn());
setIsInClient(liff.isInClient());
if (liff.isLoggedIn()) {
const userProfile = await liff.getProfile();
setProfile(userProfile);
}
}
}
init();
}, []);
const value = {
isReady,
isLoggedIn,
isInClient,
profile,
login: () => liff.login(),
logout: () => {
liff.logout();
setIsLoggedIn(false);
setProfile(null);
}
};
return (
<LiffContext.Provider value={value}>
{children}
</LiffContext.Provider>
);
}
export function useLiff() {
const context = useContext(LiffContext);
if (!context) {
throw new Error('useLiff must be used within LiffProvider');
}
return context;
}
#User Authentication
#Getting User Profile
// components/UserProfile.tsx
'use client';
import { useLiff } from './LiffProvider';
import Image from 'next/image';
export function UserProfile() {
const { isReady, isLoggedIn, profile, login, logout } = useLiff();
if (!isReady) {
return <div>Loading...</div>;
}
if (!isLoggedIn) {
return (
<button onClick={login} className="btn-primary">
Login with LINE
</button>
);
}
return (
<div className="flex items-center gap-4">
{profile?.pictureUrl && (
<Image
src={profile.pictureUrl}
alt={profile.displayName}
width={48}
height={48}
className="rounded-full"
/>
)}
<div>
<p className="font-semibold">{profile?.displayName}</p>
<button onClick={logout} className="text-sm text-gray-500">
Logout
</button>
</div>
</div>
);
}
#Access Token for Backend Verification
// Get access token for API calls
const accessToken = liff.getAccessToken();
// Send to your backend
const response = await fetch('/api/verify-user', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
});
Backend verification:
// pages/api/verify-user.ts
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const token = req.headers.authorization?.replace('Bearer ', '');
// Verify token with LINE
const verifyResponse = await fetch('https://api.line.me/oauth2/v2.1/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ access_token: token! })
});
if (!verifyResponse.ok) {
return res.status(401).json({ error: 'Invalid token' });
}
const tokenInfo = await verifyResponse.json();
// tokenInfo contains: scope, expires_in, client_id
res.json({ valid: true, ...tokenInfo });
}
#Messaging Integration
#Sending Messages
// Send message to current chat
async function sendMessage(text: string) {
if (!liff.isInClient()) {
alert('This feature only works inside LINE');
return;
}
await liff.sendMessages([
{
type: 'text',
text: text
}
]);
}
// Send Flex Message
async function sendFlexMessage() {
await liff.sendMessages([
{
type: 'flex',
altText: 'Order Confirmation',
contents: {
type: 'bubble',
body: {
type: 'box',
layout: 'vertical',
contents: [
{
type: 'text',
text: 'Order Confirmed!',
weight: 'bold',
size: 'xl'
},
{
type: 'text',
text: 'Order #12345',
color: '#06C755'
}
]
}
}
}
]);
}
#Share Target Picker
Let users share content to friends or groups:
async function shareToFriends() {
if (!liff.isApiAvailable('shareTargetPicker')) {
alert('Share feature not available');
return;
}
const result = await liff.shareTargetPicker([
{
type: 'flex',
altText: 'Check out this product!',
contents: {
type: 'bubble',
hero: {
type: 'image',
url: 'https://example.com/product.jpg',
size: 'full',
aspectRatio: '20:13'
},
body: {
type: 'box',
layout: 'vertical',
contents: [
{ type: 'text', text: 'Amazing Product', weight: 'bold' },
{ type: 'text', text: '฿999', color: '#06C755' }
]
},
footer: {
type: 'box',
layout: 'vertical',
contents: [
{
type: 'button',
action: {
type: 'uri',
label: 'View Product',
uri: 'https://liff.line.me/your-liff-id/product/123'
},
style: 'primary',
color: '#06C755'
}
]
}
}
}
]);
if (result) {
console.log('Shared successfully');
}
}
#Best Practices
#1. Handle External Browser
LIFF apps can be opened in external browsers. Handle this gracefully:
function App() {
const { isReady, isInClient, isLoggedIn, login } = useLiff();
if (!isReady) return <LoadingScreen />;
// If not in LINE and not logged in, show login button
if (!isInClient && !isLoggedIn) {
return (
<div className="text-center p-8">
<h1>Welcome!</h1>
<p>Login with LINE to continue</p>
<button onClick={login} className="btn-primary">
Login with LINE
</button>
</div>
);
}
return <MainApp />;
}
#2. Responsive Design for LIFF Sizes
/* Compact: ~50% of screen height */
/* Tall: ~75% of screen height */
/* Full: 100% of screen */
.liff-container {
min-height: 100vh;
min-height: 100dvh; /* Dynamic viewport height */
}
/* Safe area for notched devices */
.liff-content {
padding-bottom: env(safe-area-inset-bottom);
}
#3. Deep Linking
Create shareable URLs that open specific pages:
// LIFF URL format: https://liff.line.me/{liffId}/{path}
const productUrl = `https://liff.line.me/${LIFF_ID}/product/${productId}`;
// In your app, handle the path
// pages/product/[id].tsx
export default function ProductPage({ params }: { params: { id: string } }) {
// Access product ID from URL
const productId = params.id;
// ...
}
#4. Error Handling
async function safeLiffOperation<T>(
operation: () => Promise<T>,
fallback: T
): Promise<T> {
try {
if (!liff.isInClient()) {
console.warn('Operation only available in LINE');
return fallback;
}
return await operation();
} catch (error) {
console.error('LIFF operation failed:', error);
return fallback;
}
}
// Usage
const result = await safeLiffOperation(
() => liff.sendMessages([{ type: 'text', text: 'Hello' }]),
undefined
);
#Deployment
#Deploy to Vercel
# Install Vercel CLI
npm install -g vercel
# Deploy
vercel --prod
#Environment Variables
Set in Vercel dashboard or .env.local:
NEXT_PUBLIC_LIFF_ID=your-liff-id
#Update LIFF Endpoint URL
After deployment:
- Go to LINE Developers Console
- Navigate to your LIFF app
- Update Endpoint URL to your Vercel domain
- Save changes
#Testing Checklist
- Test in LINE app (iOS and Android)
- Test in external browser
- Test login/logout flow
- Test message sending (if applicable)
- Test share target picker (if applicable)
- Test on different screen sizes
- Test error scenarios
#Conclusion
LIFF enables powerful integrations between your web apps and LINE. Key takeaways:
- Initialize LIFF early in your app lifecycle
- Handle both in-client and external browser scenarios
- Use Share Target Picker for viral features
- Always verify access tokens on your backend
- Test thoroughly on actual devices
Ready to build your LIFF app?
Try LineBot.pro for rapid LIFF development with our templates and tools. Build LINE mini-apps faster with our pre-built components and infrastructure.
Related resources:
Related Services
Ready to Automate Your LINE Business?
Start automating your LINE communications with LineBot.pro today.