next-language-selector
v0.4.0
Published
Configurable language selector for Next.js
Maintainers
Readme
next-language-selector
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
Key Features
- 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.
Installation
pnpm add next-language-selector
# or
npm install next-language-selectorBasic Usage
Drop 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>
);
}Styling
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"
/>Custom UI
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>
)}
/>Dropdown mode
<LanguageSelector
locales={locales}
defaultLocale="en"
isDropdown
className="border rounded px-2 py-1"
/>Setup with next-intl
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,
},
});Without full reloads
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.
Props
| 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 |
LocaleConfig
interface LocaleConfig {
name: string; // Display name, e.g. "English"
code: string; // Locale code, e.g. "en"
flag?: string; // Optional emoji flag, e.g. "🇺🇸"
}setLocaleCookie utility
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.
SSR & hydration
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.
License
MIT
