Skip to content
Welcome to vote for your favorite component! You can also tell me anything you want to say! (*´∀`)~♥

Stereoscopic Wrapper wrapper

Give elements a cool 3D tilt effect. ˋ( ° ▽、° )

Usage Examples

Basic Usage

You can toggle it off at will, but it won't save electricity. 乁( ◔ ௰◔)「

Hello
It's floating
Cool floating
Hello
View example source code
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl p-6">
    <div class="flex gap-4 border rounded p-4">
      <base-checkbox
        v-model="enable"
        :label="t('floatToggle')"
        class="w-full"
      />
    </div>

    <div class="flex flex-col items-start gap-4">
      <wrapper-stereoscopic :enable>
        <div class="h-80 w-80 flex-center rounded bg-gray-300">
          <div class="h-40 w-40 flex-center rounded bg-gray-100">
            <div class="text-xl text-gray-600 font-bold">
              {{ t('hello') }}
            </div>
          </div>
        </div>
      </wrapper-stereoscopic>

      <div class="flex flex-col items-start justify-start gap-4">
        <wrapper-stereoscopic :enable>
          <div class="border rounded-full p-4 text-xl text-gray-600 font-bold">
            {{ t('floating') }}
          </div>
        </wrapper-stereoscopic>

        <wrapper-stereoscopic :enable>
          <div class="border rounded-full p-4 text-xl text-gray-600 font-bold">
            {{ t('coolFloat') }}
          </div>
        </wrapper-stereoscopic>

        <wrapper-stereoscopic :enable>
          <div class="border rounded-full p-4 text-xl text-gray-600 font-bold">
            {{ t('hello') }}
          </div>
        </wrapper-stereoscopic>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'

import BaseCheckbox from '../../base-checkbox.vue'
import WrapperStereoscopic from '../wrapper-stereoscopic.vue'

const { t } = useI18n()
const enable = ref(true)
</script>

<style lang="sass" scoped>
.flex-center
  display: flex
  justify-content: center
  align-items: center
</style>

Multi-Layer Parallax

Multiple layers can create a multi-level stereoscopic effect.

Hello
View example source code
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl p-6">
    <div class="content flex items-start gap-4">
      <wrapper-stereoscopic v-slot="wrapper">
        <div
          class="h-80 w-80 flex-center rounded bg-gray-300"
          :style="wrapper.style"
        >
          <wrapper-stereoscopic-layer v-slot="layer01">
            <div
              class="h-40 w-40 flex-center rounded bg-gray-200"
              :style="layer01.style"
            >
              <wrapper-stereoscopic-layer v-slot="layer02">
                <div
                  class="rounded bg-gray-100 p-4 text-xl font-bold"
                  :style="layer02.style"
                >
                  {{ t('hello') }}
                </div>
              </wrapper-stereoscopic-layer>
            </div>
          </wrapper-stereoscopic-layer>
        </div>
      </wrapper-stereoscopic>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import WrapperStereoscopicLayer from '../wrapper-stereoscopic-layer.vue'
import WrapperStereoscopic from '../wrapper-stereoscopic.vue'

const { t } = useI18n()
</script>

<style lang="sass" scoped>
.content
  perspective: 2000px
.flex-center
  display: flex
  justify-content: center
  align-items: center
</style>

Max Tilt Angle

You can set the maximum tilt angle.

( •̀ ω •́ )✧
View example source code
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl p-6">
    <div class="flex gap-4 border rounded p-4">
      <base-input
        v-model.number="x"
        type="range"
        :label="`${t('xMaxAngle')}: ${x} ${t('degrees')}`"
        class="w-full"
        :min="0"
        :max="90"
      />

      <base-input
        v-model.number="y"
        type="range"
        :label="`${t('yMaxAngle')}: ${y} ${t('degrees')}`"
        class="w-full"
        :min="0"
        :max="90"
      />
    </div>

    <div class="content flex items-start gap-4">
      <wrapper-stereoscopic
        :x-max-angle="x"
        :y-max-angle="y"
      >
        <div class="border rounded-full p-4 text-xl text-gray-600 font-bold">
          ( •̀ ω •́ )✧
        </div>
      </wrapper-stereoscopic>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'

import BaseInput from '../../base-input.vue'
import WrapperStereoscopic from '../wrapper-stereoscopic.vue'

const { t } = useI18n()
const x = ref(15)
const y = ref(15)
</script>

<style lang="sass" scoped>
.content
  perspective: 2000px
.flex-center
  display: flex
  justify-content: center
  align-items: center
</style>

Float Distance

You can set the float distance between each layer—lay flat whenever you want. _(:3」ㄥ)_

Hello
View example source code
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl p-6">
    <div class="flex gap-4 border rounded p-4">
      <base-input
        v-model.number="zOffset"
        type="range"
        :label="`${t('floatDistance')}: ${zOffset} px`"
        class="w-full"
        :min="0"
        :max="200"
      />
    </div>

    <div class="content flex items-start gap-4">
      <wrapper-stereoscopic
        v-slot="wrapper"
        :z-offset="zOffset"
      >
        <div
          class="h-80 w-80 flex-center rounded bg-gray-300"
          :style="wrapper.style"
        >
          <wrapper-stereoscopic-layer v-slot="layer01">
            <div
              class="h-40 w-40 flex-center rounded bg-gray-200"
              :style="layer01.style"
            >
              <wrapper-stereoscopic-layer v-slot="layer02">
                <div
                  class="rounded bg-gray-100 p-4 text-xl font-bold"
                  :style="layer02.style"
                >
                  {{ t('hello') }}
                </div>
              </wrapper-stereoscopic-layer>
            </div>
          </wrapper-stereoscopic-layer>
        </div>
      </wrapper-stereoscopic>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'

import BaseInput from '../../base-input.vue'
import WrapperStereoscopicLayer from '../wrapper-stereoscopic-layer.vue'
import WrapperStereoscopic from '../wrapper-stereoscopic.vue'

const { t } = useI18n()
const zOffset = ref(100)
</script>

<style lang="sass" scoped>
.content
  perspective: 2000px
.flex-center
  display: flex
  justify-content: center
  align-items: center
</style>

Custom Strategy

You can customize rotation and float distance for richer interactions.

(´● ω ●`)
Look here
View example source code
vue
<template>
  <div class="w-full flex flex-col items-center justify-center gap-8 border border-gray-200 rounded-xl p-6">
    <div class="content flex items-start">
      <wrapper-stereoscopic
        v-slot="wrapper"
        v-bind="params"
      >
        <div
          class="cursor-zoom-in select-none border rounded-full"
          :style="wrapper.style"
        >
          <wrapper-stereoscopic-layer v-slot="layer01">
            <div
              class="flex-center px-14 py-8 text-2xl tracking-widest"
              :style="layer01.style"
            >
              (´● ω ●`)
            </div>
          </wrapper-stereoscopic-layer>
        </div>
      </wrapper-stereoscopic>
    </div>

    <div
      ref="blockRef"
      class="w-full cursor-zoom-in border rounded border-dashed p-2 text-center text-xs tracking-widest"
    >
      {{ t('lookHere') }}
    </div>
  </div>
</template>

<script setup lang="ts">
import type { ExtractComponentProps } from '../../../types'
import { useElementHover } from '@vueuse/core'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { mapNumber } from '../../../common/utils'
import WrapperStereoscopicLayer from '../wrapper-stereoscopic-layer.vue'
import WrapperStereoscopic from '../wrapper-stereoscopic.vue'

type Props = ExtractComponentProps<typeof WrapperStereoscopic>

const { t } = useI18n()

const blockRef = ref()
const isHovered = useElementHover(blockRef)

const params: Props = {
  strategy(params) {
    const {
      mousePosition: { x, y },
      size: { width, height },
    } = params

    if (isHovered.value) {
      return {
        x: mapNumber(y, -height, height, -50, 50),
        y: mapNumber(x, -width, width, -60, 60),
        zOffset: -100,
      }
    }

    if (
      params.isOutside
      || !params.enable
      || !params.isVisible
      || params.isPressed) {
      return {
        x: 0,
        y: 0,
        zOffset: 0,
      }
    }

    return {
      x: mapNumber(y, -height, height, -30, 30),
      y: mapNumber(x, -width, width, -40, 40),
      zOffset: 100,
    }
  },
}
</script>

<style lang="sass" scoped>
.content
  perspective: 2000px
</style>

How It Works

Uses CSS perspective and transform3d to create 3D rotation and perspective distortion effects.

The perspective property is especially important—it's what makes objects appear to have perspective distortion.

Without it, objects just look like they got squished for no reason. ...('◉◞⊖◟◉` )

📚 CSS perspective

📚 CSS translate3d

Once you know how to tilt, the rest is simple. ( •̀ ω •́ )✧

Calculate the vector from the object's center to the mouse position, map the vector's x and y components to the configured angle range, and apply it to the transform.

There's a small trick here though—instead of setting the "current angle" directly to the "target angle", we gradually approach the "target angle".

This way, no matter how wildly the "target angle" jumps, the tilt effect always has a smooth animation, looking more natural and comfortable. ◝(≧∀≦)◟

Source Code

API

Props

interface StrategyParams {
  enable: boolean;
  xMaxAngle: number;
  yMaxAngle: number;
  zOffset: number;
  /** 以元素中心為零點,目前滑鼠的座標 */
  mousePosition: Record<'x' | 'y', number>;
  /** 元素尺寸 */
  size: Record<'width' | 'height', number>;
  /** 滑鼠是否在元素外 */
  isOutside: boolean;
  /** 元素是否可見 */
  isVisible: boolean;
  /** 是否被按下 */
  isPressed: boolean;
}

interface Props {
  /** 是否開啟 */
  enable?: boolean;
  /** x 最大偏轉角度 */
  xMaxAngle?: number;
  /** y 最大偏轉角度 */
  yMaxAngle?: number;
  /** 懸浮高度 */
  zOffset?: number;

  /** 旋轉、懸浮距離邏輯 */
  strategy?: (params: StrategyParams) => Record<'x' | 'y' | 'zOffset', number>;

  /** 更新週期,越短會越快到達目標狀態
   *
   * @default 15
   */
  updateInterval?: number;
}

v0.60.0