Skip to content

kirilinsky/next-language-selector

Repository files navigation

next-language-selector

npm version bundle size codecov security score license

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.

Live demo β†’

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_LOCALE automatically.
  • Secure: Cookie injection-safe, SameSite=Lax out of the box.

Installation

pnpm add next-language-selector
# or
npm install next-language-selector

Basic 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 reload

The 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

About

Configurable language selector for Next.js

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors