Integrate n8n Chat Widget into Next.js (Production-Ready)

Step-by-step guide to load the n8n chat widget in Next.js—avoid hydration warnings, secure env vars, and inject a…

·Updated on:·Matija Žiberna·
Integrate n8n Chat Widget into Next.js (Production-Ready)

⚡ Next.js Implementation Guides

In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.

No spam. Unsubscribe anytime.

Building a custom chatbot UI from scratch is fun, but sometimes you just need something that works immediately and connects seamlessly to your automation workflows. I recently needed to replace a legacy chatbot in a Next.js application with the native n8n chat widget, but I ran into a few specific challenges: proper lifecycle management in React, handling environment variables securely, and—most annoyingly—injecting a custom brand logo into a header that doesn't natively support it.

Here is the robust, production-ready setup I developed to solve these issues.

1. Installation

First, pull in the official package. We're using the specific @n8n/chat package which provides the lightweight loader.

pnpm add @n8n/chat
# or
npm install @n8n/chat

2. The Chat Component

Instead of dropping a <script> tag into your layout.tsx (which causes hydration warnings) or using a plain useEffect that might fire twice in development, we'll build a dedicated client component.

This component handles:

  1. Initialization: Loading the widget only once after the component mounts.
  2. Configuration: Passing the webhook URL securely.
  3. Dynamic Styling: Using CSS variables to inject a custom logo into the chat header—something the default config object doesn't support.
// File: src/components/N8nChat.tsx
'use client';

import { useEffect, useRef } from 'react';
import '@n8n/chat/dist/style.css';
import { createChat } from '@n8n/chat';

// Import your logo asset (Next.js handles the path)
import brandLogo from '@/assets/logo.png';

export const N8nChat = () => {
    // defined to prevent double initialization in Strict Mode
    const initialized = useRef(false);

    useEffect(() => {
        if (initialized.current) return;
        initialized.current = true;

        createChat({
            webhookUrl: process.env.NEXT_PUBLIC_N8N_CHAT_WEBHOOK_URL!,
            mode: 'window',
            target: '#n8n-chat',
            showWelcomeScreen: true,
            initialMessages: [
                'Hi there! 👋',
                'How can I help you today?'
            ],
            i18n: {
                en: {
                    title: 'Support Chat', // We'll hide this with CSS to show the logo
                    subtitle: 'Ask us anything',
                },
            },
        });
    }, []);

    return (
        <>
            {/* Global overrides for the chat widget */}
            <style jsx global>{`
                :root {
                    /* Brand Colors */
                    --chat--color-primary: #003882;
                    --chat--color-secondary: #008ccc;
                    --chat--color-light: #ffffff;
                    --chat--color-dark: #1f2937;
                    
                     /* Component Colors */
                    --chat--header--background: var(--chat--color-primary);
                    --chat--message--user--background: var(--chat--color-primary);
                    --chat--message--bot--background: #f2f4f7;
                }

                /* 
                 * HACK: Inject the logo into the header 
                 * The widget doesn't accept an image for the title, so we:
                 * 1. Target the header title element
                 * 2. Inject a ::before pseudo-element
                 * 3. Use a CSS variable for the image URL
                 */
                .chat-header h1 {
                    display: flex;
                    align-items: center;
                }
                
                .chat-header h1::before {
                    content: '';
                    display: block;
                    width: 120px; /* Adjust based on your logo aspect ratio */
                    height: 50px;
                    min-width: 120px;
                    margin-right: 12px;
                    
                    /* The variable is defined in the inline style below */
                    background-image: var(--chat--header--logo-url);
                    background-size: contain;
                    background-repeat: no-repeat;
                    background-position: left center;
                    flex-shrink: 0;
                }
            `}</style>

            {/* 
              * Container for the chat widget.
              * We define the logo URL here as a CSS variable so it updates 
              * instantly if the React state (logo) changes. 
              */}
            <div 
                id="n8n-chat" 
                style={{ 
                    '--chat--header--logo-url': `url('${brandLogo.src}')` 
                } as React.CSSProperties} 
            />
        </>
    );
};

Key Concepts

  • useRef(false): In React Strict Mode (development), effects run twice. This ref ensures we only call createChat once.
  • CSS Variable Injection: We pass the imported image URL (brandLogo.src) into the DOM via the --chat--header--logo-url Custom Property. This allows CSS to access the dynamic Javascript asset path.
  • Deep Selector: The .chat-header h1 selector targets the internal structure of the rendered widget. This is brittle if the library changes significantly, but stable for the current version.

3. Integration into Layout

Now, simply drop this component into your root layout. Since we marked it 'use client', it will work perfectly inside your Server Component layout.

// File: src/app/layout.tsx
import { N8nChat } from '@/components/N8nChat';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <N8nChat />
      </body>
    </html>
  );
}

Summary

You now have a chat widget that:

  1. Loads cleanly without hydration errors.
  2. Matches your brand identity using CSS variables.
  3. Displays your actual logo in the header using the CSS injection technique.

This approach gives you the speed of a pre-built widget with the visual polish of a custom component.

Thanks, Matija

7

Frequently Asked Questions

Comments

Leave a Comment

Your email will not be published

10-2000 characters

• Comments are automatically approved and will appear immediately

• Your name and email will be saved for future comments

• Be respectful and constructive in your feedback

• No spam, self-promotion, or off-topic content

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.