惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

爱范儿
爱范儿
Microsoft Azure Blog
Microsoft Azure Blog
G
Google Developers Blog
宝玉的分享
宝玉的分享
V
V2EX
MongoDB | Blog
MongoDB | Blog
S
SegmentFault 最新的问题
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Microsoft Security Blog
Microsoft Security Blog
博客园 - 聂微东
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
IT之家
IT之家
Martin Fowler
Martin Fowler
大猫的无限游戏
大猫的无限游戏
Recent Announcements
Recent Announcements
人人都是产品经理
人人都是产品经理
博客园 - 司徒正美
美团技术团队
F
Fortinet All Blogs
N
Netflix TechBlog - Medium
Hugging Face - Blog
Hugging Face - Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
罗磊的独立博客
B
Blog RSS Feed

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
плавающее оглавление на solidjs
собачья будк · 2026-04-24 · via DEV Community

иногда плавающее оглавление кажется довольно простой штукой: берёшь заголовки, рисуешь список, добавляешь якоря — готово.

в этой статье — разбор table of contents, который я собрал на solidjs: от поиска заголовков до sticky/fixed поведения и адаптивного ui.


контекст

задача :

  • собрать список заголовков статьи

  • сделать быстрые переходы по клику

  • подсвечивать текущий раздел при скролле

  • поддерживать мобильный и десктопный режим

  • уметь скрываться и появляться


сбор заголовков из документа

вся система начинается с обхода dom и поиска заголовков внутри контейнера статьи.

const updateHeadings = () => {
  const parent = document.querySelector(props.parentSelector)

  if (!parent) return

  const nodes = Array.from(
    parent.querySelectorAll<HTMLElement>('h1, h2, h3, h4')
  )

  setHeadings(nodes)
  setAreHeadingsLoaded(true)
}

Enter fullscreen mode Exit fullscreen mode

по сути это “снимок структуры документа”.


debounce пересборки оглавления

чтобы не пересобирать список слишком часто:

const debouncedUpdateHeadings = debounce(500, updateHeadings)

Enter fullscreen mode Exit fullscreen mode

это особенно важно, если контент может динамически изменяться.


определение активного заголовка

самая “живая” часть компонента — отслеживание скролла.

const isInViewport = (el: HTMLElement) => {
  const rect = el.getBoundingClientRect()

  return rect.top <= DEFAULT_HEADER_OFFSET + 24
}

Enter fullscreen mode Exit fullscreen mode

и дальше поиск активного элемента:

const updateActiveHeader = throttle(50, () => {
  const index = headings().findIndex((h) => isInViewport(h))

  setActiveHeaderIndex(index)
})

Enter fullscreen mode Exit fullscreen mode


переход к заголовку

при клике происходит ручной scroll с компенсацией fixed header’а:

const scrollToHeader = (element: HTMLElement) => {
  const top =
    element.getBoundingClientRect().top -
    document.body.getBoundingClientRect().top -
    DEFAULT_HEADER_OFFSET

  window.scrollTo({
    top,
    behavior: 'smooth'
  })
}

Enter fullscreen mode Exit fullscreen mode


состояние компонента

оглавление держит сразу несколько слоёв состояния:

const [headings, setHeadings] = createSignal<HTMLElement[]>([])
const [activeHeaderIndex, setActiveHeaderIndex] = createSignal(-1)
const [isVisible, setIsVisible] = createSignal(true)
const [areHeadingsLoaded, setAreHeadingsLoaded] = createSignal(false)
const [isDocumentReady, setIsDocumentReady] = createSignal(false)

Enter fullscreen mode Exit fullscreen mode


подписка на скролл

основная реактивность строится через window scroll:

onMount(() => {
  setIsDocumentReady(true)

  debouncedUpdateHeadings()

  window.addEventListener('scroll', updateActiveHeader)

  onCleanup(() => {
    window.removeEventListener('scroll', updateActiveHeader)
  })
})

Enter fullscreen mode Exit fullscreen mode


реакция на изменение статьи

если меняется тело статьи — пересобираем структуру:

createEffect(
  on(
    () => props.body,
    () => {
      if (isDocumentReady()) {
        debouncedUpdateHeadings()
      }
    }
  )
)

Enter fullscreen mode Exit fullscreen mode


рендер списка

основной ui — это список кнопок, привязанных к заголовкам.

<ul class={styles.TableOfContentsHeadingsList}>
  <For each={headings()}>
    {(h, index) => (
      <li>
        <button
          class={clsx(styles.TableOfContentsHeadingsItem, {
            [styles.TableOfContentsHeadingsItemH3]: h.nodeName === 'H3',
            [styles.TableOfContentsHeadingsItemH4]: h.nodeName === 'H4',
            [styles.active]: index() === activeHeaderIndex()
          })}
          innerHTML={h.textContent || ''}
          onClick={(e) => {
            e.preventDefault()
            scrollToHeader(h)
          }}
        />
      </li>
    )}
  </For>
</ul>

Enter fullscreen mode Exit fullscreen mode


визуальная иерархия заголовков

уровни заголовков просто сдвигаются визуально:

.TableOfContentsHeadingsItemH3 {
  padding-left: 8px;
}

.TableOfContentsHeadingsItemH4 {
  padding-left: 16px;
}

Enter fullscreen mode Exit fullscreen mode


активный пункт

подсветка текущего раздела минимальная, но важная:

.TableOfContentsHeadingsItem.active {
  font-weight: 700 !important;
}

Enter fullscreen mode Exit fullscreen mode


sticky поведение на десктопе

на больших экранах оглавление становится sticky-блоком:

.TableOfContentsContainer {
  @include media-breakpoint-up(xl) {
    position: sticky;
    top: 100px;
    height: calc(100vh - 120px);
    flex-direction: column;
  }
}

Enter fullscreen mode Exit fullscreen mode


mobile режим (fixed bottom panel)

на мобильных это уже не sidebar, а выезжающая панель:

.TableOfContentsFixedWrapper {
  @include media-breakpoint-down(xl) {
    position: fixed;
    bottom: 0;
    left: 0;
    width: 100%;
    max-height: 50vh;
    background: #000;
    color: #fff;
  }
}

Enter fullscreen mode Exit fullscreen mode


toggle видимости

оглавление можно скрывать и показывать:

const toggleIsVisible = () => {
  setIsVisible((v) => !v)
}

Enter fullscreen mode Exit fullscreen mode


итог

в итоге это не просто оглавление.

это:

  • парсинг DOM структуры статьи

  • debounce пересборки контента

  • scroll tracking с throttle

  • вычисление активного раздела

  • sticky + fixed адаптивный layout

  • UI, который живёт вместе со скроллом

и самое интересное — такие компоненты всегда выглядят простыми в начале, но постепенно превращаются в полноценный слой навигации поверх документа, который “понимает” структуру текста и помогает по нему двигаться.

source code