Skip to content

Instantly share code, notes, and snippets.

@Caellian
Last active July 18, 2023 16:49
Show Gist options
  • Save Caellian/6f75b2e99795891141f3090504fe3236 to your computer and use it in GitHub Desktop.
Save Caellian/6f75b2e99795891141f3090504fe3236 to your computer and use it in GitHub Desktop.
A rounded tab picker component in Svelte
<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