语义标签是组件库可扩展性的底层骨架,缺失则导致辅助技术不可识别、表单生命周期中断、seo失效;它提供原生语义契约、结构隔离与标准接口能力。

直接说结论:语义标签本身不构成组件库,但它是组件库可扩展性的底层骨架——没它,customElements.define()注册出来的标签只是带样式的
为什么<article></article>和<details></details>比div更适合做组件基底
很多团队试图用div封装所有逻辑,再靠class="card"或data-role="accordion"模拟语义。这在初期看似灵活,但很快会暴露问题:
- 屏幕阅读器完全跳过
div class="accordion",用户无法用快捷键导航到“可展开区域” - 搜索引擎把整块内容当普通文本处理,不会给其中的标题
h3赋予章节权重 - 当你想把一个“产品卡片”抽成独立 Web Component 时,
<article></article>天然支持aria-live、role="article"继承,而div得手动补全所有无障碍属性
真正可扩展的组件不是从零造轮子,而是复用浏览器已有的语义契约:<details></details>自带展开/收起状态管理;<nav></nav>默认被document.querySelector('nav')精准捕获;<main></main>确保document.querySelector('main')永远只返回一个节点——这些都不是CSS能提供的能力。
用<section></section>和<aside></aside>划分组件作用域边界
组件库一旦变大,最头疼的是样式泄漏和逻辑耦合。光靠Shadow DOM不够,结构层就要隔离:
-
<section></section>适合封装有明确主题的模块,比如“价格对比表”“用户评论流”。它隐含节标题层级,h2在<section></section>内就是该模块的二级标题,不影响外部文档大纲 -
<aside></aside>不是“侧边栏”的视觉定位,而是语义上的“附属信息”。把它用在组件内部,比如<product-card></product-card>里放一个<aside class="specs"></aside>,就天然告诉辅助技术:“这部分数据对理解主产品非必需,可跳过” - 避免
<section></section>嵌套超过两层。第三层开始,应该考虑是否该拆成独立自定义元素(如<review-list></review-list>),而不是继续堆<section><section><section></section></section></section>
这种分层不是为了好看,是让querySelector能稳定工作——card.querySelector('section.review')比card.querySelector('[data-type="review"]')更可靠,因为后者一旦改名就全挂。
自定义元素必须配合语义标签注册,否则等于没注册
写customElements.define('price-badge', PriceBadge)只是让浏览器认识这个标签名,但如果不让它承载语义,它就只是个空壳:
- 注册时必须用小写+连字符命名,
price-badge合法,PriceBadge或pricebadge会直接报Failed to execute 'define' - 类必须继承
HTMLElement,且constructor里第一行必须是super();DOM操作(比如this.innerHTML = '...')只能放在<code>connectedCallback里 - 关键一步:组件内部结构要回归语义。比如
<price-badge></price-badge>内部不要写<div class="value">,而应是<code><span class="value" aria-label="价格"></span>或直接用<data value="299"></data>——让值本身带含义,而不是靠class名解释 - 如果组件要参与表单,必须加
formAssociated: true并调this.attachInternals(),否则form.elements里根本找不到它
最容易被忽略的一点:语义标签不是装饰,是接口。你用<article></article>包裹组件内容,就自动承诺了它可被独立抓取、可被RSS订阅、可被document.querySelectorAll('article')批量处理——这些能力,div永远给不了。











