• Gitee: https://gitee.com/yuziikuko/fixed-side-bar-list.git
  • GitHub: https://github.com/yuziikuko/Fixed_SideBar_List.git
  • 🍩 顶部导航栏

  • 滚动时固定在页面最上方。
  • 🍩 侧边菜单栏

  • 一般状态下和右侧列表构成 flex 左右布局。
  • 点击菜单滚动到右侧列表相应的红色标题上方。
  • 滚动距离进入中间白色内容区时固定在侧边。
  • 滚动距离离开中间白色内容区时恢复一般状态。
  • 滚动距离进入内容区右侧列表红色标题时,自动切换到相应的菜单栏。
  • 🍩 右侧列表区

  • 鼠标经过问题时显示白色背景和阴影,问题展开时无此效果。
  • 点击问题时切换内容展开/折叠状态。
  • 对于同一个菜单下的问题,每次有且仅有一个被展开,即点击其他问题会折叠当前展开的问题。
  • 同一个菜单下最多显示 5 个问题,超出的问题隐藏并显示 “Show more”。
  • 点击 “Show more” 显示当前菜单下所有问题,点击 “Hide” 隐藏当前菜单下超出数量的问题。
  • (一)基础页面结构

    🍩 仓库文件

    见分支:initial

    🍩 页面效果

    🍩 布局

  • 整体采用上中下布局,中间渲染侧边栏和问题列表。
  • 红色区域用于撑开页面高度,更好演示滚动效果。
  • <div class="my-header">这是粘性顶部导航栏</div>
    <div class="my-area">
      <p>这是一个区域</p>
    </div>
    <div class="my-menu-list">
      <div class="my-left-div">
        <div id="my_left_bar" class="my-left-bar"></div>
      </div>
      <div class="my-right-list">
        <div id="questions_list"></div>
      </div>
    </div>
    <div class="my-area">
      <p>这是一个区域</p>
    </div>
    <div class="my-area">
      <p>这是一个区域</p>
    </div>
    <div class="my-area">
      <p>这是一个区域</p>
    </div>
    <div class="my-footer">这是底部导航栏</div>
    

    🍩 样式

  • 对顶部导航栏开启 sticky粘性定位 使其不随页面滚动。
  • 同时设置 z-index: 9; 置于顶层,否则滚动时下方元素会遮挡导航栏。
  • /* container */
      margin: 0;
      padding: 0;
      border: 0;
      outline: none;
    body {
      background-color: #f0f2f5;
    .my-header {
      width: 100%;
      height: 80px;
      background-color: #ffffff;
      box-shadow: 0px 16px 40px 0px rgba(112, 144, 176, 0.2);
      color: #303030;
      text-align: center;
      font-size: 20px;
      font-weight: bold;
      display: flex;
      align-items: center;
      justify-content: center;
      /* 滚动时固定在顶部 */
      position: sticky;
      top: 0;
      z-index: 9;
    .my-area {
      width: 100%;
      height: 300px;
      background-color: #d8152a;
      color: #f0f2f5;
      margin: 0 auto;
      text-align: center;
      font-size: 20px;
      font-weight: bold;
      display: flex;
      align-items: center;
      justify-content: center;
    .my-footer {
      width: 100%;
      height: 80px;
      background-color: #ffffff;
      color: #303030;
      text-align: center;
      font-size: 20px;
      font-weight: bold;
      display: flex;
      align-items: center;
      justify-content: center;
    .my-menu-list {
      width: 1280px;
      height: 750px;
      margin: 100px auto;
      padding: 20px;
      display: flex;
      align-items: stretch;
      justify-content: flex-start;
      position: relative;
      overflow: auto;
    /* left menu */
    .my-left-div {
      width: 250px;
      position: relative;
    .my-left-bar {
      width: 100%;
      height: 100%;
      background-color: lightgrey;
    /* right list */
    .my-right-list {
      flex: 1;
      padding: 0 30px;
      background-color: lightcoral;
    

    (二)渲染左侧列表

    🍩 仓库文件

    见分支:leftbar

    🍩 页面效果

    准备菜单项图标

  • 可以将UI提供的图标文件放到项目目录下,以 img 形式引入。
  • 这里为了方便直接使用 iconfont 提供的 CDN
  • 官方文档-使用帮助
  • 步骤如下:
  • 选择心仪的图标添加到购物车。
  • 点击顶部右侧的小车车图标,将选中的图标添加到项目中。
  • 进入资源管理-我的项目菜单,选中上一步创建的项目,依次为图标重命名,选择 Font class 生成在线链接。
  • 点击生成的在线链接会自动打开css样式代码,全选粘贴到 style.css 文件中。
  • 页面中使用:<i class="iconfont icon-xxx"></i>
  • /* iconfont */
    @font-face {
      font-family: "iconfont"; /* Project id 3867129 */
      src: url("//at.alicdn.com/t/c/font_3867129_yw1h2xfi3p.woff2?t=1674110301689")
          format("woff2"),
        url("//at.alicdn.com/t/c/font_3867129_yw1h2xfi3p.woff?t=1674110301689")
          format("woff"),
        url("//at.alicdn.com/t/c/font_3867129_yw1h2xfi3p.ttf?t=1674110301689")
          format("truetype");
    .iconfont {
      font-family: "iconfont" !important;
      font-size: 16px;
      font-style: normal;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
    .icon-arrown-up:before {
      content: "\e616";
    .icon-arrown-down:before {
      content: "\e600";
    .icon-address:before {
      content: "\e608";
    .icon-user:before {
      content: "\e60a";
    .icon-form:before {
      content: "\e613";
    .icon-notification:before {
      content: "\e614";
    .icon-lib:before {
      content: "\e615";
    /* container */
    ......
    

    定义侧边栏菜单对象

  • id:菜单项编号,用于【点击切换菜单】事件。
  • content_id:菜单项对应的问题列表项,用于【点击滚动到相应列表】事件。
  • title:菜单项标题。
  • icon:菜单项图标。
  • is_active:菜单项激活状态,仅用于首次渲染时默认选中第一个菜单项。
  • * js/data.js // 侧边栏对象 const LEFT_BAR = [ id: "address", content_id: "address_content", title: "Address", icon: "address", is_active: true, id: "user", content_id: "user_content", title: "User", icon: "user", is_active: false, id: "form", content_id: "form_content", title: "Form", icon: "form", is_active: false, id: "notification", content_id: "notification_content", title: "Notification", icon: "notification", is_active: false, id: "lib", content_id: "lib_content", title: "Libruary", icon: "lib", is_active: false,

    index.html 页面中引入该对象:

    <script src="js/data.js"></script>
    

    引入JQuery

    <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Fixed Side Bar List</title > <link rel="stylesheet" href="css/style.css" /> <script src="js/jquery.min.js"></script> </head>

    渲染侧边栏

    <script>
        // ========== 左侧菜单 ==========
        // 渲染问答模块左侧菜单
        function displayLeftBar() {
          let html = "";
          $("#my_left_bar").fadeOut(300);
          $("#my_left_bar").html("");
          LEFT_BAR.forEach((item) => {
            html += `
              <div id="${item.id}" class="my-left-item ${
              item.is_active ? "active" : ""
                <i class="iconfont icon-${item.icon} my-left-icon"></i>
                <span class="my-left-title">${item.title}</span>
              </div>`;
            if (item.id !== "lib") {
              html += `<div class="my-left-space"></div>`;
          $("#my_left_bar").html(html);
          $("#my_left_bar").fadeIn(300);
        displayLeftBar();
    </script>
    
    .my-menu-list {
      width: 1280px;
      height: auto; /* 高度自动撑开 */
      margin: 100px auto;
      padding: 20px;
      display: flex;
      align-items: stretch;
      justify-content: flex-start;
      position: relative;
      overflow: auto;
    /* left menu */
    .my-left-div {
      /* 占位div,确保侧边栏固定时右侧列表能保持flex布局 */
      width: 250px;
      position: relative;
    .my-left-bar {
      width: max-content;
    .my-left-item {
      height: 50px;
      padding: 0 20px;
      display: flex;
      align-items: center;
      cursor: pointer;
      border-left: 2px solid #cecacb;
      transition: all linear 0.2s;
    .my-left-icon {
      color: #898989;
      margin-right: 10px;
    .my-left-title {
      color: #898989;
      font-size: 18px;
      font-weight: bold;
      text-align: left;
      flex: 1;
    .my-left-space {
      /* 空菜单项,形成菜单左侧轨道 */
      height: 50px;
      border-left: 2px solid #cecacb;
    .my-left-item.active {
      cursor: default;
      border-left: 2px solid #d8152a;
    .my-left-item.active .my-left-title,
    .my-left-item.active .my-left-icon {
      color: #d8152a;
    

    侧边栏点击切换事件

  • 通过添加/删除 active 类实现菜单项的样式切换。
  • 渲染菜单时为所有菜单项绑定点击事件,获取到当前点击的菜单项 id
  • 封装一个函数 changeLeftBar ,传入当前点击的菜单项 id,先遍历所有菜单项去除 active 类,再根据当前点击的菜单项 id 修改菜单项类。
  • 如果图标是使用 img 渲染,可以多传一个 icon 参数,函数中修改 imgsrc 属性值,实现图标激活的效果。
  • // ========== 左侧菜单 ==========
    // 侧边栏切换样式
    function changeLeftBar(id) {
      // 遍历菜单项置灰
      $(".my-left-item").each((index, bar) => {
        let barImg = $(`#${bar.id} .my-left-icon`);
        if ($(bar).hasClass("active")) {
          $(bar).removeClass("active");
          barImg.removeClass("active");
      // 激活当前点击元素
      let el = $(`#${id}`);
      let img = $(`#${id} .my-left-icon`);
      if (el.hasClass("active")) {
        el.removeClass("active");
      } else {
        el.addClass("active");
    // 渲染问答模块左侧菜单
    function displayLeftBar() {
      let html = "";
      $("#my_left_bar").fadeOut(300);
      $("#my_left_bar").html("");
      LEFT_BAR.forEach((item) => {
        html += `
          <div id="${item.id}" class="my-left-item ${
          item.is_active ? "active" : ""
        }" onclick="changeLeftBar('${item.id}')">
            <i class="iconfont icon-${item.icon} my-left-icon"></i>
            <span class="my-left-title">${item.title}</span>
          </div>`;
        if (item.id !== "lib") {
          html += `<div class="my-left-space"></div>`;
      $("#my_left_bar").html(html);
      $("#my_left_bar").fadeIn(300);
    displayLeftBar();
    

    (三)渲染右侧列表

    🍩 仓库文件

    见分支:rightlist

    🍩 页面效果

    定义问题列表对象

  • id:问题列表项,用于【点击滚动到相应列表】事件。
  • title:对应的菜单项标题。
  • questions:同一菜单下的问题列表数组。
  • id:问题编号。
  • title:问题标题。
  • contents:问题内容。
  • * js/data.js // 侧边栏对象 const LEFT_BAR = [ // ... // 问题列表对象 const RIGHT_LIST = [ id: "address_content", title: "Address", questions: [ id: "1", title: "What...?", contents: ` Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad. </ol>`, id: "2", title: "Why...?", contents: ` Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad. </p>`, id: "3", title: "How...?", contents: ` Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad. </p>`, id: "4", title: "Which...?", contents: ` Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad. </ul>`, id: "5", title: "How...?", contents: ` Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad. </p>`, id: "6", title: "How...?", contents: ` Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad. </p>`, id: "7", title: "How...?", contents: ` Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam in aliquam ex quod distinctio? Architecto, repellat illum fuga laudantium earum quo dolore corrupti voluptatem quae magnam, sunt recusandae, nam ad. </p>`, id: "user_content", title: "User", questions: [ // ... id: "form_content", title: "Form", questions: [ // ... id: "notification_content", title: "Notification", questions: [ // ... id: "lib_content", title: "Libruary", questions: [ // ...

    渲染问题列表

    // ========== 右侧列表 ==========
    // 渲染问答模块右侧列表
    function
    
    
    
    
        
     displayRightList() {
      $("#questions_list").fadeOut(300);
      $("#questions_list").html("");
      RIGHT_LIST.forEach((list) => {
        let html = `
          <div class="my-right-item" id=${list.id}>
            <div class="my-right-title">${list.title}</div>`;
        list.questions.forEach((question) => {
          html += `
            <div class="my-right-content-div">
              <div class="my-right-content-title" id="${question.id}">
                <div class="my-right-content-question">
                  ${question.title}
                <i class="iconfont icon-arrown-down my-right-content-arrow"></i>
              <div class="my-right-content">
                ${question.contents}
            </div>`;
        html += "</div>";
        $("#questions_list").append(html);
      $("#questions_list").fadeIn(300);
    displayRightList();
    
    /* right list */
    .my-right-list {
      flex: 1;
      padding: 0 30px;
    .my-right-item {
      width: 100%;
      margin-bottom: 200px;
    .my-right-title {
      font-size: 24px;
      font-weight: bold;
      color: #d8152a;
      text-align: left;
      padding-left: 15px;
      margin-bottom: 30px;
    .my-right-content-div {
      width: 100%;
      margin-top: 50px;
    .my-right-content-title {
      padding: 20px 15px;
      border-bottom: 2px solid #e0e0e0;
      display: flex;
      align-items: center;
      justify-content: space-between;
      cursor: pointer;
    /* 鼠标悬浮时显示背景颜色和阴影 */
    .my-right-content-title:hover {
      background-color: #ffffff;
      box-shadow: 0px 16px 40px 0px rgba(112, 144, 176, 0.2);
      transition: all linear 0.2s;
    /* 当前问题激活时鼠标悬浮不显示背景颜色和阴影 */
    .my-right-content-title.active:hover {
      background-color: transparent;
      box-shadow: none;
    .my-right-content-question {
      font-size: 18px;
      font-weight: bold;
      color: #303030;
      text-align: left;
      transition: all linear 0.2s;
    .my-right-content-question.active {
      color: #d8152a;
    .my-right-content-arrow {
      width: 14px;
      height: 14px;
      transform: rotate(0deg);
      transition: all linear 0.2s;
    .my-right-content-arrow.active {
      transform: rotate(180deg);
    /* 问题内容的样式 */
    .my-right-content {
      padding: 15px;
      display: none;
    .my-right-content ul,
    .my-right-content ol {
      margin: 0;
      padding: 0 0 0 20px;
    .my-right-content ul li,
    .my-right-content ol li,
    .my-right-content p {
      text-align: justify;
      text-justify: inter-word;
      font-size: 16px;
      color: #555555;
      line-height: 1.5;
      margin-bottom: 20px;
    .my-right-content a {
      text-decoration: none;
      color: #d8152a;
    .my-right-content a:hover {
      border-bottom: 1px solid #d8152a;
    .my-right-content-paragraph {
      font-size: 18px;
      font-weight: bold;
      color: #303030;
      text-align: left;
      padding: 0 15px 15px;
    

    列表项点击切换事件

  • 通过添加/删除 active 类实现列表项的样式切换。
  • 为所有列表项标题绑定点击事件,获取到当前点击的列表项DOM元素。
  • 封装一个函数 handleQuestionsChanges ,传入当前点击的列表项DOM元素,先遍历当前DOM元素所属菜单下的所有列表项,清除激活状态,再激活当前点击的列表项。
  • 封装一个函数 changeQuestionsActiveClass ,传入要切换激活状态的列表项DOM元素是否是当前点击的DOM元素,分别获取到DOM元素的子节点:问题标题、下拉图标、问题内容区域,去除 active 类,再为当前点击的列表项下的三个子节点添加 active 类。
  • // 修改问题激活样式
    function changeQuestionsActiveClass(el, isCurrent = false) {
      let title = el.children(".my-right-content-question");
      let arrow = el.children(".my-right-content-arrow");
      let content = el.siblings(".my-right-content");
      if (el.hasClass("active")) {
        el.removeClass("active");
        title.removeClass("active");
        arrow.removeClass("active");
        content.slideUp(300);
      } else {
        if (isCurrent) {
          el.addClass("active");
          title.addClass("active");
          arrow.addClass("active");
          content.slideDown(300);
    // 问答折叠
    function handleQuestionsChanges(el) {
      // 清除当前类别下的激活问题
      let otherEl = el
        .parent(".my-right-content-div")
        .siblings(".my-right-content-div")
        .children(".my-right-content-title");
      changeQuestionsActiveClass(otherEl);
      // 激活当前点击的问题
      changeQuestionsActiveClass(el, true);
    // 渲染问答模块右侧列表
    function displayRightList() {
      $("#questions_list").fadeOut(300);
      $("#questions_list").html("");
      RIGHT_LIST.forEach((list) => {
        let html = `
          <div class="my-right-item" id=${list.id}>
            <div class="my-right-title">${list.title}</div>`;
        list.questions.forEach((question) => {
          html += `
            <div class="my-right-content-div">
              <div class="my-right-content-title" id="${question.id}">
                <div class="my-right-content-question">
                  ${question.title}
                <i class="iconfont icon-arrown-down my-right-content-arrow"></i>
              <div class="my-right-content">
                ${question.contents}
            </div>`;
        html += "</div>";
        $("#questions_list").append(html);
      $("#questions_list").fadeIn(300);
      // 点击问题折叠同类目下其他问题,再展开自身
      $(".my-right-content-title").click(function () {
        handleQuestionsChanges($(this));
    displayRightList();
    

    (四)页面滚动时吸附侧边栏

    🍩 仓库文件

    见分支:fixedleft

    🍩 页面效果

  • 定义两个样式,改变侧边栏DOM元素的 position定位属性 ,进入滚动区域时吸附,回到页面顶部或者离开滚动区域时恢复初始状态。
  • 侧边栏悬浮时,每一个菜单项高度改为以视口高度 vh 为单位,避免小屏幕菜单溢出屏幕。
  • /* 菜单正常样式 */
    .my-sidebar-static {
      height: auto;
      position: static;
      transition: all linear 0.2s;
    /* 菜单粘性样式 */
    .my-sidebar-fixed {
      height: 90vh;
      position: fixed;
      top: 120px;
      transition: all linear 0.2s;
    .faq-sidebar-fixed .faq-left-item {
      height: 5vh;
    .faq-sidebar-fixed .faq-left-space {
      height: 5vh;
    

    吸附侧边栏

  • 使用 addEventListener 监听页面滚动,获取到侧边栏DOM元素 sideBar视窗滚动距离 windowTop中间滚动区域顶部位置 divTop中间滚动区域底部位置(即该区域高度) divBottom 四个属性值。
  • // 页面滚动时吸附侧边栏
    window.addEventListener("scroll", function () {
      let sideBar = $("#my_left_bar");
      let windowTop = $(window).scrollTop();
      let divTop = $(".my-menu-list").offset().top - 100;
      let divBottom = $(".my-menu-list").height();
      // 滚动到问题列表顶部时吸附
      if (windowTop >= divTop) {
        sideBar.removeClass("my-sidebar-static").addClass("my-sidebar-fixed");
      // 回到页面顶部或滚动出问题列表底部时恢复
      if (windowTop < divTop || windowTop >= divBottom) {
        sideBar.removeClass("my-sidebar-fixed").addClass("my-sidebar-static");
    

    (五)切换菜单项

    🍩 仓库文件

    见分支:scrolled

    🍩 页面效果

    页面滚动效果

  • 封装一个函数,传入DOM元素和偏移量,滚动页面至元素顶部。
  • // 滚动动效
    function scrollToElementTop(el, offset = 200) {
      $("html,body").animate(
          scrollTop: el.offset().top - offset,
    

    点击菜单项时切换

  • 修改 changeLeftBar 函数,增加传入参数 是否需要滚动 ,默认 true ,滚动列表切换菜单项时不需要自动滚动,否则页面会卡死。
  • // ========== 左侧菜单 ==========
    // 侧边栏切换样式
    function changeLeftBar(id, needScroll = true) {
      // 遍历菜单项置灰
      $(".my-left-item").each((index, bar) => {
        let barImg = $(`#${bar.id} .my-left-icon`);
        if ($(bar).hasClass("active")) {
          $(bar).removeClass("active");
          barImg.removeClass("active");
      // 激活当前点击元素
      let el = $(`#${id}`);
      let img = $(`#${id} .my-left-icon`);
      if (el.hasClass("active")) {
        el.removeClass("active");
      } else {
        el.addClass("active");
      needScroll && scrollToElementTop($(`#${id}_content`));
    

    页面滚动时切换

  • 定义一个空数组 menuTops,储存每个菜单项对象的属性值 以及 相应的问题列表标题顶部距离 offsetTop,最后在数组内存储中间滚动区域底部边界的顶部距离作占位元素,确保在遍历激活菜单项时可以定位到最后一个菜单。
  • 遍历 menuTops 数组,当视窗滚动距离 大于等于 当前遍历到的内容标题顶部距离,且小于 下一个遍历到的内容标题顶部距离 时,激活当前问题对应的左侧菜单项,即在哪个问题列表项内部区域滚动时就激活哪个菜单项。
  • // 页面滚动时吸附侧边栏
    window.addEventListener("scroll", function () {
      let sideBar = $("#my_left_bar");
      let windowTop = $(window).scrollTop();
      let divTop = $(".my-menu-list").offset().top - 100;
      let divBottom = $(".my-menu-list").height();
      // 滚动到问题列表顶部时吸附
      if (windowTop >= divTop) {
        sideBar.removeClass("my-sidebar-static").addClass("my-sidebar-fixed");
      // 回到页面顶部或滚动出问题列表底部时恢复
      if (windowTop < divTop || windowTop >= divBottom) {
        sideBar.removeClass("my-sidebar-fixed").addClass("my-sidebar-static");
      // 滚动到问题标题顶部时激活相应侧边栏菜单项
      let menuTops = [];
      LEFT_BAR.forEach((menu) => {
        menuTops.push({
          ...menu,
          offsetTop: $(`#${menu.content_id}`)?.offset()?.top - 300,
      menuTops.push({
        id: "my_bottom",
        content_id: "my_bottom",
        offsetTop: divTop + divBottom,
      for (let i = 0; i < menuTops.length - 1; i++) {
          windowTop >= menuTops[i].offsetTop &&
          windowTop < menuTops[i + 1].offsetTop
          changeLeftBar(menuTops[i].id, false);
    

    (六)问题列表溢出隐藏

    🍩 仓库文件

    见分支:showmore

    🍩 页面效果

    渲染问题列表时限制数量

  • 根据问题列表的每一个 questionid 判断当前问题序号,序号小于等于5正常渲染,大于5时最外层 div 附带 my-right-content-more-div 类,用于设置隐藏样式。
  • 问题总数超过5个时在渲染完问题列表后追加 “Show more” 和 “Hide” 文本按钮,用于点击切换。
  • // 渲染问答模块右侧列表
    function displayRightList() {
      $("#questions_list").fadeOut(300);
      $("#questions_list").html("");
      RIGHT_LIST.forEach((list) => {
        let html = `
          <div class="my-right-item" id=${list.id}>
            <div class="my-right-title">${list.title}</div>`;
        list.questions.forEach((question) => {
          if (parseInt(question.id) <= 5) {
            // 前五个问题正常显示
            html += `
              <div class="my-right-content-div">
                <div class="my-right-content-title" id="${question.id}">
                  <div class="my-right-content-question">
                    ${question.title}
                  <i class="iconfont icon-arrown-down my-right-content-arrow"></i>
                <div class="my-right-content">
                  ${question.contents}
              </div>`;
          } else {
            // 超过五个问题时带上my-right-content-more-div类
            html += `
              <div class="my-right-content-div my-right-content-more-div">
                <div class="my-right-content-title" id="${question.id}">
                  <div class="my-right-content-question">
                    ${question.title}
                  <i class="iconfont icon-arrown-down my-right-content-arrow"></i>
                <div class="my-right-content">
                  ${question.contents}
              </div>`;
            if (parseInt(question.id) === list.questions.length) {
              // 超过五个问题时末尾追加Show more文本按钮
              html += `
              <div class="my-right-content-more">
                <span>Show more</span>
                <i class="iconfont icon-arrown-down my-right-content-arrow"></i>
              </div>`;
        html += "</div>";
        $("#questions_list").append(html);
      $("#questions_list").fadeIn(300);
      // 点击问题折叠同类目下其他问题,再展开自身
      $(".my-right-content-title").click(function () {
        handleQuestionsChanges($(this));
    

    点击显示/隐藏溢出问题

  • 获取到溢出的问题DOM元素,利用JQuery封装的 slideDownslideUp 函数控制元素显示/隐藏。
  • // 渲染问答模块右侧列表
    function displayRightList() {
      $("#questions_list").fadeOut(300);
      $("#questions_list").html("");
      RIGHT_LIST.forEach((list) => {
        let html = `
          <div class="my-right-item" id=${list.id}>
            <div class="my-right-title">${list.title}</div>`;
        list.questions.forEach((question) => {
          if (parseInt(question.id) <= 5) {
            // 前五个问题正常显示
            html += `
              <div class="my-right-content-div">
                <div class="my-right-content-title" id="${question.id}">
                  <div class="my-right-content-question">
                    ${question.title}
                  <i class="iconfont icon-arrown-down my-right-content-arrow"></i>
                <div class="my-right-content">
                  ${question.contents}
              </div>`;
          } else {
            // 超过五个问题时带上my-right-content-more-div类
            html += `
              <div class="my-right-content-div my-right-content-more-div">
                <div class="my-right-content-title" id="${question.id}">
                  <div class="my-right-content-question">
                    ${question.title}
                  <i class="iconfont icon-arrown-down my-right-content-arrow"></i>
                <div class="my-right-content">
                  ${question.contents}
              </div>`;
            if (parseInt(question.id) === list.questions.length) {
              // 超过五个问题时末尾追加Show more文本按钮
              html += `
              <div class="my-right-content-more">
                <span>Show more</span>
                <i class="iconfont icon-arrown-down my-right-content-arrow"></i>
              </div>`;
        html += "</div>";
        $("#questions_list").append(html);
      $("#questions_list").fadeIn(300);
      // 点击问题折叠同类目下其他问题,再展开自身
      $(".my-right-content-title").click(function () {
        handleQuestionsChanges($(this));
      // 点击 Show more / hide 时切换超过五个的问题显示状态
      $(".my-right-content-more").click(function () {
        let moreDiv = $(this).siblings(".my-right-content-more-div");
        let textSpan = $(this).children("span");
        let textIcon = $(this).children(".my-right-content-arrow");
        if (textSpan.html() === "Show more") {
          moreDiv.slideDown(300);
          textSpan.html("Hide");
          textIcon.removeClass("icon-arrown-down").addClass("icon-arrown-up");
        } else {
          moreDiv.slideUp(300);
          textSpan.html("Show more");
          textIcon.removeClass("icon-arrown-up").addClass("icon-arrown-down");
    

    Show more / Hide 样式

    .my-right-content-more-div {
      display: none;
    .my-right-content-more {
      display: block;
      text-align: center;
      margin: 50px auto 0;
      cursor: pointer;
      width: fit-content;
      font-size: 14px;
      color: #d8152a;
    

    (七)适配移动端

    🍩 仓库文件

    见分支:mobile

    🍩 页面效果

    使用@media媒体查询定义不同样式

  • 768px 作为屏幕分界线。
  • 使用 vw 作为单位。
  • @media only screen and (max-width: 767px) {
      .my-menu-list {
        width
    
    
    
    
        
    : 100%;
        height: 100%;
        margin: 0 auto;
        padding: 0;
        display: flex;
        flex-direction: column;
        align-items: stretch;
        justify-content: flex-start;
        position: relative;
        overflow: auto;
      /* left menu */
      .my-left-div {
        width: 100%;
        margin: 0 auto 10vw;
        overflow-x: auto;
        position: relative;
      /* 隐藏横向滚动条 */
      .my-left-div::-webkit-scrollbar,
      .my-sidebar-fixed::-webkit-scrollbar {
        display: none;
      .my-left-bar {
        width: max-content;
        display: flex !important;
        align-items: center;
      .show-head-bg {
        box-shadow: none !important;
      .my-sidebar-fixed {
        z-index: 2;
        width: 100vw;
        height: auto;
        overflow-x: auto;
        position: fixed;
        top: 20vw;
        background-color: #ffffff;
        box-shadow: 0px 16px 40px 0px rgba(112, 144, 176, 0.2);
        transition: all linear 0.2s;
      .my-sidebar-static {
        position: static;
        transition: all linear 0.2s;
      .my-left-item {
        height: 12vw;
        margin: 0 5vw;
        padding: 5vw 1.5vw 0;
        display: flex;
        align-items: center;
        transition: all linear 0.5s;
        border-bottom: 1vw solid transparent;
        border-left: none;
      .my-left-icon {
        margin-right: 1.5vw;
        font-size: 4vw;
      .my-left-title {
        color: #898989;
        font-size: 4vw;
        font-weight: bold;
        text-align: left;
        flex: 1;
      .my-left-space {
        display: none;
      .my-left-item.active {
        border-bottom: 1vw solid #d8152a;
        border-left: none;
      .my-left-item.active .my-left-title {
        color: #d8152a;
      /* right list */
      .my-right-list {
        flex: 1;
        padding: 0 5vw;
      .my-right-item {
        width: 100%;
        margin-bottom: 30vw;
      .my-right-title {
        font-size: 5vw;
        font-weight: bold;
        color: #d8152a;
        text-align: left;
        padding-left: 3vw;
        margin-bottom: 10vw;
      .my-right-content-div {
        width: 100%;
        margin-top: 5vw;
      .my-right-content-more-div {
        display: none;
      .my-right-content-more {
        display: block;
      .my-right-content-title {
        padding: 3vw;
        border-bottom: 0.5vw solid #e0e0e0;
        display: flex;
        align-items: flex-start;
        justify-content: space-between;
      .my-right-content-title:hover {
        background-color: transparent;
        box-shadow: none;
        transition: none;
      .my-right-content-question {
        font-size: 4vw;
        font-weight: bold;
        color: #303030;
        text-align: left;
        transition: all linear 0.2s;
      .my-right-content-question.active {
        color: #d8152a;
      .my-right-content-arrow {
        width: 4vw;
        height: 4vw;
        transform: rotate(0deg);
        transition: all linear 0.2s;
      .my-right-content-arrow.active {
        transform: rotate(180deg);
      .my-right-content {
        padding: 3vw;
        display: none;
      .my-right-content ul,
      .my-right-content ol {
        margin: 0;
        padding: 0 0 0 3vw;
      .my-right-content ul li,
      .my-right-content ol li,
      .my-right-content p {
        text-align: justify;
        text-justify: inter-word;
        font-size: 3vw;
        color: #555555;
        line-height: 1.5;
        margin-bottom: 5vw;
      .my-right-content a {
        text-decoration: none;
        color: #d8152a;
      .my-right-content a:hover {
        border-bottom: 0.5vw solid #d8152a;
      .my-right-content-paragraph {
        font-size: 3vw;
        font-weight: bold;
        color: #303030;
        text-align: left;
        padding: 0 3vw 3vw;
      .my-right-content-more {
        width: 100%;
        margin: 5vw 0;
        font-size: 3vw;
        color: #d8152a;
        display: flex;
        align-items: center;
        justify-content: center;
      .my-right-content-more span {
        margin-right: 3vw;
      .my-right-content-more .my-right-content-arrow {
        width: 3vw;
        height: 3vw;
        transform: rotate(0deg);
        transition: all linear 0.2s;
      .my-right-content-more .my-right-content-arrow.active {
        transform: rotate(-180deg);
    

    判断当前视口是否为移动端

    //判断设备
    function isMobile() {
      return window.innerWidth < 768;
    

    滚动时切换选中菜单项

  • 在原先的 menuTops 数组基础上,额外记录每一项菜单对应的左侧偏移量 offsetLeft ,表示横向滚动菜单内的每一项初始时距离视口左侧的距离。
  • 切换菜单时判断是否为移动设备,是则使用JQuery的 animate() 函数横向滚动到对应菜单项。
  • // 页面滚动时吸附侧边栏
    window.addEventListener("scroll", function () {
      let sideBar = $("#my_left_bar");
      let windowTop = $(window).scrollTop();
      let divTop = $(".my-menu-list").offset().top - 100;
      let divBottom = $(".my-menu-list").height();
      // 滚动到问题列表顶部时吸附
      if (windowTop >= divTop) {
        sideBar.removeClass("my-sidebar-static").addClass("my-sidebar-fixed");
      // 回到页面顶部或滚动出问题列表底部时恢复
      if (windowTop < divTop || windowTop >= divBottom) {
        sideBar.removeClass("my-sidebar-fixed").addClass("my-sidebar-static");
      // 滚动到问题标题顶部时激活相应侧边栏菜单项
      let menuTops = [];
      LEFT_BAR.forEach((menu) => {
        menuTops.push({
          ...menu,
          offsetTop: $(`#${menu.content_id}`)?.offset()?.top - 300,
          offsetLeft: $(`#${menu.id}`)?.offset()?.left - 20,
      menuTops.push({
        id: "my_bottom",
        content_id: "my_bottom",
        offsetTop: divTop + divBottom,
        offsetLeft: 0,
      for (let i = 0; i < menuTops.length - 1; i++) {
          windowTop >= menuTops[i].offsetTop &&
          windowTop < menuTops[i + 1].offsetTop
          if (isMobile()) {
            $("#my_left_bar").animate(
                scrollLeft:
                  menuTops[i].offsetLeft -
                  $("#my_left_bar").offset().left +
                  $("#my_left_bar").scrollLeft(),
          changeLeftBar(menuTops[i].id, false);
    
  • 以上就是本篇文章全部内容,一个非常常见且十分简单的小功能,视觉样式来自公司UI小姐姐~
  • 一开始看到设计稿还担心实现很复杂,一步一步拆解再整理出来才发现都是很基础的交互逻辑。
  • 希望能帮助到有需要的小伙伴,有不足之处欢迎指出😃
  • 分类:
    前端