Tutorial

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.

LineBot.pro Team16 min read
LIFF App Development: LINE Front-end Framework Tutorial with React & Next.js 2025

#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 CaseExampleBenefits
E-commerceProduct catalog, checkoutNo app download needed
ReservationsRestaurant booking, appointmentsQuick access via LINE
MembershipLoyalty cards, point systemsAuto-authenticated
FormsSurveys, registrationsRich UX, pre-filled data
GamesMini-games, quizzesViral sharing features

Learn more about LINE app development services.

#Project Setup

#Step 1: Create LIFF App in LINE Console

  1. Go to LINE Developers Console
  2. Select your provider and channel
  3. Navigate to LIFF tab
  4. Click "Add LIFF app"
  5. 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

bash
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:

typescript
// 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

tsx
// 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

tsx
// 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

typescript
// 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:

typescript
// 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

typescript
// 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:

typescript
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:

typescript
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

css
/* 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:

typescript
// 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

typescript
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

bash
# Install Vercel CLI
npm install -g vercel

# Deploy
vercel --prod

#Environment Variables

Set in Vercel dashboard or .env.local:

env
NEXT_PUBLIC_LIFF_ID=your-liff-id

#Update LIFF Endpoint URL

After deployment:

  1. Go to LINE Developers Console
  2. Navigate to your LIFF app
  3. Update Endpoint URL to your Vercel domain
  4. 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:

LineBot.pro

Ready to Automate Your LINE Business?

Start automating your LINE communications with LineBot.pro today.