개발 기술/css 애니메이션 (with js)

[Effect] 메뉴 버튼 center 효과

by GicoMomg (Lux) 2022. 5. 22.

😊 이번 시간에는 메뉴에서 활성화된 버튼을 중앙에 배치하는 효과를 만들어보았다!


1. 미리보기

See the Pen button center efffect by KumJungMin (@kumjungmin) on CodePen.



2. 코드 분석

1) html

  • html을 보면 전체를 감싸는 .container.list가 여러 개의 .btn을 감싸는 구조이다.

<div>
  <h2 class="text">You cliked "<span class="result">?</span> "</h2>
</div>
<br/>

<!--메뉴 부분-->
<div class="container">
  <div class="list">
    <button class="btn" onclick="onClickBtn(this)"><span class="text">apple</span></button>
    <button class="btn" onclick="onClickBtn(this)"><span class="text">pineapple</span></button>
    <button class="btn" onclick="onClickBtn(this)"><span class="text">orange</span></button>
    <button class="btn" onclick="onClickBtn(this)"><span class="text">pear</span></button>
    <button class="btn" onclick="onClickBtn(this)"><span class="text">banana</span></button>
    <button class="btn" onclick="onClickBtn(this)"><span class="text">tomato</span></button>
    <button class="btn" onclick="onClickBtn(this)"><span class="text">orange</span></button>
    <button class="btn" onclick="onClickBtn(this)"><span class="text">pear</span></button>
  </div>
</div>



2) CSS(SCSS)

(1) body에 기본 스타일 지정하기

  • body 내부 요소들이 수직, 수평 중앙 정렬이 되도록 flex를 사용한다.
body {
  height: 100vh;            /* 수직중앙을 위해 높이 설정 필수 */
  display: flex;            /* 중앙배치 */
  flex-direction: column; 
  align-items: center;      /* 중앙배치 */
  justify-content: center;  /* 중앙배치 */
  background-color: #010a01;
}

(2) list를 감싸는 container 요소

  • .container는 스크롤이 발생하는 .list를 감싸는 역할을 한다.


  • [1] 먼저, .container의 너비와 높이를 지정해준다.
  • [2] 특정 너비 이상인 경우에 자식요소(.*list*)가 벗어나지 않게 overflow:hidden을 해준다.
.container {
  position: relative;
  width: 100%;          /* [1] */
  max-width: 400px;     /* [1] */
  height: 50px;         /* [1] */
  overflow: hidden;     /* [2] */     
  border: 1px solid #fff;
  border-radius: 30px;
}

(3) btn을 감싸는 list 요소

  • .list.btn을 감싸는 부모 클래스로, 스크롤이 발생하는 요소이다.


  • [1] 스크롤이 발생할 수 있도록 overflow-x: auto를 지정한다.
  • [2] 자식 클래스(.btn)이 수직 중앙이 되도록 align-items: center를 한다.
  • [3] gap을 사용해 자식 클래스 간에 간격을 준다.(flex로 설정해야 사용가능)
.list {
  height: inherit;
  overflow-x: auto;       /* [1] */
  display: flex;          /* [2] */
  align-items: center;    /* [2] */
  gap: 12px;              /* [3] */
  padding: 0 4px;
}

(4) btn 스타일 지정하기

  • .btn에는 원하는 스타일을 지정하면 된다.


  • 본인이 지정한 스타일은 아래와 같다.
.btn {
  padding: 3px 17px;
  font-size: 24px;
  border-radius: 20px;
  border: 1px solid #fff;
  color: #fff;
  background-color: #010a01;
  cursor: pointer;
  &.active {
    background-color: red;
  }
}

(3) 기타 스타일 설정하기

  • 모든 글자에 네온 효과를 주기위해 .texttext-shadow를 지정했다.
.text {
  color: #fff;
  text-shadow:
    0 0 7px #fff,
    0 0 10px #fff,
    0 0 21px #fff,
    0 0 42px #0fa,
    0 0 82px #0fa,
    0 0 92px #0fa,
    0 0 102px #0fa,
    0 0 151px #0fa;
}

  • 현재 클릭한 버튼의 내용을 나타나는 .result에는 아래와 같이 스타일링했다.

.result {
  font-size: 30px;
  font-style: italic;
}

  • 마지막으로 스크롤바가 보이지 않도록 scrollbardisplay: none을 지정했다.
::-webkit-scrollbar {
  display: none;
}



3) JS

(1) 버튼 중앙 배치 함수

  • 클릭된 버튼을 중앙에 배치하기 위해서는 .container의 너비, 클릭된 .btn의 너비offsetLeft가 필요하다.


🤔 클릭된 .btn의 offsetLeft값은 왜 필요할까?

  • 우리는 아이템4를 클릭했고, 이 요소가 .container를 기준으로 중앙에 위치해야 한다.
  • 먼저, .list를 기준으로 아이템4의 offsetLeft값을 알아야 한다.
  • offsetLeft값은 곧, 아이템4가 container 시작 부분에 나타나기 위해 알아야할 스크롤 값과 같다.
  • offsetLeft값만으로 scrollLeft를 지정하면 아래와 같이 .container 시작 부분에 배치된다.


👀 .container, 클릭된 .btn의 너비는 왜 필요할까?

  • 앞서 우리는 .btnoffsetLeftscrollLeft값을 지정했다.
  • 하지만 실제 우리가 원하는 건 .btn.container를 기준으로 중앙에 위치하는 것이다.
  • 그러기 위해서는 아래 그림처럼 .container의 너비의 반 만큼 스크롤을 우측으로 이동하고, 클릭한 .btn의 너비의 반만큼 좌측으로 이동해야한다.


💡 앞서 설명한 내용을 코드로 나타내면 아래와 같다.

const container = document.querySelector('.container');
const scrollDom = document.querySelector('.list');
const result = document.querySelector('.result');
function onClickBtn(target) {
  const btnWidth = target.clientWidth;  // 클릭한 버튼의 너비
  const btnLeft = target.offsetLeft;    // 클릭한 버튼의 offsetLeft
  const center = container.clientWidth - btnWidth;
  const left = btnLeft - center / 2;
  scrollDom.scrollTo({ left, behavior: "smooth" });
}

(2) 마우스 휠 이벤트 변환 함수

  • 마우스 휠을 위, 아래로 움직였을 때 메뉴의 scrollLeft가 변화도록 해보았다.
  • wheel이벤트가 발생할 때마다 스크롤이 발생하는 scrollDomscrollLeft값을 변경해준다.
scrollDom.addEventListener('wheel', (e)=> {
  e.preventDefault();
  scrollDom.scrollLeft += e.deltaY;
})

(3) 버튼 active 클래스 추가 함수

  • 클릭된 버튼에 active 클래스를 추가하여 별도 스타일링을 하고자 한다.
  • [1] 반복문을 이용해 btn들에 active 클래스가 있는지 체크하고 있다면 제거한다.
  • [2] 그 다음, 클릭된 target에만 active 클래스를 추가해준다.
const btns = document.querySelectorAll('.btn');

function addActiveClass(target) {
  for (const btn of btns) {
    // [1] 만약 btn에 active 클래스가 있다면 제거
    const isActive = btn.classList.contains('active');
    if(isActive) btn.classList.remove('active');
  }
  // [2] 현재 클릭한 target에 active 클래스를 추가
  target.classList.add('active');
}

  • 버튼이 클릭될 때마다 active클래스를 지정해야하므로 onClickBtn에 해당 함수를 호출해주었다.
function onClickBtn(target) {
  // ...
  toggleActive(target);    // 추가
}

(4) 클릭한 버튼의 text 나타내는 함수

  • [1] target의 클래스가 text인 dom에 접근한다.
  • [2] 그 다음, result의 text를 변경해준다.
const result = document.querySelector('.result');

function changeResultText(target) {
  const text = target.querySelector('.text');  // [1]
  result.innerText = text.innerText;           // [2]
}

  • 버튼이 클릭될 때마다 동작해야하므로 onClickBtn에 함수를 호출해주었다.
function onClickBtn(target) {
  // ...
  toggleActive(target);
  changeResultText(target); // 추가
}

(5) 최종 코드

const container = document.querySelector('.container');
const scrollDom = document.querySelector('.list');
const result = document.querySelector('.result');
const btns = document.querySelectorAll('.btn');

// change btn position to center
function onClickBtn(target) {
  const btnWidth = target.clientWidth;
  const btnLeft = target.offsetLeft;
  const center = container.clientWidth - btnWidth;
  const left = btnLeft - center / 2;
  scrollDom.scrollTo({ left, behavior: "smooth" });
  toggleActive(target);
  changeResultText(target);
}

function changeResultText(target) {
  const text = target.querySelector('.text');
  result.innerText = text.innerText;
}

// add active class on clicked button
function toggleActive(target) {
  for (const btn of btns) {
    const isActive = btn.classList.contains('active');
    if(isActive) btn.classList.remove('active');
  }
  target.classList.add('active');
}

// change wheel to srollLeft
scrollDom.addEventListener('wheel', (e)=> {
  e.preventDefault();
  scrollDom.scrollLeft += e.deltaY;
})

💡 추가적으로 window에서 resize가 발생한 경우에 대해서도 코드를 추가할 수 있을 것이다 🙂

반응형

댓글