成为全栈·Next.js 网站前台篇·响应式、可访问性与错误状态:内容站不能只验桌面首页

成为全栈·Next.js 网站前台篇·响应式、可访问性与错误状态:内容站不能只验桌面首页

设计稿在 1440px 正常,只能证明一张静态画面。真实内容站还要面对 320px、四级菜单、键盘焦点、宽表格、重复标题、空数据、404 和服务端故障。

前言

响应式经常被简化成"加几个媒体查询",可访问性则被简化成"补 aria-label"。真正验收时,问题往往发生在组合路径:手机展开四级分类后能否关闭,代码块横向滚动会不会带动整页,错误按钮能不能真的恢复服务端数据。

本项目不只检查桌面首页,而是把视口、输入方式、内容边界和系统状态组成一张矩阵。

响应式验收不是设备型号清单

宽度 主要观察
320 最窄主体、标题换行、按钮与表单
375 常见手机、导航与文章目录
768 单栏到多栏的临界布局
1024 侧栏、首页焦点与菜单空间
1440 最大内容宽度与信息密度

现有样例在五种宽度下未发现主体横向溢出,但这不代表任意长标题和表格永远安全。正式内容接入后还要抽取最长标题、最深分类和最宽代码重新测。

四级导航优先使用原生 disclosure

tsx 复制代码
const CategoryItem = ({ node, depth = 0 }: Props) =>
  node.children?.length ? (
    <details className={`nav-branch depth-${depth}`}>
      <summary>
        {node.name}<span aria-hidden="true"> ▾</span>
      </summary>
      <div className="drop">
        <Link href={`/categories/${node.slug}`}>{node.name} · 全部</Link>
        {node.children.map((child) => (
          <CategoryItem key={child.id} node={child} depth={depth + 1} />
        ))}
      </div>
    </details>
  ) : <Link href={`/categories/${node.slug}`}>{node.name}</Link>

details/summary 提供基础键盘展开语义,但还需要处理路由切换、外部点击与 Escape。

tsx 复制代码
useEffect(() => {
  if (event.key === 'Escape') {
    const active = document.activeElement
    ref.current?.querySelectorAll('details[open]').forEach((item) => {
      item.open = false
    })
    active?.closest('details')?.querySelector('summary')?.focus()
    setOpen(false)
  }
}, [])

关闭后把焦点送回触发展开的位置,键盘用户才知道自己在哪里。

手机菜单按钮必须暴露真实状态

tsx 复制代码
<button
  type="button"
  aria-expanded={open}
  aria-controls="site-navigation"
  onClick={() => setOpen(!open)}
>
  栏目 ☰
</button>
tsx 复制代码
<nav
  id="site-navigation"
  className={`headnav ${open ? 'open' : ''}`}
  aria-label="主导航"
>

样式隐藏与语义状态要同步。路由变化后还会关闭所有 details,避免新页面保留旧菜单遮挡内容。

跳过链接让键盘直接进入正文

tsx 复制代码
<a className="skip-link" href="#main-content">
  跳到主要内容
</a>

<main id="main-content">{children}</main>

页头包含品牌、四级导航、搜索和会员菜单,键盘用户每页都逐项经过会很疲惫。跳过链接在获得焦点时显示,是低成本但实际有用的结构。

宽内容只在自己的边界内滚动

tsx 复制代码
table: ({ children }) => (
  <div className="table-scroll">
    <table>{children}</table>
  </div>
)

代码块同样由 CodeBlock 管理 overflow。不要给整篇 article 设置横向滚动,否则标题、段落和目录都会跟着左右移动。长 URL 更适合断行,表格与代码才使用局部滚动。

Loading、Empty、Failure、404 必须分开

tsx 复制代码
export const Skeleton = () => (
  <div role="status" aria-busy="true" aria-label="正在加载">
    <span className="sr-only">正在加载内容</span>
    <SkeletonRows />
  </div>
)
tsx 复制代码
export const Failure = ({ retry, message }: Props) => (
  <div className="hint" role="alert">
    <p>{message}</p>
    <button type="button" onClick={retry}>重新加载</button>
  </div>
)
状态 意义 合理动作
Loading 结果尚未确定 等待,保留结构
Empty 请求成功但没有内容 浏览其他入口或创建内容
Failure 请求未成功 重试当前模块或页面
404 资源明确不存在 返回索引/首页

500 不能被 catch 成空数组后显示"暂无文章",否则用户无法判断内容是否真的为空。

服务端错误恢复需要真正重新请求

tsx 复制代码
'use client'

export default function ErrorPage() {
  return (
    <div role="alert">
      <h1>内容暂时没有加载出来</h1>
      <button onClick={() => window.location.reload()}>
        重新加载
      </button>
    </div>
  )
}

项目曾使用错误边界 reset,但恢复后端后服务端内容没有重新获取。完整 reload 经故障注入验证有效。若未来错误页包着未保存长表单,则要重新设计细粒度恢复,不能机械照搬。

焦点与状态不能只靠颜色

tsx 复制代码
<Link
  href="/articles"
  aria-current={pathname === '/articles' ? 'page' : undefined}
>
  全部文章
</Link>
tsx 复制代码
<button aria-pressed={liked}>{liked ? '已赞同' : '赞同'}</button>

当前页面、轮播指示点、点赞状态都需要语义属性和可见文本。CSS 的 selected 色只是视觉辅助。表单错误用 role="alert",操作成功用 role="status",避免所有消息都打断读屏。

一套组合验收路径

text 复制代码
1. 320px 键盘打开手机菜单,展开到第四级,Escape 关闭并检查焦点
2. 打开含宽表格和长代码的文章,只允许局部横向滚动
3. 点击第二个重复中文目录标题,检查正确锚点与当前章节
4. 禁用 JavaScript,确认公开标题、正文和分页仍可读
5. 停止后端,区分搜索错误、文章错误与真实 404
6. 恢复后端后点击重新加载,确认内容和登录状态恢复
7. 使用空收藏、空搜索、空分类,核对各自下一步文案
8. 仅用键盘完成搜索、导航、登录和评论输入

自动工具可以发现缺少 label、对比度和部分语义问题,却不能证明焦点顺序合理、菜单不会困住用户、错误按钮真的恢复数据。最终仍需浏览器操作证据。

适用边界

本文完成的是现有页面和测试内容下的本地验收,不代表完整 WCAG 审计,也没有覆盖真实手机硬件、所有读屏软件和跨浏览器矩阵。

内容和运营图片变化会制造新问题,因此可访问性与响应式不是一次性验收章,而是持续回归条件。

小结

内容站的质量藏在正常截图之外:最窄视口、最深菜单、最宽内容、键盘焦点和失败恢复共同决定页面是否可用。

把 Loading、Empty、Failure 与 404 分开,再用真实操作覆盖组合路径,才能让"响应式和可访问"从样式口号变成证据。

延伸阅读


如果这篇文章对你有帮助,欢迎订阅我的 CSDN 专栏 「成为全栈」:

🔗 专栏地址:https://blog.csdn.net/fungleo/category_13204651.html

📦 本系列配套代码仓库:https://github.com/fengcms/become-a-full-stack-developer

相关推荐
ss2732 小时前
AI全栈实战 | 3.5-01 Python 全栈前端最小集:不会 React 也能做出能看的后台,关键就一条线
前端·python·react.js
FungLeo14 小时前
成为全栈·Next.js 网站前台篇·站点设置如何驱动页头、页脚与 SEO
react·isr·next.js·metadata·成为全栈·站点配置
记得开心一点嘛21 小时前
Trellora:基于 Electron、React 和本地知识库的 AI 知识工作台
人工智能·react.js·electron
500841 天前
React Native for OpenHarmony 实战:三方库 react-native-url-polyfill 的鸿蒙化适配指南
javascript·react native·react.js·性能优化·electron·harmonyos
500841 天前
React Native for OpenHarmony 实战:三方库 react-native-volume-control 的鸿蒙化适配指南
javascript·react native·react.js·electron·harmonyos
薛一半1 天前
React-Redux三重优化实战揭秘
javascript·vue.js·react.js
FungLeo3 天前
成为全栈·Next.js 网站前台篇·同源 BFF 代理:API、附件、Cookie 与跨站写入保护
react·csrf·next.js·bff·成为全栈·route handler
薛一半3 天前
React-Redux核心原理与容器组件实战
前端·javascript·react.js
FungLeo3 天前
成为全栈·Next.js 网站前台篇·评论系统:叠楼、回复、删除与内容审核如何落到前台
react.js·内容审核·next.js·评论系统·树形数据·成为全栈