관리 메뉴

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

[해피CGI][cgimall] Transition API를 활용한 대시보드 화면 전환 본문

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

[해피CGI][cgimall] Transition API를 활용한 대시보드 화면 전환

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

대시보드의 그리드 구조가 변경될 때 각 콘텐츠가 자연스럽게 이동하도록 구현한 예제입니다.
Transition API를 적용하여 요소가 갑자기 사라지거나 나타나지 않고 이전 위치에서 새로운 위치로 부드럽게 전환됩니다.
카드 정렬, 레이아웃 변경, 메뉴 전환 등 다양한 UI 변화에 활용할 수 있습니다.
관리자 페이지, 통계 화면, 업무용 대시보드와 같은 인터페이스에 적용하기 좋습니다.

HTML 구조

<!doctype html>

<html lang="en">

  <head>

    <meta charset="UTF-8" />

    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <title>FLUX — Morphing Dashboard</title>

    <link rel="stylesheet" href="style.css" />

    <link

      href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&display=swap"

      rel="stylesheet"

    />

  </head>

  <body data-theme="light">

    <div class="container">

      <header class="top-bar">

        <div class="brand">

          <div class="logo-icon"></div>

          <h1>FLUX</h1>

        </div>

 

        <div class="controls">

          <div class="layout-switcher">

            <button class="layout-btn active" data-layout="bento">Bento</button>

            <button class="layout-btn" data-layout="grid">Grid</button>

            <button class="layout-btn" data-layout="list">List</button>

          </div>

 

          <button class="theme-toggle" id="themeToggle" title="Toggle Theme">

            <span class="icon sun">☀</span>

            <span class="icon moon">☾</span>

          </button>

        </div>

      </header>

 

      <main class="dashboard layout-bento" id="dashboard">

        <div class="card card-1">

          <div class="card-content">

            <h2>Revenue</h2>

            <p>$48.2k</p>

            <span class="trend">+12%</span>

          </div>

        </div>

        <div class="card card-2">

          <div class="card-content">

            <h2>Users</h2>

            <p>8,492</p>

            <span class="trend">+5%</span>

          </div>

        </div>

        <div class="card card-3">

          <div class="card-content chart-vis"></div>

        </div>

        <div class="card card-4">

          <div class="card-content">

            <h2>Sessions</h2>

            <p>1,204</p>

            <span class="trend">-2%</span>

          </div>

        </div>

        <div class="card card-5">

          <div class="card-content">

            <h2>Bounce Rate</h2>

            <p>32%</p>

            <span class="trend">-1%</span>

          </div>

        </div>

        <div class="card card-6">

          <div class="card-content">

            <h2>Performance</h2>

            <p>98/100</p>

            <span class="trend">+10%</span>

          </div>

        </div>

      </main>

    </div>

 

    <script>

      const dashboard = document.getElementById("dashboard");

      const themeToggle = document.getElementById("themeToggle");

 

      function switchLayout(layoutClass) {

        dashboard.className = `dashboard layout-${layoutClass}`;

        document.querySelectorAll(".layout-btn").forEach((btn) => {

          btn.classList.toggle("active", btn.dataset.layout === layoutClass);

        });

      }

 

      // Layout Morphing Transition

      document.querySelectorAll(".layout-btn").forEach((btn) => {

        btn.addEventListener("click", () => {

          const layoutClass = btn.dataset.layout;

 

          // Fallback

          if (!document.startViewTransition) {

            switchLayout(layoutClass);

            return;

          }

 

          // Trigger transition

          document.startViewTransition(() => {

            switchLayout(layoutClass);

          });

        });

      });

 

      // Theme Circular Reveal Transition

      themeToggle.addEventListener("click", (e) => {

        const isDark = document.body.getAttribute("data-theme") === "dark";

        const newTheme = isDark ? "light" : "dark";

 

        // Calculate click coordinates for the circular reveal origin

        const rect = themeToggle.getBoundingClientRect();

        const x = rect.left + rect.width / 2;

        const y = rect.top + rect.height / 2;

 

        // Fallback

        if (!document.startViewTransition) {

          document.body.setAttribute("data-theme", newTheme);

          return;

        }

 

        const transition = document.startViewTransition(() => {

          document.body.setAttribute("data-theme", newTheme);

        });

 

        // Animate the new view with a circular clip-path

        transition.ready.then(() => {

          document.documentElement.animate(

            {

              clipPath: [

                `circle(0px at ${x}px ${y}px)`,

                `circle(150% at ${x}px ${y}px)`

              ]

            },

            {

              duration: 600,

              easing: "ease-in-out",

              pseudoElement: "::view-transition-new(root)"

            }

          );

        });

      });

    </script>

  </body>

</html>

 



CSS 소스

:root {

  --bg: #f4f4f9;

  --fg: #1a1a2e;

  --card-bg: #ffffff;

  --accent: #6c5ce7;

  --accent-2: #00cec9;

  --border: rgba(0, 0, 0, 0.05);

  --shadow: 0 10px 30px rgba(0, 0, 0, 0.05);

}

 

[data-theme="dark"] {

  --bg: #0f0f1a;

  --fg: #f4f4f9;

  --card-bg: #1e1e2e;

  --accent: #a29bfe;

  --accent-2: #00cec9;

  --border: rgba(255, 255, 255, 0.1);

  --shadow: 0 10px 30px rgba(0, 0, 0, 0.3);

}

 

* {

  margin: 0;

  padding: 0;

  box-sizing: border-box;

}

 

body {

  font-family: "Outfit", sans-serif;

  background: var(--bg);

  color: var(--fg);

  height: 100vh;

  overflow: hidden;

  transition: background 0.3s;

}

 

.container {

  max-width: 1200px;

  margin: 0 auto;

  padding: 40px;

  height: 100vh;

  display: flex;

  flex-direction: column;

}

 

.top-bar {

  display: flex;

  justify-content: space-between;

  align-items: center;

  margin-bottom: 40px;

}

 

.brand {

  display: flex;

  align-items: center;

  gap: 12px;

}

 

.logo-icon {

  width: 32px;

  height: 32px;

  background: var(--accent);

  border-radius: 8px;

  transform: rotate(45deg);

  box-shadow: 0 4px 15px var(--accent);

}

 

.brand h1 {

  font-size: 24px;

  font-weight: 800;

  letter-spacing: 0.05em;

}

 

.controls {

  display: flex;

  align-items: center;

  gap: 20px;

}

 

.layout-switcher {

  display: flex;

  background: var(--card-bg);

  border-radius: 100px;

  padding: 4px;

  box-shadow: var(--shadow);

  border: 1px solid var(--border);

}

 

.layout-btn {

  background: transparent;

  border: none;

  color: var(--fg);

  padding: 8px 16px;

  border-radius: 100px;

  cursor: pointer;

  font-family: inherit;

  font-weight: 600;

  font-size: 12px;

  opacity: 0.6;

  transition: all 0.2s;

}

 

.layout-btn.active {

  background: var(--accent);

  color: white;

  opacity: 1;

}

 

.theme-toggle {

  width: 44px;

  height: 44px;

  border-radius: 50%;

  border: 1px solid var(--border);

  background: var(--card-bg);

  cursor: pointer;

  position: relative;

  box-shadow: var(--shadow);

  overflow: hidden;

}

 

.theme-toggle .icon {

  position: absolute;

  top: 50%;

  left: 50%;

  transform: translate(-50%, -50%);

  font-size: 18px;

  transition:

    transform 0.3s,

    opacity 0.3s;

}

 

[data-theme="dark"] .icon.sun {

  opacity: 0;

  transform: translate(-50%, 100%);

}

[data-theme="dark"] .icon.moon {

  opacity: 1;

  transform: translate(-50%, -50%);

}

 

.icon.sun {

  opacity: 1;

  color: #f39c12;

}

.icon.moon {

  opacity: 0;

  transform: translate(-50%, -100%);

  color: var(--accent);

}

 

/* Dashboard Layouts */

.dashboard {

  flex: 1;

  display: grid;

  gap: 20px;

}

 

.card {

  background: var(--card-bg);

  border-radius: 20px;

  padding: 24px;

  box-shadow: var(--shadow);

  border: 1px solid var(--border);

  display: flex;

  flex-direction: column;

  justify-content: flex-end;

  position: relative;

  overflow: hidden;

  /* Unique names for morphing transitions */

  view-transition-name: var(--vt-name);

}

.card-1 {

  --vt-name: card-1;

}

.card-2 {

  --vt-name: card-2;

}

.card-3 {

  --vt-name: card-3;

}

.card-4 {

  --vt-name: card-4;

}

.card-5 {

  --vt-name: card-5;

}

.card-6 {

  --vt-name: card-6;

}

 

.card h2 {

  font-size: 14px;

  font-weight: 600;

  opacity: 0.6;

  margin-bottom: 5px;

}

.card p {

  font-size: 32px;

  font-weight: 800;

  letter-spacing: -0.02em;

}

 

.trend {

  font-size: 12px;

  font-weight: 600;

  color: var(--accent-2);

  position: absolute;

  top: 24px;

  right: 24px;

  background: rgba(0, 206, 201, 0.1);

  padding: 4px 8px;

  border-radius: 20px;

}

 

/* Bento Layout */

.dashboard.layout-bento {

  grid-template-columns: repeat(4, 1fr);

  grid-template-rows: repeat(3, 1fr);

}

.layout-bento .card-1 {

  grid-column: span 2;

  grid-row: span 2;

}

.layout-bento .card-2 {

  grid-column: span 2;

}

.layout-bento .card-3 {

  grid-column: span 2;

}

.layout-bento .card-4 {

  grid-column: span 1;

}

.layout-bento .card-5 {

  grid-column: span 1;

}

.layout-bento .card-6 {

  grid-column: span 2;

  background: var(--accent);

  color: white;

}

.layout-bento .card-6 .trend {

  background: rgba(255, 255, 255, 0.2);

  color: white;

}

 

/* Grid Layout */

.dashboard.layout-grid {

  grid-template-columns: repeat(3, 1fr);

  grid-template-rows: repeat(2, 1fr);

}

.layout-grid .card {

  grid-column: span 1;

  grid-row: span 1;

}

.layout-grid .card-6 {

  background: var(--card-bg);

  color: var(--fg);

}

.layout-grid .card-6 .trend {

  background: rgba(0, 206, 201, 0.1);

  color: var(--accent-2);

}

 

/* List Layout */

.dashboard.layout-list {

  grid-template-columns: 1fr;

  grid-template-rows: repeat(6, 1fr);

}

.layout-list .card {

  flex-direction: row;

  align-items: center;

  justify-content: space-between;

}

.layout-list .card-content {

  display: flex;

  width: 100%;

  align-items: center;

  justify-content: space-between;

}

.layout-list .card h2 {

  margin-bottom: 0;

}

 

/* Chart visualization */

.chart-vis {

  width: 100%;

  height: 100%;

  background-image:

    linear-gradient(var(--accent) 2px, transparent 2px),

    linear-gradient(90deg, var(--accent) 2px, transparent 2px);

  background-size:

    20px 100%,

    100% 20px;

  background-position:

    0 100%,

    0 0;

  background-repeat: no-repeat;

  opacity: 0.8;

  position: relative;

}

.chart-vis::after {

  content: "";

  position: absolute;

  bottom: 0;

  left: 0;

  right: 0;

  height: 70%;

  background: linear-gradient(transparent, var(--accent-2));

  clip-path: polygon(

    0 100%,

    20% 40%,

    40% 80%,

    60% 20%,

    80% 60%,

    100% 30%,

    100% 100%

  );

}

 

/* === VIEW TRANSITIONS API STYLING === */

::view-transition-old(root),

::view-transition-new(root) {

  animation: none;

  mix-blend-mode: normal;

}

 

::view-transition-old(root) {

  z-index: 1;

}

::view-transition-new(root) {

  z-index: 9999;

}

 

@media (max-width: 768px) {

  .container {

    padding: 20px;

  }

  .top-bar {

    flex-direction: column;

    gap: 20px;

    align-items: flex-start;

  }

  .dashboard.layout-bento,

  .dashboard.layout-grid {

    grid-template-columns: 1fr;

    grid-template-rows: auto;

  }

  .layout-bento .card-1,

  .layout-bento .card-2,

  .layout-bento .card-3,

  .layout-bento .card-4,

  .layout-bento .card-5,

  .layout-bento .card-6,

  .layout-grid .card {

    grid-column: span 1;

    grid-row: span 1;

    min-height: 120px;

  }

}

 

 

Comments