用 CSS 和 JavaScript 实现常见网页特效代码

生活资讯2小时前发布 jdkjadmin
0 00
用 CSS 和 JavaScript 实现常见网页特效代码

用 CSS 和 JavaScript 实现常见网页特效代码

网页特效不只是让页面“动起来”。一个好的交互应当让用户看得出哪里可以操作、操作后发生了什么,并且在键盘、触屏和辅助技术环境下仍能使用。许多常见效果只需少量 CSS;涉及状态切换、滚动位置或弹层时,再用 JavaScript 补充。

本文从几个常见场景出发,提供可复制的代码片段。每段代码都以独立示例为目标;将 HTML 放在页面内容中,将 CSS 放入样式表,将 JavaScript 放在相应元素之后,或等 DOM 加载完成后运行。示例没有依赖第三方库。

一、先确定特效的边界

实现前先回答三个问题:这个效果是否必须依赖 JavaScript?用户如何知道它可以操作?关闭动画、键盘操作或 JavaScript 不可用时,页面是否仍可理解?如果只改变颜色、透明度或位移,优先用 CSS;如果要维护展开状态、切换内容或监听页面滚动,再使用 JavaScript。

动画不应代替信息本身。比如隐藏内容若对完成任务必不可少,应确保它能通过明确的按钮打开,而不是只能悬停才能看到。移动设备没有可靠的悬停状态,键盘用户也无法触发只写在 :hover 中的交互。

二、按钮和卡片的悬停反馈

悬停反馈适用于按钮、可点击卡片和导航链接。用颜色、边框或轻微位移提示状态即可;不要用大幅跳动制造干扰。还要为键盘用户提供 :focus-visible 样式,不能只定义悬停外观。

<button class="action-button" type="button">查看方案</button>
.action-button {
  border: 0;
  border-radius: 0.5rem;
  padding: 0.7rem 1rem;
  background: #2457d6;
  color: #fff;
  font: inherit;
  cursor: pointer;
  transition: background-color 160ms ease, transform 160ms ease;
}

.action-button:hover {
  background: #1743ad;
  transform: translateY(-2px);
}

.action-button:focus-visible {
  outline: 3px solid #ffbf47;
  outline-offset: 3px;
}

.action-button:active {
  transform: translateY(0);
}

这里的 transition 只对指定属性做过渡,避免使用 transition: all 让意外变化也产生动画。悬停状态不是点击状态的替代品:触屏设备可能不会显示悬停,按钮点击后的实际操作仍应由事件处理程序实现。

用户可能在系统设置中要求减少动态效果,可以用媒体查询缩短或移除动画:

@media (prefers-reduced-motion: reduce) {
  .action-button {
    transition: none;
  }

  .action-button:hover,
  .action-button:active {
    transform: none;
  }
}

三、原生手风琴:优先使用 details

常见问题、设置说明和折叠式目录适合使用手风琴。若只需要展开和收起文本,HTML 的 <details> 与 <summary> 通常比自写脚本更稳妥:它们自带交互语义,也可以通过键盘操作。

<details class="faq-item">
  <summary>这项服务需要安装吗?</summary>
  <p>不需要安装,使用浏览器即可访问。</p>
</details>

<details class="faq-item">
  <summary>可以随时取消吗?</summary>
  <p>请按服务页面列出的取消流程操作。</p>
</details>
.faq-item {
  max-width: 42rem;
  border-bottom: 1px solid #d6dbe5;
  padding: 0.9rem 0;
}

.faq-item summary {
  cursor: pointer;
  font-weight: 600;
}

.faq-item summary:focus-visible {
  outline: 2px solid #2457d6;
  outline-offset: 4px;
}

.faq-item p {
  margin: 0.75rem 0 0;
  line-height: 1.7;
}

如果产品要求“一次只能展开一项”,可以用同一组 name 值关联多个 details。该能力在较新的浏览器中可用,面向旧浏览器时应检查目标环境,或改用下文的按钮加脚本方案。

<details name="shipping-faq">
  <summary>配送范围是什么?</summary>
  <p>具体范围以结算页面显示为准。</p>
</details>
<details name="shipping-faq">
  <summary>如何查询物流?</summary>
  <p>下单后可在订单详情中查看物流信息。</p>
</details>

四、用 JavaScript 实现可控手风琴

需要统计展开状态、控制单项互斥,或与其他组件同步时,可以使用按钮和 aria-expanded。下面的实现支持多个面板同时展开;点击按钮会同步更新状态。按钮的 aria-controls 指向对应内容,便于辅助技术识别关联关系。

<section class="accordion">
  <h2>
    <button class="accordion-toggle" type="button"
      aria-expanded="false" aria-controls="panel-a" id="toggle-a">
      如何修改联系信息?
    </button>
  </h2>
  <div class="accordion-panel" id="panel-a" hidden>
    <p>登录后进入账户设置即可修改。</p>
  </div>
</section>

<script>
  document.querySelectorAll(".accordion-toggle").forEach((button) => {
    button.addEventListener("click", () => {
      const panelId = button.getAttribute("aria-controls");
      const panel = document.getElementById(panelId);
      if (!panel) return;

      const isOpen = button.getAttribute("aria-expanded") === "true";
      button.setAttribute("aria-expanded", String(!isOpen));
      panel.hidden = isOpen;
    });
  });
</script>

hidden 会让面板不显示,也不进入正常的键盘焦点顺序。不要一边用 hidden 隐藏内容,一边又通过强制设置 display 让它重新出现。若要为展开过程添加高度动画,需要额外处理高度测量、内容变化和减少动态效果等问题;简单场景不必为了动画牺牲可靠性。

五、标签页切换

标签页适用于同一区域内互斥的内容面板,例如产品介绍中的“概览、规格、评价”。如果内容之间不是同一组视图,而是普通页面导航,应使用链接,不要把导航伪装成标签页。

下面给出一个支持鼠标点击和方向键切换的最小实现。当前标签设置为 aria-selected="true",其余标签设为 tabindex="-1",内容面板通过 hidden 控制显示。

<div class="tabs" role="tablist" aria-label="产品信息">
  <button id="tab-overview" role="tab" type="button"
    aria-selected="true" aria-controls="panel-overview" tabindex="0">概览</button>
  <button id="tab-spec" role="tab" type="button"
    aria-selected="false" aria-controls="panel-spec" tabindex="-1">规格</button>
</div>

<section id="panel-overview" role="tabpanel"
  aria-labelledby="tab-overview">
  <p>这里是产品概览。</p>
</section>
<section id="panel-spec" role="tabpanel"
  aria-labelledby="tab-spec" hidden>
  <p>这里是产品规格。</p>
</section>

<script>
  const tabs = [...document.querySelectorAll('[role="tab"]')];

  function activateTab(nextTab) {
    tabs.forEach((tab) => {
      const selected = tab === nextTab;
      tab.setAttribute("aria-selected", String(selected));
      tab.tabIndex = selected ? 0 : -1;
      document.getElementById(tab.getAttribute("aria-controls")).hidden = !selected;
    });
    nextTab.focus();
  }

  tabs.forEach((tab, index) => {
    tab.addEventListener("click", () => activateTab(tab));
    tab.addEventListener("keydown", (event) => {
      let nextIndex = index;
      if (event.key === "ArrowRight") nextIndex = (index + 1) % tabs.length;
      else if (event.key === "ArrowLeft") nextIndex = (index - 1 + tabs.length) % tabs.length;
      else return;

      event.preventDefault();
      activateTab(tabs[nextIndex]);
    });
  });
</script>

这个示例按水平标签页处理,使用左右方向键。垂直标签布局通常应使用上下方向键;若要支持 Home、End 键,也应明确实现并测试。标签页、面板的 ID 必须唯一,且 aria-controls 指向的元素必须真实存在,否则切换代码可能报错或导致辅助技术无法关联内容。

六、对话框:使用原生 dialog

确认操作、显示简短表单或补充说明时,可以使用原生 <dialog>。调用 showModal() 会将对话框置于模态状态,并限制焦点在对话框中;原生实现通常比手写遮罩层更容易处理焦点和背景交互。

<button id="open-dialog" type="button">查看说明</button>

<dialog id="info-dialog" aria-labelledby="dialog-title">
  <h2 id="dialog-title">操作说明</h2>
  <p>提交前请确认信息无误。</p>
  <button id="close-dialog" type="button">关闭</button>
</dialog>

<script>
  const dialog = document.getElementById("info-dialog");
  const openButton = document.getElementById("open-dialog");
  const closeButton = document.getElementById("close-dialog");

  openButton.addEventListener("click", () => {
    if (!dialog.open) dialog.showModal();
    closeButton.focus();
  });

  closeButton.addEventListener("click", () => dialog.close());

  dialog.addEventListener("close", () => openButton.focus());
</script>
dialog {
  width: min(32rem, calc(100% - 2rem));
  border: 0;
  border-radius: 0.75rem;
  padding: 1.5rem;
  box-shadow: 0 1rem 3rem rgb(0 0 0 / 25%);
}

dialog::backdrop {
  background: rgb(0 0 0 / 55%);
}

dialog button:focus-visible {
  outline: 3px solid #2457d6;
  outline-offset: 3px;
}

不支持原生 dialog 的旧浏览器需要单独评估兼容策略,不能只假定所有环境都有相同表现。生产代码还应考虑按 Escape 关闭、关闭后恢复焦点、表单提交和错误提示等场景。不要仅靠一个视觉遮罩模拟模态框,却不管理焦点:用户可能仍能 Tab 到背景中的控件。

七、滚动进入视口时显现内容

卡片或分段内容进入视口时淡入,适合用于辅助浏览的非关键信息。使用 IntersectionObserver 可以避免每次滚动都手动计算元素位置。关键内容应默认可见,避免脚本加载失败时整页内容被隐藏。

<article class="reveal">
  <h2>服务特点</h2>
  <p>这段内容进入视口后显示。</p>
</article>

<style>
  .reveal {
    opacity: 1;
    transform: none;
  }

  @media (prefers-reduced-motion: no-preference) {
    .reveal.is-pending {
      opacity: 0;
      transform: translateY(12px);
      transition: opacity 300ms ease, transform 300ms ease;
    }

    .reveal.is-visible {
      opacity: 1;
      transform: none;
    }
  }
</style>

<script>
  const items = [...document.querySelectorAll(".reveal")];
  const canAnimate = window.matchMedia("(prefers-reduced-motion: no-preference)").matches;

  if (canAnimate && "IntersectionObserver" in window) {
    items.forEach((item) => item.classList.add("is-pending"));

    const observer = new IntersectionObserver((entries, currentObserver) => {
      entries.forEach((entry) => {
        if (!entry.isIntersecting) return;
        entry.target.classList.add("is-visible");
        currentObserver.unobserve(entry.target);
      });
    }, { threshold: 0.15 });

    items.forEach((item) => observer.observe(item));
  }
</script>

这里的 threshold: 0.15 是观察目标可见比例达到约 15% 时触发,不代表滚动距离或像素值。若浏览器不支持观察器,内容仍保持默认可见。不要在脚本执行前就给所有内容设置透明,否则 JavaScript 出错时用户可能看不到内容。大列表也不宜给每个元素叠加长时间动画,必要时应减少动画数量和位移幅度。

八、返回顶部按钮

长文章可以提供“返回顶部”按钮。按钮是否显示由滚动位置决定,点击后使用平滑滚动;减少动态效果的用户则使用即时滚动。以下示例在页面滚动超过 400 像素时显示按钮。

<button id="back-to-top" type="button" aria-label="返回页面顶部" hidden>
  ↑
</button>

<script>
  const topButton = document.getElementById("back-to-top");
  const motionPreference = window.matchMedia("(prefers-reduced-motion: reduce)");

  function updateTopButton() {
    topButton.hidden = window.scrollY < 400;
  }

  window.addEventListener("scroll", updateTopButton, { passive: true });
  updateTopButton();

  topButton.addEventListener("click", () => {
    window.scrollTo({
      top: 0,
      behavior: motionPreference.matches ? "instant" : "smooth"
    });
  });
</script>
#back-to-top {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  z-index: 10;
  width: 3rem;
  height: 3rem;
  border: 0;
  border-radius: 50%;
  background: #2457d6;
  color: white;
  font-size: 1.4rem;
  cursor: pointer;
}

显示阈值 400 是设计选择,不是浏览器限制,可根据页面长度调整。某些环境对 scrollTo 的平滑行为支持不同;如果项目需要覆盖较旧浏览器,应测试目标设备,并保证即使没有平滑效果也能回到顶部。若使用 CSS 的 scroll-behavior: smooth,同样应考虑减少动态效果偏好。

九、让通知自动消失,但不丢失可读性

短暂提示适合用于“已复制”或“已保存”等结果反馈。提示内容应使用文本节点写入,避免把外部数据拼成 HTML。下面的例子提供显式关闭按钮,并设置自动隐藏时间;如果提示是错误或重要状态,不应只短暂显示,可以让用户有足够时间阅读或主动关闭。

<div id="notice" role="status" aria-live="polite" hidden>
  <span id="notice-message"></span>
  <button id="notice-close" type="button">关闭提示</button>
</div>

<script>
  const notice = document.getElementById("notice");
  const message = document.getElementById("notice-message");
  const closeNotice = document.getElementById("notice-close");
  let noticeTimer;

  function showNotice(text) {
    window.clearTimeout(noticeTimer);
    message.textContent = text;
    notice.hidden = false;
    noticeTimer = window.setTimeout(() => {
      notice.hidden = true;
    }, 5000);
  }

  closeNotice.addEventListener("click", () => {
    window.clearTimeout(noticeTimer);
    notice.hidden = true;
  });

  // 示例:在需要反馈时调用
  // showNotice("内容已复制");
</script>

role="status" 和 aria-live="polite" 用于让辅助技术适时播报状态变化。若提示需要用户立即处理,可能应使用不同的语义和交互设计,而不是把重要错误当作普通状态自动消失。自动关闭时间也不是通用标准,应结合内容长度、使用场景和用户测试决定。

十、参数限制与安全注意事项

  • CSS 动画时长:毫秒数是作者设置的时长,不存在适用于所有页面的固定值。应避免过长、频繁或大幅移动的动画,并尊重 prefers-reduced-motion。
  • 滚动与阈值:IntersectionObserver 的 threshold 表示可见比例,取值范围为 0 到 1;滚动按钮中的像素阈值则是页面设计参数。不要把二者混为一谈。
  • 选择器和 ID:组件 ID 应唯一;使用 querySelector 时,选择器要符合 CSS 语法。动态 ID 若包含特殊字符,应正确转义,或改用已持有的 DOM 引用。
  • 数据与 HTML:将用户输入、接口返回值放进页面时,优先使用 textContent,不要直接拼接到 innerHTML。后者会把字符串解析为 HTML,未经可信消毒的数据可能造成跨站脚本风险。需要展示富文本时,应使用经过验证的消毒方案,而不是自制正则过滤。
  • 打开链接或执行操作:客户端交互不能替代服务器端权限校验。隐藏按钮、禁用控件或前端校验都不能保护接口;服务器仍需验证身份、权限、输入和业务规则。
  • 事件监听:滚动、尺寸变化等高频事件中不要执行昂贵的布局计算。可用被动监听改善不调用 preventDefault() 的滚动监听;需要节流、观察器或动画帧调度时,应根据实际性能问题选择,而不是机械添加。

十一、常见错误排查

  • 按钮没有反应:检查脚本是否在元素生成后执行、ID 是否拼写一致,以及浏览器控制台是否有异常。把脚本放在页面末尾或使用 defer 通常可以避免 DOM 尚未就绪的问题。
  • 面板状态和屏幕阅读器不一致:切换视觉显示时,也要同步更新 aria-expanded、aria-selected 等状态属性。不要只改颜色或 CSS 类名。
  • 动画导致内容无法访问:检查脚本加载失败时的默认样式,确保重要内容不是默认透明或不可见;测试键盘焦点、触屏和减少动态效果设置。
  • 标签页内容没有切换:确认每个标签的 aria-controls 与面板 ID 一一对应,且 ID 没有重复。切换时应只显示当前面板。
  • 移动端交互异常:不要要求用户悬停才能获得关键信息。可点击元素应使用按钮或链接,并设置足够清晰的焦点和按下状态。
  • 弹层关闭后焦点丢失:关闭时通常应把焦点还给打开弹层的控件,并测试 Escape、关闭按钮和表单错误等路径。

结语

实现常见网页特效,重点不是写出更多动画,而是选对语义和交互方式:简单折叠优先用原生元素,内容切换时同步维护状态,模态对话框要照顾焦点,滚动动画要有可见的默认回退。复制示例后,应在项目的实际浏览器、键盘操作和触屏环境中验证,并按内容重要性调整动画、阈值和提示时长。

© 版权声明

相关文章