관리 메뉴

웹솔루션개발 26년 노하우! 해피CGI의 모든것

[해피CGI][cgimall] GIF 없이 구현하는 동적 애니메이션 버튼 본문

웹프로그램밍 자료실/기타 자료

[해피CGI][cgimall] GIF 없이 구현하는 동적 애니메이션 버튼

해피CGI윤실장 2026. 9. 11. 09:10

이미지 애니메이션 파일을 사용하지 않고 HTML과 CSS 효과를 활용해 동적인 버튼을 구현했습니다.
마우스를 올리거나 클릭하면 버튼 내부의 그래픽 요소와 배경이 자연스럽게 움직입니다.
문구, 색상, 크기, 애니메이션 속도를 코드에서 간편하게 수정할 수 있습니다.
이벤트 페이지, 신청 화면, 상품 상세 페이지 등 강조가 필요한 버튼에 활용하기 좋습니다.

HTML 구조

<div id="root"></div>



CSS 소스

* {

border: 0;

box-sizing: border-box;

margin: 0;

padding: 0;

}

 

:root {

--hue: 210;

--gray50: hsl(var(--hue) 10% 95%);

--gray950: hsl(var(--hue) 10% 5%);

--primary500: hsl(var(--hue) 90% 45%);

--trans-dur: 0.3s;

color-scheme: light dark;

font-size: clamp(1.5rem, 1.425rem + 0.375vw, 1.875rem);

}

 

body,

button {

font: 1em / 1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

}

 

body {

background-color: light-dark(var(--gray50), var(--gray950));

color: light-dark(var(--gray950), var(--gray50));

display: grid;

place-items: center;

height: 100vh;

transition:

background-color var(--trans-dur),

color var(--trans-dur);

}

 

button {

-webkit-tap-highlight-color: transparent;

}

 

.plane-btn {

background-color: var(--primary500);

border-radius: 0.5em;

color: var(--gray50);

cursor: pointer;

display: block;

overflow: hidden;

padding: 0.5em 1.5em;

position: relative;

transition:

background-color var(--trans-dur),

color var(--trans-dur),

transform var(--trans-dur);

 

&__cloud {

background-color: hsl(0 0% 100%);

border-radius: 50%;

display: block;

filter: blur(16px);

opacity: 0.7;

height: 2.25em;

 

&--l {

transform: translateX(-33%);

}

 

&--r {

margin-left: auto;

transform: translateX(33%);

}

 

&--sm {

width: 33%;

}

 

&--md {

width: 50%;

}

 

&--lg {

width: 67%;

}

}

 

&__land {

background-color: hsl(120 30% 50%);

border-radius: 33% 67% 70% 30% / 33% 67% 33% 67%;

box-shadow: 0 0 0 0.5em hsla(0 0% 100% / 0.1);

width: 3.5em;

height: 2.25em;

transform: translateX(-25%);

}

 

&__plane,

&__scroller {

position: absolute;

}

 

&__plane,

&__text {

transition: transform 0.5s ease-in-out;

}

 

&__plane {

display: grid;

place-items: center;

font-size: 1.25em;

inset: 0;

transform: translateY(100%);

 

&::before {

animation: 1.5s plane-move ease-in-out infinite;

content: "✈️";

display: block;

}

}

 

&__scroller {

display: flex;

flex-direction: column;

inset: 0 0 auto;

padding: 3em 0.75em;

transform: translateY(-100%);

}

 

&__text {

display: block;

}

 

&:focus-visible,

&:hover {

transform: scale(1.05);

}

 

&:active {

transform: scale(0.95);

}

 

&:focus-visible &__plane,

&:hover &__plane {

transform: translateY(0);

}

 

&:focus-visible &__scroller,

&:hover &__scroller {

animation: 3s scroller-move linear infinite;

}

 

&:focus-visible &__text,

&:hover &__text {

transform: translateY(-2.5em);

}

}

 

// Animations

@keyframes plane-move {

0%,

100% {

transform: translateX(-0.125rem) rotate(-45deg);

}

 

50% {

transform: translateX(0.125rem) rotate(-45deg);

}

}

 

@keyframes scroller-move {

0% {

transform: translateY(-100%);

}

 

100% {

transform: translateY(0);

}

}



JS 소스

import React, { StrictMode } from "https://esm.sh/react";

import { createRoot } from "https://esm.sh/react-dom/client";

import clsx from "https://esm.sh/clsx";

 

createRoot(document.getElementById("root")!).render(

<StrictMode>

<AirplaneTicketButton />

</StrictMode>

);

 

interface AirplaneTicketButtonProps {

onclick?: () => void;

}

function AirplaneTicketButton({

onclick

}: Readonly<AirplaneTicketButtonProps>) {

return (

<button

className="plane-btn"

type="button"

onClick={onclick}>

<span className="plane-btn__scroller">

<AirplaneTicketButtonLand />

</span>

<span className="plane-btn__scroller">

<AirplaneTicketButtonCloud position="r" size="lg" />

<AirplaneTicketButtonCloud />

<AirplaneTicketButtonCloud size="sm" />

<AirplaneTicketButtonCloud position="r" size="lg" />

</span>

<span className="plane-btn__plane" aria-hidden="true"></span>

<span className="plane-btn__text">Get Tickets</span>

</button>

);

}

 

interface AirplaneTicketButtonCloudProps {

position?: "l" | "r";

size?: "sm" | "md" | "lg";

}

function AirplaneTicketButtonCloud({

position = "l",

size = "md"

}: Readonly<AirplaneTicketButtonCloudProps>) {

const _position = `plane-btn__cloud--${position}`;

const _size = `plane-btn__cloud--${size}`;

 

return (

<span className={clsx("plane-btn__cloud", _position, _size)}></span>

);

}

 

interface AirplaneTicketButtonLandProps {

position?: "l" | "r";

}

function AirplaneTicketButtonLand({

position = "l"

}: Readonly<AirplaneTicketButtonLandProps>) {

const _position = `plane-btn__land--${position}`;

 

return (

<span className={clsx("plane-btn__land", _position)}></span>

);

}


 

 

Comments