Tutorial

LIFF App Development Guide 2026: LINE Front-end Framework with React, Next.js & TypeScript

Build LIFF (LINE Front-end Framework) apps that run inside LINE — login, profile, shareTargetPicker, openWindow external, and the HTTPS endpoint requirement. React + Next.js code, updated for LIFF v2.24+ in 2026.

LineBot.pro Team16 min read
LIFF App Development Guide 2026: LINE Front-end Framework with React, Next.js & TypeScript

#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.