Overlay

SwitchLang

Selector de idioma con enlace por locale

Preview

size
current
variant
icon

Code

import css from "./SwitchLang.module.css";
import { useState, type MouseEvent } from "react";

type Size = "sm" | "md" | "lg";

interface ResponsiveSize {
  base: Size;
  mobile?: Size;
  tablet?: Size;
  desktop?: Size;
  wide?: Size;
}

interface SwitchLangOption {
  value: string;
  label: string;
  href: string;
}

interface SwitchLangProps {
  // Exactly two: with more languages the pattern is a dropdown, not a switch.
  options: [SwitchLangOption, SwitchLangOption];
  current: string;
  size: Size | ResponsiveSize;
  variant: "inline" | "track";
  icon?: React.ReactNode;
}

function getSizeClasses(size: Size | ResponsiveSize): string {
  if (typeof size === "string") {
    return css[size];
  }

  const classes = [css[`${size.base}_base`]];

  if (size.mobile) classes.push(css[`${size.mobile}_mobile`]);
  if (size.tablet) classes.push(css[`${size.tablet}_tablet`]);
  if (size.desktop) classes.push(css[`${size.desktop}_desktop`]);
  if (size.wide) classes.push(css[`${size.wide}_wide`]);

  return classes.join(" ");
}

export default function SwitchLang({
  options,
  current,
  size,
  variant,
  icon,
}: SwitchLangProps) {
  const [first, second] = options;
  const [interacted, setInteracted] = useState(false);
  const [picked, setPicked] = useState<string | null>(null);
  const [lastCurrent, setLastCurrent] = useState(current);

  if (current !== lastCurrent) {
    setLastCurrent(current);
    setPicked(null);
  }

  const shown = picked ?? current;

  const pick = (ev: MouseEvent, value: string) => {
    if (ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;

    setInteracted(true);
    setPicked(value);
  };

  return (
    <div
      className={`${css.switchlang} ${variant === "track" ? css.track : ""} ${interacted ? css.animated : ""} ${getSizeClasses(size)}`}
    >
      {icon && (
        <span className={css.switchlang_icon} aria-hidden="true">
          {icon}
        </span>
      )}
      <span className={css.switchlang_options}>
        <a
          className={`${css.switchlang_option} ${first.value === shown ? css.active : ""}`}
          href={first.href}
          lang={first.value}
          hrefLang={first.value}
          aria-current={first.value === current ? "true" : undefined}
          onClick={(ev) => pick(ev, first.value)}
        >
          {first.label}
        </a>
        <span className={css.switchlang_separator} aria-hidden="true">
          /
        </span>
        <a
          className={`${css.switchlang_option} ${second.value === shown ? css.active : ""}`}
          href={second.href}
          lang={second.value}
          hrefLang={second.value}
          aria-current={second.value === current ? "true" : undefined}
          onClick={(ev) => pick(ev, second.value)}
        >
          {second.label}
        </a>
      </span>
    </div>
  );
}

Props

PropTypeRequiredDescription
options[SwitchLangOption, SwitchLangOption]requiredExactamente dos idiomas ({ value, label, href }); con más, lo correcto es un desplegable.
currentstringrequiredIdioma activo (marca su enlace con aria-current).
size"sm" | "md" | "lg" | ResponsiveSizerequiredTamaño del texto y de la caja de clic de cada opción (inline) o del track (track). Fijo o responsive.
variant"inline" | "track"requiredForma: texto con separador (header) o una píldora que se desliza entre las opciones de un track (drawer).
iconReact.ReactNode—Icono decorativo delante de las opciones.