Tabs with Links
Tabs trigger를 링크로 위임해 URL hash와 선택 상태를 단순 동기화합니다.
<script lang="ts">
import * as Tabs from '@odbd/svelte/tabs'
type AsChildProps = () => object
const sections = [
{
value: 'overview',
label: '요약',
heading: '요약 섹션',
body: '핵심 상태와 다음 작업을 한 화면에서 확인해요.',
},
{
value: 'metrics',
label: '지표',
heading: '지표 섹션',
body: '전환율, 지연 건수, 처리 속도를 같은 흐름에서 비교해요.',
},
{
value: 'activity',
label: '활동',
heading: '활동 섹션',
body: '최근 변경 내역과 담당자 메모를 시간순으로 살펴봐요.',
},
]
const sectionValues = new Set(sections.map((section) => section.value))
let selected = $state(sections[0].value)
const selectedFromHash = () => {
if (typeof window === 'undefined') return sections[0].value
const value = window.location.hash.replace('#', '')
return sectionValues.has(value) ? value : sections[0].value
}
const syncHashSelection = () => {
selected = selectedFromHash()
}
const updateSelection = (details: { value: string }) => {
selected = details.value
}
$effect(() => {
if (typeof window === 'undefined') return
syncHashSelection()
window.addEventListener('hashchange', syncHashSelection)
return () => {
window.removeEventListener('hashchange', syncHashSelection)
}
})
</script>
<div class="examples-tabs-links">
<Tabs.Root value={selected} onValueChange={updateSelection}>
<Tabs.List aria-label="문서 섹션">
{#each sections as section}
<Tabs.Trigger value={section.value}>
{#snippet asChild(props: AsChildProps)}
<a
{...props()}
href={`#${section.value}`}
aria-current={selected === section.value ? 'page' : undefined}
>
{section.label}
</a>
{/snippet}
</Tabs.Trigger>
{/each}
<Tabs.Indicator />
</Tabs.List>
{#each sections as section}
<Tabs.Content value={section.value}>
<section class="examples-tabs-links__panel" aria-labelledby={`${section.value}-title`}>
<strong id={`${section.value}-title`}>{section.heading}</strong>
<p>{section.body}</p>
</section>
</Tabs.Content>
{/each}
</Tabs.Root>
</div>
<style>
.examples-tabs-links {
width: min(100%, 28rem);
}
.examples-tabs-links :global(.odbd-tabs__list) {
overflow-x: auto;
}
.examples-tabs-links :global(.odbd-tabs__trigger) {
display: inline-flex;
align-items: center;
text-decoration: none;
white-space: nowrap;
}
.examples-tabs-links__panel {
display: grid;
min-height: 6rem;
gap: var(--odbd-space-2);
padding: var(--odbd-space-4);
color: var(--odbd-color-muted-foreground);
background: var(--odbd-color-surface-raised);
border: 1px solid var(--odbd-color-border);
border-radius: var(--odbd-radius-md);
line-height: 1.65;
}
.examples-tabs-links__panel strong {
color: var(--odbd-color-foreground);
}
.examples-tabs-links__panel p {
margin: 0;
}
</style>