半円のスライダーメニュー(上向き)

仕様:メニューのボタンを押すと、背景色と背景画像と表示されるテキストが切り替わる。 ボタンを押したところまで目印の白丸が動いていく。

画面録画 2025-01-27 114512.mp4

HTMLの構成

「menu-slider」の中に「slider-container」を設置。 テキスト部分の「menu-text」とメニュー切り替え用の「circular-slider」の2つに大きく分かれる。

    <!-- 上向き半円スライダー コンテンツメニュー ここから-->
    <section class="menu-slider">
      <div class="slider-container">
        <div class="menu-text">
          <h2>HTML & CSS</h2>
          <p>
            説明文を書くところ
          </p>
        </div>
        <div class="menu-text">
          <h2>Graphic Edit</h2>
          <p>
            説明文を書くところ
          </p>
        </div>
        <div class="menu-text">
          <h2>Office Soft</h2>
          <p>
            説明文を書くところ
          </p>
        </div>
        <div class="menu-text">
          <h2>Javascript</h2>
          <p>
            説明文を書くところ
          </p>
        </div>
        <div class="menu-text">
          <h2>PHP</h2>
          <p>
            説明文を書くところ
          </p>
        </div>
        <div class="circular-slider">
          <div class="indicator"></div>
          <div class="indicator-menu">
            <div>
              <span>HTML&CSS</span>
            </div>
            <div>
              <span>Graphic</span>
            </div>
            <div>
              <span>Office</span>
            </div>
            <div>
              <span>Javascript</span>
            </div>
            <div>
              <span>PHP</span>
            </div>
          </div>
          <div class="indicator-img"></div>
        </div>
      </div>
    </section>
    <!-- 上向き半円スライダー メニュー ここまで -->

<h2>はテキスト部分のタイトル <div>タグ内の<span>はメニューボタンの表示テキスト

CSSの記述

/* 上向き半円スライダー コンテンツメニュー */
.menu {
  background-color: #333;
}

.menu-slider {
  background: radial-gradient(var(--office-primary-clr), var(--office-secondary-clr));
  width: 95%;
  height: 80vw;
  min-height: 600px;
  max-height: 900px;
  padding: 1em 2em 0 2em;
  color: var(--white-clr);
  position: relative;
  overflow: hidden;
  text-align: center;
  transition: all 0.5s;
  border-radius: 15px 15px 0 0;
}

.menu-text:not(:nth-child(3)) {
  display: none;
}

.menu-text h2 {
  font-size: 3em;
  margin: 0.5em 0;
}

.menu-text p {
  max-width: 550px;
  line-height: 30px;
  margin: 0 auto;
  text-align: left;
}

.circular-slider {
  position: absolute;
  /* bottom: -18em; */
  bottom: -40vw;
  left: 50%;
  transform: translateX(-50%);
}

.circular-slider .indicator-img {
  width: 75vw;
  height: 75vw;
  max-width: 750px;
  max-height: 750px;
  background-image: url(../images/circle3.png);
  background-position: center;
  background-repeat: no-repeat;
  background-size: cover;
  transition: 300ms;
}

/* max-width max-height オリジナルは700px */
.indicator {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 80vw;
  height: 80vw;
  max-width: 800px;
  max-height: 800px;
  border: 2px var(--white-clr) solid;
  border-radius: 50%;
  transition: transform 300ms;
}

.indicator::before {
  content: "";
  position: absolute;
  top: -0.5em;
  left: 50%;
  transform: translateX(-50%);
  width: 1em;
  height: 1em;
  background-color: var(--white-clr);
  border-radius: 50%;
}

/* max-width max-height オリジナルは750px */
.indicator-menu,
.indicator-menu div {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 85vw;
  height: 85vw;
  max-width: 850px;
  max-height: 850px;
  border-radius: 50%;
}

.indicator-menu span {
  position: absolute;
  top: -1.25em;
  left: 50%;
  transform: translateX(-50%);
  cursor: pointer;
  transition: 200ms;
  text-align: center;
  width: 120px;
  padding: 0.4em 1em;
  font-weight: 700;
  border: 2px var(--white-clr) solid;
  border-radius: 20px;
  transition: all 0.3s;
}

.indicator-menu span:hover {
  border: 2px var(--accent-clr) solid;
  color: var(--accent-clr);
}

/* .indicator-menu span:hover {
  color: #ccc;
} */

.indicator-menu div:nth-child(1) {
  transform: translate(-50%, -50%) rotate(-58deg);
}

.indicator-menu div:nth-child(2) {
  transform: translate(-50%, -50%) rotate(-32deg);
}

.indicator-menu div:nth-child(3) {
  transform: translate(-50%, -50%) rotate(0deg);
}

.indicator-menu div:nth-child(4) {
  transform: translate(-50%, -50%) rotate(32deg);
}

.indicator-menu div:nth-child(5) {
  transform: translate(-50%, -50%) rotate(58deg);
}

@media screen and (min-width: 768px) {
  .circular-slider {
    bottom: -24em;
  }
}
/* 上向き半円スライダー コンテンツメニュー -----*/

カラーコードは変数を使っている。 「indicator-menu」というdivのブロックの角度を変えることで、メニューが動く仕組みになっている。

驚くほどpositionを設定するので、はじめは意味が分からず混乱した。

JSの記述

YouTubeを参考にしたので英語のまま持ってきた。

// 上半分の半円
// Get elements from the DOM
const slider = document.querySelector(".menu-slider");
const image = document.querySelector(".circular-slider .indicator-img");
const indicator = document.querySelector(".indicator");
const menuItems = document.querySelectorAll(".indicator-menu span");
const descriptions = document.querySelectorAll(".menu-text");

// Rotation value for each menu item
const rotationValues = [-58, -32, 0, 32, 58];

// Background colors for each menu item
const colors = [
  "radial-gradient( #40a8c4, #07689f)",
  "radial-gradient( #9c75ff, #6a42c2)",
  "radial-gradient( #41b3a2, #0d7c66)",
  "radial-gradient( #b09761, #665035)",
  "radial-gradient( #e15b7d, #af1740)",
];

// Image names to add for each menu item
const images = ["circle1", "circle2", "circle3", "circle4", "circle5"];

// Current menu item index
let itemIndex = 2;

// Function for rotating the slider
function rotate(rotationValue) {
  /* Rotate the image using the value
  we pass then this function is called */
  // 画像の傾きがコントールできないので不採用
  // image.style.transform = `rotate(${rotationValue}deg)`;
  /*Rotate the indicator using the value
  we pass when this function is called */
  indicator.style.transform = `translate(-50%, -50%) rotate(${rotationValue}deg)`;
}

// Loop through each menu item
menuItems.forEach((menuItem, i) => {
  // Add a click event to each menu item
  menuItem.addEventListener("click", () => {
    // Add the image url to the image for each menu item
    image.style.backgroundImage = "url(images/" + images[i] + ".png)";
    // Add the colors for each menu item
    slider.style.background = colors[i];
    /* Create the short counter-rotation
      before the actual rotation */
    /* If the menu item you click has a
      greater index value than the previous one */
    if (i > itemIndex) {
      // Make the counter-rotation counter-clockwise
      rotate(rotationValues[itemIndex] - 10);
    } else if (i < itemIndex) {
      // Make the counter-rotation clockwise
      rotate(rotationValues[itemIndex] + 10);
    } else {
      return "";
    }

    // Wait for the counter-rotation to finish
    setTimeout(() => {
      // Rotate using the rotationValues Array
      // Assign each array value to the corresponding menu item
      rotate(rotationValues[i]);
    }, 300);

    // Hide all descriptions
    descriptions.forEach((text) => {
      text.style.display = "none";
    });
    // Show only the corresponding description
    descriptions[i].style.display = "block";
    // Update the itemIndex variable to the current menu item index
    itemIndex = i;
  });
});

必要となる角度、色、画像の名前などを配列にして定数として定義しておく。メニューをクリックした場所のindex番号に応じた要素を表示させる。 動作の遊びとして、移動する逆側に少し(10px)だけ動いてから、移動するようになっている。

本来の仕様では、表示される画像も角度がついてくるのだが、人や動物などをちゃんと表示しようとすると、あらかじめ画像の表示角度を合わせておかないと傾いて表示されてしまう。この機能については、今回は採用を見送った。