Skip to content

Commit 7dcc4be

Browse files
committed
Redesign language switcher with flag dropdown
Replace native select element with custom dropdown featuring country flags, animated chevron, checkmark for active language, smooth open/close transitions, and click-outside/escape dismissal.
1 parent ea58fc5 commit 7dcc4be

2 files changed

Lines changed: 176 additions & 38 deletions

File tree

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup>
22
import { useData, useRouter } from 'vitepress'
3-
import { computed } from 'vue'
3+
import { computed, ref, onMounted, onUnmounted } from 'vue'
44
55
const props = defineProps({
66
languages: {
@@ -9,8 +9,12 @@ const props = defineProps({
99
}
1010
})
1111
12+
const flags = { en: '🇬🇧', de: '🇩🇪', fr: '🇫🇷', it: '🇮🇹' }
13+
1214
const { frontmatter, page } = useData()
1315
const router = useRouter()
16+
const open = ref(false)
17+
const dropdownRef = ref(null)
1418
1519
const currentLang = computed(() => {
1620
const rel = page.value.relativePath || ''
@@ -23,28 +27,70 @@ const baseUrl = computed(() => {
2327
return '/' + rel.replace(/\.(de|fr|it|en)$/, '')
2428
})
2529
26-
function switchLanguage(event) {
27-
const lang = event.target.value
28-
if (lang === currentLang.value) return
30+
function toggle() {
31+
open.value = !open.value
32+
}
33+
34+
function switchLanguage(lang) {
35+
if (lang === currentLang.value) {
36+
open.value = false
37+
return
38+
}
39+
open.value = false
2940
const suffix = lang === 'en' ? '' : `.${lang}`
3041
router.go(`${baseUrl.value}${suffix}.html`)
3142
}
43+
44+
function onClickOutside(e) {
45+
if (dropdownRef.value && !dropdownRef.value.contains(e.target)) {
46+
open.value = false
47+
}
48+
}
49+
50+
function onKeydown(e) {
51+
if (e.key === 'Escape') open.value = false
52+
}
53+
54+
onMounted(() => {
55+
document.addEventListener('click', onClickOutside)
56+
document.addEventListener('keydown', onKeydown)
57+
})
58+
59+
onUnmounted(() => {
60+
document.removeEventListener('click', onClickOutside)
61+
document.removeEventListener('keydown', onKeydown)
62+
})
3263
</script>
3364
3465
<template>
35-
<div class="lang-switcher">
36-
<label class="lang-switcher-label">
37-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
38-
<select :value="currentLang" @change="switchLanguage">
39-
<option
66+
<div class="lang-switcher" ref="dropdownRef">
67+
<button
68+
class="lang-trigger"
69+
:class="{ 'lang-trigger--open': open }"
70+
@click="toggle"
71+
:aria-expanded="open"
72+
aria-haspopup="listbox"
73+
>
74+
<span class="lang-flag">{{ flags[currentLang] }}</span>
75+
<span class="lang-name">{{ languages[currentLang] }}</span>
76+
<svg class="lang-chevron" :class="{ 'lang-chevron--open': open }" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>
77+
</button>
78+
<Transition name="lang-dropdown">
79+
<ul v-if="open" class="lang-menu" role="listbox">
80+
<li
4081
v-for="(label, code) in languages"
4182
:key="code"
42-
:value="code"
43-
:selected="code === currentLang"
83+
class="lang-option"
84+
:class="{ 'lang-option--active': code === currentLang }"
85+
role="option"
86+
:aria-selected="code === currentLang"
87+
@click="switchLanguage(code)"
4488
>
45-
{{ label }}
46-
</option>
47-
</select>
48-
</label>
89+
<span class="lang-flag">{{ flags[code] }}</span>
90+
<span class="lang-name">{{ label }}</span>
91+
<svg v-if="code === currentLang" class="lang-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
92+
</li>
93+
</ul>
94+
</Transition>
4995
</div>
5096
</template>

docs/.vitepress/theme/release-notes.css

Lines changed: 115 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -339,49 +339,141 @@
339339
display: flex;
340340
justify-content: flex-end;
341341
margin-bottom: 1.5em;
342+
position: relative;
342343
}
343344

344-
.lang-switcher-label {
345+
.lang-trigger {
345346
display: inline-flex;
346347
align-items: center;
347-
gap: 6px;
348+
gap: 8px;
349+
padding: 7px 12px 7px 10px;
350+
background: var(--blog-card-bg);
351+
border: 1px solid var(--blog-border);
352+
border-radius: 10px;
348353
font-family: 'Lato', sans-serif;
349-
font-size: 0.85em;
350-
color: var(--blog-text-muted);
354+
font-size: 0.88em;
355+
color: var(--blog-text);
356+
cursor: pointer;
357+
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
358+
user-select: none;
359+
line-height: 1;
360+
}
361+
362+
.lang-trigger:hover {
363+
border-color: var(--blog-green);
364+
background: var(--blog-bg-subtle);
365+
}
366+
367+
.lang-trigger--open {
368+
border-color: var(--blog-green);
369+
box-shadow: 0 0 0 3px rgba(0, 148, 144, 0.12);
370+
}
371+
372+
.lang-trigger:focus-visible {
373+
outline: none;
374+
border-color: var(--blog-green);
375+
box-shadow: 0 0 0 3px rgba(0, 148, 144, 0.18);
376+
}
377+
378+
.lang-flag {
379+
font-size: 1.25em;
380+
line-height: 1;
381+
}
382+
383+
.lang-trigger .lang-name {
384+
font-weight: 600;
351385
}
352386

353-
.lang-switcher-label svg {
387+
.lang-chevron {
388+
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
389+
opacity: 0.5;
354390
flex-shrink: 0;
355391
}
356392

357-
.lang-switcher select {
358-
appearance: none;
359-
-webkit-appearance: none;
393+
.lang-chevron--open {
394+
transform: rotate(180deg);
395+
}
396+
397+
/* Dropdown menu */
398+
.lang-menu {
399+
position: absolute;
400+
top: calc(100% + 6px);
401+
right: 0;
402+
z-index: 100;
403+
min-width: 180px;
404+
margin: 0;
405+
padding: 6px;
406+
list-style: none;
360407
background: var(--blog-card-bg);
361408
border: 1px solid var(--blog-border);
362-
border-radius: 6px;
363-
padding: 4px 28px 4px 10px;
409+
border-radius: 12px;
410+
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1), 0 2px 8px rgba(0, 0, 0, 0.04);
411+
}
412+
413+
.dark .lang-menu {
414+
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35), 0 2px 8px rgba(0, 0, 0, 0.2);
415+
}
416+
417+
.lang-option {
418+
display: flex;
419+
align-items: center;
420+
gap: 10px;
421+
padding: 9px 12px;
422+
border-radius: 8px;
423+
cursor: pointer;
364424
font-family: 'Lato', sans-serif;
365-
font-size: 1em;
425+
font-size: 0.9em;
366426
color: var(--blog-text);
367-
cursor: pointer;
368-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
369-
background-repeat: no-repeat;
370-
background-position: right 8px center;
427+
transition: background 0.15s;
428+
position: relative;
429+
margin: 0;
371430
}
372431

373-
.dark .lang-switcher select {
374-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
432+
.lang-option::before {
433+
display: none;
375434
}
376435

377-
.lang-switcher select:hover {
378-
border-color: var(--blog-green);
436+
.lang-option:hover {
437+
background: var(--blog-bg-subtle);
379438
}
380439

381-
.lang-switcher select:focus {
382-
outline: none;
383-
border-color: var(--blog-green);
384-
box-shadow: 0 0 0 2px rgba(0, 148, 144, 0.2);
440+
.lang-option--active {
441+
color: var(--blog-green);
442+
font-weight: 600;
443+
}
444+
445+
.lang-option--active:hover {
446+
background: rgba(0, 148, 144, 0.06);
447+
}
448+
449+
.lang-option .lang-name {
450+
flex: 1;
451+
}
452+
453+
.lang-check {
454+
color: var(--blog-green);
455+
flex-shrink: 0;
456+
}
457+
458+
/* Dropdown animation */
459+
.lang-dropdown-enter-active {
460+
transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
461+
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
462+
}
463+
464+
.lang-dropdown-leave-active {
465+
transition: opacity 0.15s cubic-bezier(0.4, 0, 1, 1),
466+
transform 0.15s cubic-bezier(0.4, 0, 1, 1);
467+
}
468+
469+
.lang-dropdown-enter-from {
470+
opacity: 0;
471+
transform: translateY(-6px) scale(0.97);
472+
}
473+
474+
.lang-dropdown-leave-to {
475+
opacity: 0;
476+
transform: translateY(-4px) scale(0.98);
385477
}
386478

387479
/* Infographic image */

0 commit comments

Comments
 (0)