Last active
July 18, 2023 16:49
-
-
Save Caellian/6f75b2e99795891141f3090504fe3236 to your computer and use it in GitHub Desktop.
A rounded tab picker component in Svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script lang="ts"> | |
import { onSelected } from "src/lib/util"; | |
import { onMount, type ComponentType, createEventDispatcher } from "svelte"; | |
/** | |
* Gap between tabs (in px). | |
*/ | |
export const tabGap = 10; | |
export let backgroundColor = "#35393E"; | |
export let tabColor = "#242A32"; | |
export let selectionColor = "#3F4349"; | |
export let selectionBorderColor = "#1F988A"; | |
export let tabs: { | |
icon?: ComponentType; | |
name: string; | |
}[]; | |
var tabI = 0; | |
var container: HTMLDivElement; | |
interface TabInfo { | |
offsetX: number; | |
width: number; | |
height: number; | |
} | |
var tabInfo: TabInfo[]; | |
var colorVars = `--color-bg:${backgroundColor};--color-tab:${tabColor};--color-selection:${selectionColor};--color-selection-border:${selectionBorderColor};`; | |
var tabVars = ""; | |
const updateVars = (tab: TabInfo) => | |
(tabVars = `--tab-gap:${tabGap}px;--selection-pos:${tab.offsetX}px;--selection-width:${tab.width}px;--selection-height:${tab.height}px;`); | |
const dispatch = createEventDispatcher(); | |
function selectTab(n: number) { | |
tabI = n; | |
updateVars(tabInfo[n]); | |
dispatch("tabChanged", { | |
tab: tabI, | |
}); | |
} | |
function handleSelect(n: number) { | |
return onSelected(() => { | |
selectTab(n); | |
}); | |
} | |
function tabClass(n: number) { | |
if (tabI === n) { | |
return "tab current"; | |
} else { | |
return "tab"; | |
} | |
} | |
function recalculateBounds() { | |
tabInfo = []; | |
var total = 0; | |
for (const t of Array.from(container.children)) { | |
let tRect = t.getBoundingClientRect(); | |
const info = { | |
offsetX: total, | |
width: tRect.width, | |
height: tRect.height, | |
}; | |
if (tabInfo.length === tabI) { | |
updateVars(info); | |
} | |
tabInfo.push(info); | |
total += tabGap + tRect.width; | |
} | |
} | |
onMount(() => { | |
recalculateBounds(); | |
const resizeObserver = new ResizeObserver(recalculateBounds); | |
resizeObserver.observe(container); | |
return () => { | |
resizeObserver.unobserve(container); | |
}; | |
}); | |
</script> | |
<div | |
bind:this={container} | |
style={colorVars + tabVars} | |
class="tabs" | |
role="menubar" | |
> | |
{#each tabs as tab, i} | |
<div | |
on:click={handleSelect(i)} | |
on:keyup={handleSelect(i)} | |
class={tabClass(i)} | |
role="menuitemradio" | |
aria-checked={tabI === i} | |
tabindex="0" | |
> | |
{#if tab.icon} | |
<div class="icon"> | |
<svelte:component this={tab.icon} /> | |
</div> | |
{/if} | |
<p>{tab.name}</p> | |
</div> | |
{/each} | |
</div> | |
<style> | |
.tabs { | |
display: flex; | |
justify-content: space-evenly; | |
gap: var(--tab-gap); | |
width: max-content; | |
position: relative; | |
margin-inline: auto; | |
--outer-padding: 0.5ch; | |
padding: var(--outer-padding); | |
background-color: var(--color-bg); | |
border-radius: 9999px; | |
} | |
.tabs::after { | |
content: ""; | |
display: block; | |
position: absolute; | |
left: calc(var(--outer-padding) + var(--selection-pos, 0px)); | |
top: var(--outer-padding); | |
width: var(--selection-width); | |
height: var(--selection-height, calc(100% - var(--outer-padding) * 2)); | |
background-color: var(--color-selection); | |
outline: 2px solid var(--color-selection-border); | |
border-radius: 9999px; | |
transition: all 0.4s cubic-bezier(0.76, 0, 0.24, 1); | |
} | |
.tab { | |
display: flex; | |
gap: 0.5ch; | |
align-items: center; | |
background-color: var(--color-tab); | |
padding: 0.5ch 1ch; | |
border-radius: 9999px; | |
cursor: pointer; | |
} | |
.tab .icon { | |
width: 1.2em; | |
height: 1.2em; | |
} | |
.tab > * { | |
position: relative; | |
z-index: 1; | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment