테크플레이 블로그를 PC에서 보면 독특한 마우스 커서 이펙트 효과가 적용된 것을 볼 수 있습니다. 이러한 커서를 “팔로잉 마우스 커서” 또는 “팔로잉 마우스 포인터” 라고 부르는데, 이런 효과를 적용하기 위해서는 적지 않은 코드들이 들어가게 됩니다. 자신만의 사이트에 여러분이 원하는 커서를 적용하고 싶다면, 예제에서 빠르게 적용해 볼 수 있도록 여러가지를 가져와 봤습니다. 😊(세상엔 똑똑한 분이 참 많으셔요.)
Ghost cursor – CodePenChallenge
귀여운 고스트 커서 입니다. 클릭 할 때마다 눈을 깜빡이네요. 할로윈 사이트에 어울릴 것 같아요. 👻
See the Pen Ghost cursor – CodePenChallenge by Fabio Ottaviani (@supah) on CodePen.
Custom Cursor – Circle Follows The Mouse Pointer
갤러리, 포트폴리오 이미지 링크 호버시 유용할 것 같습니다.
See the Pen Custom Cursor – Circle Follows The Mouse Pointer by Cojea Gabriel (@gabrielcojea) on CodePen.
Stars following mouse
별이 좀 정신없이 튀어나오지만, 조금 조정하여 쓰면 괜찮을 것 같습니다. 이벤트 페이지에 어울릴 것 같아요.
See the Pen Stars following mouse by David Hartley (@davidhartley) on CodePen.
Twinkle Star Following Cursor
귀여운 별들이 반짝거리네요. 어디에 어울릴까요?
See the Pen Twinkle Star Following Cursor by Ziv Rozov (@zivro) on CodePen.
Follow cursor mouse with TweenMax.js
강력한 트윈맥스 라이브러리네요, 갤러리등에 잘 어울릴 것 같습니다.
See the Pen Follow cursor mouse with TweenMax.js by pierrinho (@pierrinho) on CodePen.
Curzr | Free cursor library
이 사이트에 적용한 커서가 5번 글리치 커서 이며, 적용하는 방법에 대해 아래 챕터에서 다뤄보겠습니다.
See the Pen Curzr | Free cursor library by Taylon, Chan (@tin-fung-hk) on CodePen.
워드프레스에 팔로잉 마우스 커서 적용
아래 소심하게 캡쳐한 gif ani를 보면 마우스아이콘을 on/off 할 수 있게 적용해 보았습니다. 이유는 모든 사람이 커서효과를 좋아한다고 볼 수도 없으며, 그냥 개인 취향이 담긴 커서 이거니와, 특히 맥 사파리(Safari)에서는 자바스크립트 엔진이 신통치 않아 아주 느립니다. 그래서 맥 사파리에서는 off하는게 좋고, 애플 MAC에서라도 크롬이나 파이어폭스에서는 성능 이슈 없이 사용 할 수 있으니 이점 참고 바랍니다.
Curzr | Free cursor library 5번 커서가 마음에 들어 적용해보겠습니다. 근데 코드를 아는 분들에게는 어렵지 않지만 5번 커서만 적용하기 위해서는 약간의 코드 수정이 필요합니다.
JS 수정
불필요한 코드들은 모두 걷어내고, 마우스 온/오프 기능을 추가 했습니다. 페이지 이동할 때 상태 값을 기억할 수 있도록 로컬 스토리지에 상태 값을 저장할 수 있도록 했습니다.
// GlitchEffect 클래스 정의
class GlitchEffect {
constructor() {
// 기본 설정과 변수 초기화
this.root = document.body // 페이지의 body 태그를 root로 설정
this.cursor = document.querySelector(".glitch-effect") // CSS 클래스가 'glitch-effect'인 요소를 cursor로 설정
// 커서 이동과 관련된 변수 초기화
this.distanceX = 0,
this.distanceY = 0,
this.pointerX = 0,
this.pointerY = 0,
this.previousPointerX = 0
this.previousPointerY = 0
this.cursorSize = 15 // 커서의 기본 크기 설정
this.glitchColorB = '#00feff' // 글리치 이펙트의 첫 번째 색상
this.glitchColorR = '#ff4f71' // 글리치 이펙트의 두 번째 색상
// 커서의 스타일 설정
this.cursorStyle = {
boxSizing: 'border-box',
position: 'fixed',
top: `${ this.cursorSize / -1.0 }px`, // 커서 위치 조정
left: `${ this.cursorSize / -1.0 }px`, // 커서 위치 조정
zIndex: '2147483647', // z-index를 최대값으로 설정하여 항상 위에 보이도록 함
width: `${ this.cursorSize }px`, // 커서의 너비 설정
height: `${ this.cursorSize }px`, // 커서의 높이 설정
backgroundColor: '#222', // 커서의 기본 배경색
borderRadius: '50%', // 커서를 원형으로 만듦
boxShadow: `0 0 0 ${this.glitchColorB}, 0 0 0 ${this.glitchColorR}`, // 글리치 이펙트를 위한 그림자 색상
transition: '100ms, transform 100ms', // 부드러운 전환 효과
userSelect: 'none', // 텍스트 선택 방지
pointerEvents: 'none' // 포인터 이벤트 방지
}
// backdrop-filter 지원 여부에 따라 스타일을 조정
if (CSS.supports("backdrop-filter", "invert(1)")) {
this.cursorStyle.backdropFilter = 'invert(1)' // invert 필터 적용
this.cursorStyle.backgroundColor = '#fff0' // 배경색 투명도 조정
} else {
this.cursorStyle.backgroundColor = '#222' // 지원하지 않을 경우 기본 색상 사용
}
// 초기화 함수 호출
this.init(this.cursor, this.cursorStyle)
}
// init 함수: 커서 스타일 적용 및 활성화
init(el, style) {
Object.assign(el.style, style) // 전달받은 요소에 스타일 적용
setTimeout(() => {
this.cursor.removeAttribute("hidden") // 숨김 속성 제거
}, 500)
this.cursor.style.opacity = 1 // 커서를 불투명하게 설정
}
// move 함수: 마우스 이동에 따라 커서 위치 및 스타일 조정
move(event) {
// 이전 포인터 위치를 현재 위치로 업데이트
this.previousPointerX = this.pointerX
this.previousPointerY = this.pointerY
// 새로운 포인터 위치 계산
this.pointerX = event.pageX + this.root.getBoundingClientRect().x
this.pointerY = event.pageY + this.root.getBoundingClientRect().y
// 포인터 이동 거리를 계산하고 제한을 두어 글리치 효과 조절
this.distanceX = Math.min(Math.max(this.previousPointerX - this.pointerX, -10), 10)
this.distanceY = Math.min(Math.max(this.previousPointerY - this.pointerY, -10), 10)
// 특정 요소 위에 있을 때 hover 함수 호출
if (event.target.localName === 'svg' ||
event.target.localName === 'a' ||
event.target.onclick !== null ||
Array.from(event.target.classList).includes('curzr-hover')) {
this.hover()
} else {
this.hoverout()
}
// 커서의 위치와 그림자(글리치 효과)를 업데이트
this.cursor.style.transform = `translate3d(${this.pointerX}px, ${this.pointerY}px, 0)`
this.cursor.style.boxShadow = `
${+this.distanceX}px ${+this.distanceY}px 0 ${this.glitchColorB},
${-this.distanceX}px ${-this.distanceY}px 0 ${this.glitchColorR}`
this.stop() // 글리치 효과 중지
}
// hover 함수: 커서 크기 변경
hover() {
this.cursorSize = 30 // 크기 증가
}
// hoverout 함수: 커서 크기 복원
hoverout() {
this.cursorSize = 15 // 기본 크기로 복원
}
// click 함수: 클릭 시 커서 스타일 변화
click() {
this.cursor.style.transform += ` scale(0.75)` // 클릭 시 축소 효과
setTimeout(() => {
this.cursor.style.transform = this.cursor.style.transform.replace(` scale(0.75)`, '') // 원래 크기로 복원
}, 35)
}
// stop 함수: 글리치 이펙트 중지
stop() {
if (!this.moving) {
this.moving = true
setTimeout(() => {
this.cursor.style.boxShadow = '' // 그림자(글리치 효과) 제거
this.moving = false
}, 50)
}
}
// hidden 함수: 커서 숨기기
hidden() {
this.cursor.style.opacity = 0 // 불투명도를 0으로 설정하여 숨김
setTimeout(() => {
this.cursor.setAttribute("hidden", "hidden") // 숨김 속성 설정
}, 500)
}
}
// GlitchEffect 인스턴스 생성 및 이벤트 핸들러 함수 정의
let cursor = new GlitchEffect();
function handleMouseMove(event) {
cursor.move(event); // 마우스 이동 이벤트 처리
}
function handleTouchMove(event) {
cursor.move(event.touches[0]); // 터치 이동 이벤트 처리
}
function handleClick() {
if (typeof cursor.click === 'function') {
cursor.click(); // 클릭 이벤트 처리
}
}
// updateCursorEffect 함수: 마우스 이펙트 상태에 따라 커서 업데이트
function updateCursorEffect() {
const isChecked = localStorage.getItem('mouseEffect') === 'true';
document.getElementById('tb-bell').checked = isChecked;
if (isChecked) {
document.body.style.cursor = 'none'; // 기본 커서 숨김
cursor = new GlitchEffect();
document.onmousemove = handleMouseMove;
document.ontouchmove = handleTouchMove;
document.onclick = handleClick;
} else {
document.body.style.cursor = 'auto'; // 기본 커서 복원
cursor.hidden();
}
}
// tb-bell 요소의 상태 변경에 따라 updateCursorEffect 함수 호출
document.getElementById('tb-bell').addEventListener('change', function() {
localStorage.setItem('mouseEffect', this.checked);
updateCursorEffect();
});
// 페이지 로드 시 updateCursorEffect 함수를 호출하여 초기 상태 설정
window.onload = updateCursorEffect;
위 자바스크립트를 차일드 테마에 JS폴더를 생성하고, 원하는 파일명으로 저장합니다. 전 assets/js폴더를 만들어서 mouse.js 로 저장했습니다.
HTML 마우스 효과 및 온/오프 버튼 적용
“아이콘 라이브러리”는 헤더에 링크를 추가하고, “글리치 마우스 커서”는 원하는 곳에 삽입합니다. “온/오프 토글 버튼”도 마찬가지로 원하는 곳에 넣어주면 됩니다.
<!-- 아이콘 라이브러리 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.3.67/css/materialdesignicons.min.css" />
<!-- 마우스 효과 -->
<div class="glitch-effect"></div>
<!-- 마우스 버튼 on/off -->
<label class="toggle-button" for="tb-bell" style="--toggle-button-checked-color:#2979FF">
<input class="toggle-button__input curzr-hover" id="tb-bell" aria-labelledby="tb-bell-tip" type="checkbox">
<i class="toggle-button__checked mdi mdi-mouse curzr-hover" role="presentation"></i>
<i class="toggle-button__unchecked mdi mdi-mouse-off curzr-hover" role="presentation"></i>
<span class="toggle-button__background curzr-hover" role="presenatation"></span>
<span class="toggle-button__tip" id="tb-heart-tip">mouse effect</span>
</label>
CSS 스타일 입히기
차일드 테마에 /assets/css/mouse.css 파일을 만들어서 아래 스타일을 적용합니다.
/*toggle*/
.toggle-button {
--toggle-button-checked-color: #e2e2e3;
--toggle-button-unchecked-color: rgba(0, 0, 0, 0.56);
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 40px;
height: 40px;
color: var(--toggle-button-unchecked-color);
font-family: inherit;
font-size: inherit;
}
.toggle-button::before {
position: absolute;
box-sizing: inherit;
width: 100%;
height: 100%;
content: "";
background-color: currentColor;
border-radius: 50%;
opacity: 0;
transform-origin: 50% 50%;
transform: scale(0.25);
transition: opacity 120ms ease-out, transform 120ms ease-out;
}
.toggle-button:focus::before, .toggle-button:focus-within::before {
opacity: 0.08;
transform: scale(0.8);
}
.toggle-button:hover::before {
opacity: 0.04;
transform: scale(0.9);
}
.toggle-button:active::before {
opacity: 0.12;
transform: scale(1);
}
.toggle-button__input {
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
box-sizing: inherit;
opacity: 0;
cursor: pointer;
}
.toggle-button__checked, .toggle-button__unchecked {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
box-sizing: inherit;
width: 24px;
height: 24px;
font-size: 24px;
pointer-events: none;
transform-origin: 50% 50%;
transition: opacity 120ms ease-out, transform 120ms ease-out;
}
.toggle-button__checked {
color: var(--toggle-button-checked-color);
}
.toggle-button__input:not(:checked) ~ .toggle-button__checked {
opacity: 0;
transform: scale(0.375);
}
.toggle-button__input:checked ~ .toggle-button__checked {
opacity: 1;
transform: scale(1);
transition-delay: 180ms;
transition-duration: 180ms;
}
.toggle-button__unchecked {
color: currentColor;
}
.toggle-button__input:not(:checked) ~ .toggle-button__unchecked {
opacity: 1;
transition-delay: 120ms;
}
.toggle-button__input:checked ~ .toggle-button__unchecked {
opacity: 0;
}
.toggle-button__background {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
box-sizing: inherit;
width: 100%;
height: 100%;
pointer-events: none;
}
.toggle-button__background::before, .toggle-button__background::after {
position: absolute;
box-sizing: inherit;
content: "";
transform-origin: 50% 50%;
}
.toggle-button__background::before {
width: 100%;
height: 100%;
background-color: var(--toggle-button-checked-color);
border-radius: 50%;
opacity: 0;
transform: scale(0.6);
}
.toggle-button__background::after {
top: 50%;
left: 50%;
width: 0.25rem;
height: 0.25rem;
margin-top: -0.125rem;
margin-left: -0.125rem;
border-radius: 50%;
opacity: 0;
transform: scale(0.5);
box-shadow: 0px -24px 0px 0rem #f1cf01, 18.7639555792px -14.9637552446px 0px 0rem #46f101, 23.3982698924px 5.340502415px 0px 0rem #01f18a, 10.4132097388px 21.6232528297px 0px 0rem #018af1, -10.4132097388px 21.6232528297px 0px 0rem #4601f1, -23.398269892px 5.340502415px 0px 0rem #f101cf, -18.7639554503px -14.9637552232px 0px 0rem #f10101;
}
.toggle-button__input:checked ~ .toggle-button__background::before {
-webkit-animation: toggle-button-bg-animation-before 390ms ease-out forwards;
animation: toggle-button-bg-animation-before 390ms ease-out forwards;
}
.toggle-button__input:checked ~ .toggle-button__background::after {
-webkit-animation: toggle-button-bg-animation-after 360ms ease-out 210ms forwards;
animation: toggle-button-bg-animation-after 360ms ease-out 210ms forwards;
}
.toggle-button__tip {
position: absolute;
bottom: 100%;
box-sizing: inherit;
max-width: 144px;
padding: 0px 6px;
color: white;
font-size: 0.625rem;
letter-spacing: 0.0275em;
line-height: 20px;
text-shadow: 0px 0px 1px rgba(0, 0, 0, 0.08);
background-color: rgba(0, 0, 0, 0.56);
border-radius: 3px;
overflow: hidden;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transform-origin: center bottom;
transform: translateY(-4px);
transition: opacity 60ms ease-out 0ms, transform 0ms ease-out 60ms;
}
.toggle-button__input:focus-visible ~ .toggle-button__tip, .toggle-button__input:hover ~ .toggle-button__tip {
opacity: 1;
transform: translateY(-8px);
transition-delay: 90ms;
transition-duration: 120ms;
}
@-webkit-keyframes toggle-button-bg-animation-before {
0% {
opacity: 0;
transform: scale(0.3);
}
2.5% {
opacity: 0.87;
}
60% {
opacity: 0.2;
transform: scale(0.75);
-webkit-animation-timing-function: linear;
animation-timing-function: linear;
}
100% {
opacity: 0;
transform: scale(1);
}
}
@keyframes toggle-button-bg-animation-before {
0% {
opacity: 0;
transform: scale(0.3);
}
2.5% {
opacity: 0.87;
}
60% {
opacity: 0.2;
transform: scale(0.75);
-webkit-animation-timing-function: linear;
animation-timing-function: linear;
}
100% {
opacity: 0;
transform: scale(1);
}
}
@-webkit-keyframes toggle-button-bg-animation-after {
0% {
opacity: 0;
transform: scale(0.5);
}
5% {
opacity: 1;
transform: scale(0.5);
}
60% {
transform: scale(0.875);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
100% {
opacity: 0;
transform: scale(1);
}
}
@keyframes toggle-button-bg-animation-after {
0% {
opacity: 0;
transform: scale(0.5);
}
5% {
opacity: 1;
transform: scale(0.5);
}
60% {
transform: scale(0.875);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
100% {
opacity: 0;
transform: scale(1);
}
}
.toggle-button {
margin: 8px;
}
워드프레스 함수 파일 수정
아래 코드에 주석을 자세히 달아 두었습니다. 차일드 테마 함수 파일(child-theme/functions.php)에 위에서 만든 mouse.css와 mouse.js파일을 불러 옵니다.
// mouse effect 스타일시트 추가를 위한 액션 후크
add_action('wp_enqueue_scripts', 'custom_style_sheet');
function custom_style_sheet() {
wp_enqueue_style( 'custom-styling', get_stylesheet_directory_uri() . '/assets/css/mouse.css' );
// 여기서 wp_enqueue_style 함수는 WordPress 사이트에 사용자 정의 스타일시트를 추가합니다.
// 'custom-styling'은 스타일시트의 핸들 이름입니다.
// get_stylesheet_directory_uri() . '/assets/css/mouse.css'는 스타일시트의 경로를 지정합니다.
}
// mouse effect 스크립트 추가를 위한 액션 후크
add_action( 'wp_enqueue_scripts', 'Scripts_mouse_effect' );
function Scripts_mouse_effect() {
wp_enqueue_script('mouse-e-script', get_stylesheet_directory_uri() . '/assets/js/mouse.js', array(), '1.0.0', true );
// 여기서 wp_enqueue_script 함수는 WordPress 사이트에 사용자 정의 자바스크립트 파일을 추가합니다.
// 'mouse-e-script'는 스크립트의 핸들 이름입니다.
// get_stylesheet_directory_uri() . '/assets/js/mouse.js'는 스크립트 파일의 경로를 지정합니다.
// array()는 스크립트의 종속성을 나타내며, 여기서는 종속성이 없음을 의미합니다.
// '1.0.0'은 스크립트 버전을 지정합니다.
// true는 스크립트를 페이지의 footer에 로드하도록 지정합니다.
}
// 스크립트 태그에 defer 속성 추가를 위한 필터 후크
add_filter('script_loader_tag', 'add_defer_attribute', 10, 2);
function add_defer_attribute($tag, $handle) {
if ('mouse-e-script' !== $handle)
return $tag;
// 'mouse-e-script' 핸들을 가진 스크립트에만 defer 속성을 추가합니다.
return str_replace(' src', ' defer="defer" src', $tag);
// defer 속성을 사용하면 브라우저가 HTML 파싱을 방해하지 않고 스크립트를 비동기적으로 로드합니다.
}
마무리
한 가지 예제를 따라 해봤습니다. 다른 여러 사람들이 만든 커서 효과 역시도 이와 같은 방식으로 적용이 가능합니다. 자신의 사이트에 적용하려면 플랫폼에 따라, 코드 수정방식이나 요구 사항이 다를 수는 있습니다.
위 예제가 잘 적용되지 않나요? js, css, 파일이 잘 불러오는지 확인해 보세요. 브라우저 개발자 콘솔을 통해 에러 로그를 확인해 보세요. 도움이 필요하면 언제든지 연락주세요.