DEV Community

Cover image for How to Build a 3D Tilt Profile Card with HTML, CSS & JavaScript
CodeRipple
CodeRipple

Posted on

How to Build a 3D Tilt Profile Card with HTML, CSS & JavaScript

How to Build a 3D Tilt Profile Card with HTML, CSS & JavaScript

Modern profile cards don't have to feel flat.

In this tutorial, we'll build an interactive 3D Tilt Profile Card that responds to mouse movement with smooth rotation, layered parallax, cursor-follow lighting, and a dynamic glare effect.

The component is built with pure HTML, CSS, and JavaScript, without any external animation library.

What We're Building

The card includes:

  • Smooth cursor-based 3D tilt
  • Layered parallax movement
  • Cursor-follow glow
  • Dynamic glare effect
  • Animated availability indicator
  • Rotating avatar ring
  • Profile skills and statistics
  • Responsive layout
  • Touch-device fallback
  • Reduced-motion support

The final component works well for developer portfolios, personal websites, team pages, creator profiles, and modern landing pages.


HTML Structure

Let's start with the markup.

The data-depth attributes will later allow JavaScript to move different sections of the card at different speeds, creating the layered 3D effect.

<div class="cr-tilt-profile" id="crTiltProfile">

  <div class="cr-tilt-profile__stage">

    <article class="cr-tilt-profile__card">

      <!-- CURSOR FOLLOW GLOW -->
      <div class="cr-tilt-profile__glow" aria-hidden="true"></div>

      <!-- GLARE EFFECT -->
      <div class="cr-tilt-profile__glare" aria-hidden="true"></div>

      <!-- DECORATIVE BACKGROUND -->
      <div class="cr-tilt-profile__decor" aria-hidden="true">

        <span
          class="cr-tilt-profile__orb cr-tilt-profile__orb--one"
        ></span>

        <span
          class="cr-tilt-profile__orb cr-tilt-profile__orb--two"
        ></span>

        <span class="cr-tilt-profile__grid"></span>

      </div>

      <!-- PROFILE CONTENT -->
      <div class="cr-tilt-profile__content">

        <!-- TOP STATUS -->
        <div
          class="cr-tilt-profile__top"
          data-depth="18"
        >

          <span class="cr-tilt-profile__status">

            <span
              class="cr-tilt-profile__status-dot"
            ></span>

            Available for work

          </span>

          <span class="cr-tilt-profile__badge">
            PRO
          </span>

        </div>

        <!-- AVATAR -->
        <div
          class="cr-tilt-profile__avatar-wrap"
          data-depth="34"
        >

          <div class="cr-tilt-profile__avatar">
            <span>AR</span>
          </div>

          <span
            class="cr-tilt-profile__avatar-ring"
          ></span>

        </div>

        <!-- PROFILE INFO -->
        <div
          class="cr-tilt-profile__info"
          data-depth="26"
        >

          <span class="cr-tilt-profile__eyebrow">
            FRONTEND DEVELOPER
          </span>

          <h2>Alex Rivera</h2>

          <p>
            Building clean interfaces, smooth interactions
            and memorable digital experiences.
          </p>

        </div>

        <!-- SKILLS -->
        <div
          class="cr-tilt-profile__skills"
          data-depth="22"
        >

          <span>HTML</span>
          <span>CSS</span>
          <span>JavaScript</span>

        </div>

        <!-- STATS -->
        <div
          class="cr-tilt-profile__stats"
          data-depth="20"
        >

          <div class="cr-tilt-profile__stat">
            <strong>42</strong>
            <span>Projects</span>
          </div>

          <div class="cr-tilt-profile__divider"></div>

          <div class="cr-tilt-profile__stat">
            <strong>18K</strong>
            <span>Followers</span>
          </div>

          <div class="cr-tilt-profile__divider"></div>

          <div class="cr-tilt-profile__stat">
            <strong>4.9</strong>
            <span>Rating</span>
          </div>

        </div>

        <!-- ACTIONS -->
        <div
          class="cr-tilt-profile__actions"
          data-depth="30"
        >

          <button
            class="cr-tilt-profile__button cr-tilt-profile__button--primary"
            type="button"
          >

            <span>View Profile</span>

            <svg
              viewBox="0 0 24 24"
              aria-hidden="true"
            >
              <path
                d="M5 12h14M13 6l6 6-6 6"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
                stroke-linecap="round"
                stroke-linejoin="round"
              />
            </svg>

          </button>

          <button
            class="cr-tilt-profile__button cr-tilt-profile__button--icon"
            type="button"
            aria-label="Send message"
          >

            <svg
              viewBox="0 0 24 24"
              aria-hidden="true"
            >
              <path
                d="M21 3L9.5 14.5M21 3l-7 18-4.5-6.5L3 10l18-7z"
                fill="none"
                stroke="currentColor"
                stroke-width="1.8"
                stroke-linecap="round"
                stroke-linejoin="round"
              />
            </svg>

          </button>

        </div>

      </div>

      <!-- BOTTOM ACCENT -->
      <div
        class="cr-tilt-profile__accent"
        aria-hidden="true"
      ></div>

    </article>

    <!-- INTERACTION HINT -->
    <div class="cr-tilt-profile__hint">

      <span class="cr-tilt-profile__hint-icon">
        ↗
      </span>

      Move your cursor over the card

    </div>

  </div>

</div>
Enter fullscreen mode Exit fullscreen mode

CSS Styling

Now let's create the dark glass-style interface.

The perspective and transform-style: preserve-3d properties are especially important because they allow the card and its internal layers to appear at different depths.

.cr-tilt-profile {
  --cr-bg: #070914;
  --cr-card: rgba(13, 17, 35, 0.86);
  --cr-card-border: rgba(255, 255, 255, 0.1);
  --cr-cyan: #22d3ee;
  --cr-purple: #a855f7;
  --cr-text: #f8fafc;
  --cr-muted: #94a3b8;
  --cr-soft: #cbd5e1;

  width: 100%;
  min-height: 650px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 48px 20px;

  background:
    radial-gradient(
      circle at 25% 20%,
      rgba(34, 211, 238, 0.08),
      transparent 32%
    ),
    radial-gradient(
      circle at 78% 78%,
      rgba(168, 85, 247, 0.1),
      transparent 35%
    ),
    var(--cr-bg);

  font-family:
    Inter,
    system-ui,
    -apple-system,
    BlinkMacSystemFont,
    "Segoe UI",
    sans-serif;

  box-sizing: border-box;
  overflow: hidden;
}

.cr-tilt-profile *,
.cr-tilt-profile *::before,
.cr-tilt-profile *::after {
  box-sizing: border-box;
}

.cr-tilt-profile__stage {
  width: min(100%, 420px);
  display: flex;
  flex-direction: column;
  align-items: center;
  perspective: 1100px;
}

.cr-tilt-profile__card {
  position: relative;
  width: 100%;
  min-height: 535px;
  padding: 28px;
  border: 1px solid var(--cr-card-border);
  border-radius: 28px;

  background:
    linear-gradient(
      145deg,
      rgba(255, 255, 255, 0.075),
      rgba(255, 255, 255, 0.018)
    ),
    var(--cr-card);

  box-shadow:
    0 30px 80px rgba(0, 0, 0, 0.5),
    0 0 0 1px rgba(255, 255, 255, 0.02) inset,
    0 0 55px rgba(34, 211, 238, 0.035);

  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);

  transform-style: preserve-3d;
  transform: rotateX(0deg) rotateY(0deg);

  transition:
    transform 180ms ease,
    box-shadow 250ms ease,
    border-color 250ms ease;

  overflow: hidden;
  will-change: transform;
}

.cr-tilt-profile__card:hover {
  border-color: rgba(34, 211, 238, 0.22);

  box-shadow:
    0 38px 95px rgba(0, 0, 0, 0.58),
    0 0 65px rgba(34, 211, 238, 0.07),
    0 0 80px rgba(168, 85, 247, 0.045);
}

.cr-tilt-profile__glow {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 320px;
  height: 320px;
  border-radius: 50%;

  background:
    radial-gradient(
      circle,
      rgba(34, 211, 238, 0.16) 0%,
      rgba(168, 85, 247, 0.08) 34%,
      transparent 70%
    );

  transform: translate(-50%, -50%);
  filter: blur(18px);
  opacity: 0.5;
  pointer-events: none;
  z-index: 0;
}

.cr-tilt-profile__glare {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  z-index: 6;
  transition: opacity 250ms ease;
}

.cr-tilt-profile__decor {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 0;
}

.cr-tilt-profile__orb {
  position: absolute;
  width: 190px;
  height: 190px;
  border-radius: 50%;
  filter: blur(12px);
  opacity: 0.18;
}

.cr-tilt-profile__orb--one {
  top: -90px;
  right: -65px;

  background:
    radial-gradient(
      circle,
      var(--cr-cyan),
      transparent 68%
    );
}

.cr-tilt-profile__orb--two {
  bottom: -100px;
  left: -75px;

  background:
    radial-gradient(
      circle,
      var(--cr-purple),
      transparent 68%
    );
}

.cr-tilt-profile__grid {
  position: absolute;
  inset: 0;

  background-image:
    linear-gradient(
      rgba(255, 255, 255, 0.018) 1px,
      transparent 1px
    ),
    linear-gradient(
      90deg,
      rgba(255, 255, 255, 0.018) 1px,
      transparent 1px
    );

  background-size: 28px 28px;
}

.cr-tilt-profile__content {
  position: relative;
  z-index: 3;
  display: flex;
  flex-direction: column;
  min-height: 475px;
  transform-style: preserve-3d;
}

.cr-tilt-profile [data-depth] {
  position: relative;
  transform-style: preserve-3d;
  will-change: transform;
  transition: transform 180ms ease;
}

.cr-tilt-profile__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 28px;
}

.cr-tilt-profile__status {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--cr-soft);
  font-size: 11px;
  font-weight: 600;
}

.cr-tilt-profile__status-dot {
  position: relative;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #34d399;

  box-shadow:
    0 0 0 4px rgba(52, 211, 153, 0.08),
    0 0 14px rgba(52, 211, 153, 0.55);
}

.cr-tilt-profile__status-dot::after {
  content: "";
  position: absolute;
  inset: -4px;
  border: 1px solid rgba(52, 211, 153, 0.25);
  border-radius: inherit;
  animation: crTiltStatusPulse 2.2s ease-out infinite;
}

@keyframes crTiltStatusPulse {
  0% {
    transform: scale(0.7);
    opacity: 0.8;
  }

  70%,
  100% {
    transform: scale(1.8);
    opacity: 0;
  }
}

.cr-tilt-profile__badge {
  padding: 6px 10px;
  border: 1px solid rgba(168, 85, 247, 0.25);
  border-radius: 999px;
  background: rgba(168, 85, 247, 0.08);
  color: #d8b4fe;
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.14em;
}

.cr-tilt-profile__avatar-wrap {
  position: relative;
  width: 104px;
  height: 104px;
  margin: 2px auto 22px;

  display: flex;
  align-items: center;
  justify-content: center;
}

.cr-tilt-profile__avatar {
  position: relative;
  width: 88px;
  height: 88px;

  display: flex;
  align-items: center;
  justify-content: center;

  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 26px;

  background:
    linear-gradient(
      135deg,
      rgba(34, 211, 238, 0.22),
      rgba(168, 85, 247, 0.22)
    ),
    #11162b;

  box-shadow:
    0 15px 35px rgba(0, 0, 0, 0.3),
    0 0 30px rgba(34, 211, 238, 0.1);

  transform: rotate(-4deg);
  z-index: 2;
}

.cr-tilt-profile__avatar span {
  background:
    linear-gradient(
      135deg,
      #67e8f9,
      #c084fc
    );

  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;

  font-size: 27px;
  font-weight: 800;
}

.cr-tilt-profile__avatar-ring {
  position: absolute;
  inset: 0;
  border: 1px dashed rgba(34, 211, 238, 0.2);
  border-radius: 32px;
  animation: crTiltAvatarRing 14s linear infinite;
}

@keyframes crTiltAvatarRing {
  to {
    transform: rotate(360deg);
  }
}

.cr-tilt-profile__info {
  text-align: center;
}

.cr-tilt-profile__eyebrow {
  display: block;
  margin-bottom: 7px;
  color: var(--cr-cyan);
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.16em;
}

.cr-tilt-profile__info h2 {
  margin: 0;
  color: var(--cr-text);
  font-size: 31px;
  line-height: 1.1;
}

.cr-tilt-profile__info p {
  max-width: 310px;
  margin: 11px auto 0;
  color: var(--cr-muted);
  font-size: 12px;
  line-height: 1.65;
}

.cr-tilt-profile__skills {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 7px;
  margin-top: 18px;
}

.cr-tilt-profile__skills span {
  padding: 6px 10px;
  border: 1px solid rgba(255, 255, 255, 0.07);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.035);
  color: #b6c2d3;
  font-size: 9px;
  font-weight: 650;
}

.cr-tilt-profile__stats {
  display: grid;
  grid-template-columns: 1fr auto 1fr auto 1fr;
  align-items: center;

  margin-top: 24px;
  padding: 15px 10px;

  border-top: 1px solid rgba(255, 255, 255, 0.065);
  border-bottom: 1px solid rgba(255, 255, 255, 0.065);
}

.cr-tilt-profile__stat {
  text-align: center;
}

.cr-tilt-profile__stat strong {
  display: block;
  color: var(--cr-text);
  font-size: 16px;
}

.cr-tilt-profile__stat span {
  display: block;
  margin-top: 4px;
  color: #748198;
  font-size: 8px;
  text-transform: uppercase;
}

.cr-tilt-profile__divider {
  width: 1px;
  height: 25px;

  background:
    linear-gradient(
      to bottom,
      transparent,
      rgba(255, 255, 255, 0.12),
      transparent
    );
}

.cr-tilt-profile__actions {
  display: grid;
  grid-template-columns: 1fr 46px;
  gap: 10px;

  margin-top: auto;
  padding-top: 24px;
}

.cr-tilt-profile__button {
  appearance: none;
  border: 0;
  outline: none;
  font: inherit;
  cursor: pointer;
}

.cr-tilt-profile__button--primary {
  min-height: 46px;

  display: flex;
  align-items: center;
  justify-content: center;

  gap: 9px;
  padding: 0 18px;

  border: 1px solid rgba(34, 211, 238, 0.3);
  border-radius: 13px;

  background:
    linear-gradient(
      135deg,
      rgba(34, 211, 238, 0.15),
      rgba(168, 85, 247, 0.13)
    );

  color: #e6fbff;
  font-size: 11px;
  font-weight: 700;
}

.cr-tilt-profile__button--primary svg {
  width: 16px;
  height: 16px;
}

.cr-tilt-profile__button--icon {
  width: 46px;
  height: 46px;

  display: flex;
  align-items: center;
  justify-content: center;

  border: 1px solid rgba(255, 255, 255, 0.09);
  border-radius: 13px;

  background: rgba(255, 255, 255, 0.035);
  color: #b9c5d6;
}

.cr-tilt-profile__button--icon svg {
  width: 17px;
  height: 17px;
}

.cr-tilt-profile__accent {
  position: absolute;
  left: 18%;
  right: 18%;
  bottom: -1px;
  height: 2px;

  background:
    linear-gradient(
      90deg,
      transparent,
      var(--cr-cyan),
      var(--cr-purple),
      transparent
    );
}

.cr-tilt-profile__hint {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;

  margin-top: 18px;

  color: #5f6c82;
  font-size: 10px;
  font-weight: 600;
}

.cr-tilt-profile__hint-icon {
  color: var(--cr-cyan);
  font-size: 13px;
  animation: crTiltHintMove 1.8s ease-in-out infinite;
}

@keyframes crTiltHintMove {
  0%,
  100% {
    transform: translate(0, 0);
  }

  50% {
    transform: translate(3px, -3px);
  }
}

@media (max-width: 520px) {
  .cr-tilt-profile {
    min-height: auto;
    padding: 32px 16px;
  }

  .cr-tilt-profile__stage {
    max-width: 390px;
  }

  .cr-tilt-profile__card {
    min-height: 510px;
    padding: 23px 20px;
  }
}

@media (hover: none), (pointer: coarse) {
  .cr-tilt-profile__hint {
    display: none;
  }

  .cr-tilt-profile__card,
  .cr-tilt-profile [data-depth] {
    transform: none !important;
  }

  .cr-tilt-profile__glare {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cr-tilt-profile__status-dot::after,
  .cr-tilt-profile__avatar-ring,
  .cr-tilt-profile__hint-icon {
    animation: none !important;
  }
}
Enter fullscreen mode Exit fullscreen mode

JavaScript for the 3D Tilt Effect

The JavaScript calculates the cursor position relative to the card and converts it into X and Y rotation values.

We also use the data-depth values to move individual sections independently.

(() => {
  const component = document.querySelector("#crTiltProfile");
  if (!component) return;

  const card = component.querySelector(".cr-tilt-profile__card");
  const glow = component.querySelector(".cr-tilt-profile__glow");
  const glare = component.querySelector(".cr-tilt-profile__glare");
  const depthItems = component.querySelectorAll("[data-depth]");

  if (!card || !glow || !glare) return;

  const MAX_TILT = 10;
  const PARALLAX_STRENGTH = 0.32;

  const canHover = window.matchMedia(
    "(hover: hover) and (pointer: fine)"
  );

  const reduceMotion = window.matchMedia(
    "(prefers-reduced-motion: reduce)"
  );

  let targetRotateX = 0;
  let targetRotateY = 0;

  let currentRotateX = 0;
  let currentRotateY = 0;

  let pointerX = 0;
  let pointerY = 0;

  let isHovering = false;
  let animationFrame = null;

  const clamp = (value, min, max) => {
    return Math.min(Math.max(value, min), max);
  };

  const updateDepthItems = () => {
    depthItems.forEach((item) => {
      const depth =
        Number.parseFloat(item.dataset.depth) || 0;

      const moveX =
        pointerX * depth * PARALLAX_STRENGTH;

      const moveY =
        pointerY * depth * PARALLAX_STRENGTH;

      item.style.transform =
        `translate3d(${moveX}px, ${moveY}px, ${depth}px)`;
    });
  };

  const resetDepthItems = () => {
    depthItems.forEach((item) => {
      item.style.transform =
        "translate3d(0px, 0px, 0px)";
    });
  };

  const animate = () => {
    currentRotateX +=
      (targetRotateX - currentRotateX) * 0.12;

    currentRotateY +=
      (targetRotateY - currentRotateY) * 0.12;

    card.style.transform =
      `rotateX(${currentRotateX}deg)
       rotateY(${currentRotateY}deg)`;

    if (
      isHovering ||
      Math.abs(currentRotateX) > 0.01 ||
      Math.abs(currentRotateY) > 0.01
    ) {
      animationFrame =
        requestAnimationFrame(animate);
    } else {
      currentRotateX = 0;
      currentRotateY = 0;

      card.style.transform =
        "rotateX(0deg) rotateY(0deg)";

      animationFrame = null;
    }
  };

  const handlePointerMove = (event) => {
    if (!canHover.matches || reduceMotion.matches) {
      return;
    }

    const rect = card.getBoundingClientRect();

    const x = event.clientX - rect.left;
    const y = event.clientY - rect.top;

    const normalizedX =
      clamp((x / rect.width) * 2 - 1, -1, 1);

    const normalizedY =
      clamp((y / rect.height) * 2 - 1, -1, 1);

    pointerX = normalizedX;
    pointerY = normalizedY;

    targetRotateX =
      normalizedY * -MAX_TILT;

    targetRotateY =
      normalizedX * MAX_TILT;

    glow.style.left = `${x}px`;
    glow.style.top = `${y}px`;

    const glareX =
      50 + normalizedX * 35;

    const glareY =
      50 + normalizedY * 35;

    glare.style.background =
      `radial-gradient(
        circle at ${glareX}% ${glareY}%,
        rgba(255, 255, 255, 0.20) 0%,
        rgba(103, 232, 249, 0.08) 18%,
        rgba(168, 85, 247, 0.04) 34%,
        transparent 62%
      )`;

    updateDepthItems();
  };

  const handlePointerEnter = () => {
    if (!canHover.matches || reduceMotion.matches) {
      return;
    }

    isHovering = true;

    glow.style.opacity = "0.9";
    glare.style.opacity = "0.55";

    if (!animationFrame) {
      animationFrame =
        requestAnimationFrame(animate);
    }
  };

  const handlePointerLeave = () => {
    isHovering = false;

    targetRotateX = 0;
    targetRotateY = 0;

    pointerX = 0;
    pointerY = 0;

    glow.style.left = "50%";
    glow.style.top = "50%";
    glow.style.opacity = "0.5";

    glare.style.opacity = "0";

    resetDepthItems();

    if (!animationFrame) {
      animationFrame =
        requestAnimationFrame(animate);
    }
  };

  card.addEventListener(
    "pointerenter",
    handlePointerEnter
  );

  card.addEventListener(
    "pointermove",
    handlePointerMove
  );

  card.addEventListener(
    "pointerleave",
    handlePointerLeave
  );

  window.addEventListener("blur", () => {
    if (isHovering) {
      handlePointerLeave();
    }
  });

  document.addEventListener(
    "visibilitychange",
    () => {
      if (document.hidden && isHovering) {
        handlePointerLeave();
      }
    }
  );
})();
Enter fullscreen mode Exit fullscreen mode

How the Tilt Effect Works

The interaction starts by reading the pointer position inside the card:

const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
Enter fullscreen mode Exit fullscreen mode

Those values are converted to a range between -1 and 1.

That normalized position determines how far the card should rotate.

targetRotateX = normalizedY * -MAX_TILT;
targetRotateY = normalizedX * MAX_TILT;
Enter fullscreen mode Exit fullscreen mode

The maximum rotation is limited to:

const MAX_TILT = 10;
Enter fullscreen mode Exit fullscreen mode

Keeping the rotation relatively small makes the interaction feel smooth instead of making the card difficult to read.


Creating Layered Parallax

The HTML contains attributes such as:

data-depth="18"
data-depth="34"
data-depth="26"
Enter fullscreen mode Exit fullscreen mode

JavaScript reads these values and applies different translate3d() positions to each content group.

For example, the avatar uses a larger depth value than the status row, so it appears closer to the viewer while the card is moving.

This creates the layered 3D illusion without requiring WebGL or a JavaScript animation library.


Cursor-Follow Glow

The glow element follows the pointer:

glow.style.left = `${x}px`;
glow.style.top = `${y}px`;
Enter fullscreen mode Exit fullscreen mode

Because the glow itself is a blurred radial gradient, moving it around the card creates the appearance of an interactive light source.

The cyan and purple colors also match the rest of the card's visual system.


Dynamic Glare

The glare position is calculated from the normalized pointer coordinates:

const glareX = 50 + normalizedX * 35;
const glareY = 50 + normalizedY * 35;
Enter fullscreen mode Exit fullscreen mode

Those values are then used as the center of a radial gradient.

As the cursor moves, the highlight moves across the surface of the card, helping reinforce the 3D effect.


Touch Device Support

A mouse-based tilt effect doesn't translate naturally to every touch device.

The component checks whether the browser supports a traditional hover-capable pointer:

const canHover = window.matchMedia(
  "(hover: hover) and (pointer: fine)"
);
Enter fullscreen mode Exit fullscreen mode

The CSS also disables the tilt transforms and glare on coarse-pointer devices.

@media (hover: none), (pointer: coarse) {
  .cr-tilt-profile__card,
  .cr-tilt-profile [data-depth] {
    transform: none !important;
  }

  .cr-tilt-profile__glare {
    display: none;
  }
}
Enter fullscreen mode Exit fullscreen mode

The profile card therefore remains usable on phones and tablets instead of depending on mouse interaction.


Reduced Motion Support

Interactive effects should also respect users who prefer reduced motion.

The component checks:

@media (prefers-reduced-motion: reduce)
Enter fullscreen mode Exit fullscreen mode

and disables decorative animations such as the pulsing availability indicator, rotating avatar ring, and interaction hint.

JavaScript also checks the same preference before activating pointer-based motion.


Try the Live Demo

You can experiment with the complete component on CodePen:

👉 3D Tilt Profile Card - Live CodePen Demo

Move your cursor across different parts of the card to see how the rotation, parallax layers, glow, and glare respond.


Where Can You Use This Component?

A 3D profile card can work especially well in:

  • Developer portfolios
  • Designer portfolios
  • Team member sections
  • Personal landing pages
  • Creator profiles
  • Agency websites
  • SaaS team pages
  • Interactive showcase sections

Because the profile information is regular HTML, you can easily replace the name, role, skills, statistics, buttons, and avatar with your own content.


Customization Ideas

Once you understand the interaction logic, there are plenty of ways to customize the component.

You could:

  • Replace the initials with a real profile image
  • Change the cyan and purple theme
  • Add social media buttons
  • Link the View Profile button to a portfolio
  • Add GitHub or LinkedIn information
  • Adjust MAX_TILT for stronger or softer rotation
  • Change individual data-depth values
  • Add more profile statistics
  • Convert it into a team member card
  • Use the same tilt engine for product or pricing cards

For a stronger 3D effect, increase the depth values slightly.

For a more subtle professional interface, reduce both the depth values and MAX_TILT.


Final Thoughts

This component combines several small effects to create one polished interaction.

The card itself uses CSS for the visual design, while JavaScript handles pointer tracking, rotation, parallax depth, cursor-follow lighting, and dynamic glare.

The important part is that the effect remains progressive: desktop users get the full interactive experience, while touch devices still receive a clean and functional profile card.

You can view the complete tutorial and copy the source code here:

👉 3D Tilt Profile Card - Full Tutorial & Source Code

If you're experimenting with modern CSS and JavaScript interactions, try changing the tilt strength and data-depth values to create your own version.

Happy coding! 🚀

Top comments (0)