In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.
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.
bash
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:
Initialization: Loading the widget only once after the component mounts.
Configuration: Passing the webhook URL securely.
Dynamic Styling: Using CSS variables to inject a custom logo into the chat header—something the default config object doesn't support.
typescript
// 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';
exportconstN8nChat = () => {
// defined to prevent double initialization in Strict Modeconst 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 logosubtitle: 'Ask us anything',
},
},
});
}, []);
return (
<>
{/* Global overrides for the chat widget */}
<stylejsxglobal>{`
: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-headerh1 {
display: flex;
align-items: center;
}
.chat-headerh1::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.
*/}
<divid="n8n-chat"style={{
'--chat--header--logo-url': `url('${brandLogo.src}')`
} asReact.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.