A lightweight, unstyled language selector for Next.js (App Router & Pages Router).
Manages the NEXT_LOCALE cookie and works with next-intl or any i18n solution.
- Installation
- Basic Usage
- Styling
- Custom UI
- Dropdown mode
- Setup with next-intl
- Without full reloads
- Props
setLocaleCookieutility- SSR & hydration
- Next.js Native: Built for the Next.js ecosystem (App Router & Pages Router).
- Zero Dependencies: No runtime deps β just React.
- Unstyled by default: Bring your own CSS, Tailwind, Shadcn, Radix β no style conflicts.
- Cookie-based: Reads and writes
NEXT_LOCALEautomatically. - Secure: Cookie injection-safe,
SameSite=Laxout of the box.
pnpm add next-language-selector
# or
npm install next-language-selectorDrop the component into your Footer or Navbar. It handles cookie sync and state out of the box.
import { LanguageSelector } from "next-language-selector";
const locales = [
{ name: "English", code: "en", flag: "πΊπΈ" },
{ name: "Deutsch", code: "de", flag: "π©πͺ" },
];
export default function Footer() {
return (
<footer>
<LanguageSelector
locales={locales}
defaultLocale="en"
/>
</footer>
);
}The component is unstyled by default. Use className / itemClassName props or target the data-active attribute:
/* Plain CSS */
.lang-btn {
background: none;
border: none;
cursor: pointer;
opacity: 0.5;
}
.lang-btn[data-active="true"] {
opacity: 1;
font-weight: 600;
border-bottom: 2px solid currentColor;
}<LanguageSelector
locales={locales}
defaultLocale="en"
className="flex gap-2"
itemClassName="lang-btn"
/>With Tailwind:
<LanguageSelector
locales={locales}
defaultLocale="en"
className="flex items-center gap-3"
itemClassName="text-sm text-gray-400 data-[active=true]:text-black data-[active=true]:font-semibold"
/>Use the renderCustom prop to take full control over rendering while keeping the cookie logic.
<LanguageSelector
locales={locales}
defaultLocale="en"
renderCustom={({ locales, currentLocale, onChange }) => (
<div className="flex gap-4">
{locales.map((lang) => (
<button
key={lang.code}
onClick={() => onChange(lang.code)}
className={currentLocale === lang.code ? "font-bold" : "opacity-50"}
>
{lang.flag} {lang.name}
</button>
))}
</div>
)}
/><LanguageSelector
locales={locales}
defaultLocale="en"
isDropdown
className="border rounded px-2 py-1"
/>Update your middleware.ts to read the cookie set by this component:
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";
export default createMiddleware({
...routing,
localeCookie: {
name: "NEXT_LOCALE",
path: "/",
maxAge: 31536000,
},
});By default the page reloads after a locale change so the server picks up the new cookie. For a smoother UX combine autoReload={false} with the onChange callback and the Next.js router:
"use client";
import { useRouter } from "next/navigation";
import { LanguageSelector } from "next-language-selector";
export function LocaleSwitch() {
const router = useRouter();
return (
<LanguageSelector
locales={locales}
defaultLocale="en"
autoReload={false}
onChange={() => router.refresh()}
/>
);
}onChange fires with the selected code before the cookie is written (and before the reload when autoReload is on) β also handy for analytics.
| Prop | Type | Default | Description |
|---|---|---|---|
locales |
LocaleConfig[] |
Required | Array of { name, code, flag? } objects |
defaultLocale |
string |
Required | Fallback locale code |
isDropdown |
boolean |
false |
Render as <select> instead of buttons |
autoReload |
boolean |
true |
Reload page after cookie change |
onChange |
(code: string) => void |
- | Called on selection, before cookie write/reload |
cookieName |
string |
NEXT_LOCALE |
Cookie name to store the selected locale |
className |
string |
- | CSS class for the wrapper <div> or <select> |
itemClassName |
string |
- | CSS class for each <button> or <option> |
renderCustom |
Function |
- | Render prop for fully custom UI |
interface LocaleConfig {
name: string; // Display name, e.g. "English"
code: string; // Locale code, e.g. "en"
flag?: string; // Optional emoji flag, e.g. "πΊπΈ"
}The cookie writer is exported separately β useful if you want to switch the locale from your own code (a settings page, a keyboard shortcut, etc.) without rendering the component:
import { setLocaleCookie } from "next-language-selector";
// setLocaleCookie(locale, cookieName?, autoReload?)
setLocaleCookie("de"); // sets NEXT_LOCALE=de and reloads
setLocaleCookie("de", "MY_LOCALE", false); // custom cookie, no reloadThe name and value are URI-encoded (cookie-injection safe), written with max-age=31536000; path=/; SameSite=Lax. On the server it is a no-op.
The component renders null until it has mounted and read the cookie on the client, so server and client markup never mismatch. Expect the selector to appear only after hydration β reserve space with CSS if layout shift matters. Malformed or unknown cookie values are ignored and defaultLocale is used.
Buttons are rendered with type="button", so placing the selector inside a <form> won't trigger submits.
MIT