Skip to content

Member Card

The 3D animated membership card is a signature visual element in the customer-app. It displays the member's tier, points balance, and membership details with a premium, tactile feel.

RetailOS Member
RINA SARI
8847 2290 3341
Gold Member
12.450 poin

Anatomy

+-------------------------------------------+
|  [Chip]                      [Contactless] |
|                                            |
|  RETAILOS MEMBER                           |
|                                            |
|  RINA SARI                                 |
|  ID: 8847 2290 3341                        |
|                                            |
|  GOLD MEMBER        12.450 poin            |
+-------------------------------------------+

Tier Gradients

TierGradientText Color
Silverlinear-gradient(135deg, #C0C0C0, #E8E8E8, #A8A8A8)#4A4A4A
Goldlinear-gradient(135deg, #D4A843, #F2D477, #C49A2A)#5C3D0E
Platinumlinear-gradient(135deg, #2D2D2D, #4A4A4A, #1A1A1A)#E0E0E0
Employeelinear-gradient(135deg, #2D6B4A, #4A9E72, #1E5038)#FFFFFF

Visual Elements

ElementStyle
Card size340x200px (mobile), aspect-ratio 1.7:1
Border radius16px
Chip36x28px gold rectangle with etched lines, top-left
Contactless symbol3 curved arcs, 20px, top-right
Brand text10px, uppercase, letter-spacing 0.15em
Member name16px, font-semibold, uppercase
Member IDfont-mono, 13px, spaced groups of 4 digits
Tier label12px, uppercase, letter-spacing 0.1em
Pointsfont-mono, 16px, font-bold

3D Tilt Effect

The card tilts in response to device motion (mobile) or mouse position (web):

  • Maximum tilt: 15 degrees on each axis
  • Perspective: 1000px
  • Transition: transform 100ms ease-out
  • Glare overlay: white gradient that shifts with tilt angle, opacity 0.15
  • Shadow: dynamic, shifts opposite to tilt direction

Implementation

tsx
import { useRef, useState } from 'react'

interface TiltState {
  rotateX: number
  rotateY: number
  glareX: number
  glareY: number
}

export function MemberCard({ member }: { member: MemberData }) {
  const cardRef = useRef<HTMLDivElement>(null)
  const [tilt, setTilt] = useState<TiltState>({ rotateX: 0, rotateY: 0, glareX: 50, glareY: 50 })

  const handleMouseMove = (e: React.MouseEvent) => {
    const rect = cardRef.current?.getBoundingClientRect()
    if (!rect) return

    const x = (e.clientX - rect.left) / rect.width
    const y = (e.clientY - rect.top) / rect.height

    setTilt({
      rotateX: (0.5 - y) * 30,    // max 15deg
      rotateY: (x - 0.5) * 30,    // max 15deg
      glareX: x * 100,
      glareY: y * 100,
    })
  }

  const handleMouseLeave = () => {
    setTilt({ rotateX: 0, rotateY: 0, glareX: 50, glareY: 50 })
  }

  const tierGradients = {
    silver: 'linear-gradient(135deg, #C0C0C0, #E8E8E8, #A8A8A8)',
    gold: 'linear-gradient(135deg, #D4A843, #F2D477, #C49A2A)',
    platinum: 'linear-gradient(135deg, #2D2D2D, #4A4A4A, #1A1A1A)',
    employee: 'linear-gradient(135deg, #2D6B4A, #4A9E72, #1E5038)',
  }

  const tierTextColors = {
    silver: '#4A4A4A', gold: '#5C3D0E', platinum: '#E0E0E0', employee: '#FFFFFF',
  }

  return (
    <div style={{ perspective: '1000px' }} className="flex justify-center">
      <div
        ref={cardRef}
        onMouseMove={handleMouseMove}
        onMouseLeave={handleMouseLeave}
        className="relative w-[340px] aspect-[1.7/1] rounded-2xl overflow-hidden cursor-pointer select-none"
        style={{
          background: tierGradients[member.tier],
          transform: `rotateX(${tilt.rotateX}deg) rotateY(${tilt.rotateY}deg)`,
          transition: 'transform 100ms ease-out',
          boxShadow: `${tilt.rotateY * 0.5}px ${-tilt.rotateX * 0.5}px 20px rgba(0,0,0,0.25)`,
        }}
      >
        {/* Glare overlay */}
        <div
          className="absolute inset-0 pointer-events-none"
          style={{
            background: `radial-gradient(circle at ${tilt.glareX}% ${tilt.glareY}%, rgba(255,255,255,0.25), transparent 60%)`,
          }}
        />

        {/* Content */}
        <div className="relative z-10 p-5 h-full flex flex-col justify-between"
          style={{ color: tierTextColors[member.tier] }}
        >
          <div className="flex justify-between items-start">
            {/* Chip */}
            <div className="w-9 h-7 rounded-[4px] bg-gradient-to-br from-yellow-400 to-yellow-600 border border-yellow-700/30" />
            {/* Contactless */}
            <ContactlessIcon className="w-5 h-5 opacity-60" />
          </div>

          <div>
            <p className="text-[10px] uppercase tracking-[0.15em] opacity-70 mb-3">RetailOS Member</p>
            <p className="text-base font-semibold uppercase tracking-wide">{member.name}</p>
            <p className="font-mono text-[13px] opacity-80 mt-0.5">
              {member.id.match(/.{4}/g)?.join(' ')}
            </p>
          </div>

          <div className="flex justify-between items-end">
            <p className="text-xs uppercase tracking-[0.1em] opacity-80">
              {member.tier} Member
            </p>
            <p className="font-mono text-base font-bold">
              {member.points.toLocaleString('id-ID')} poin
            </p>
          </div>
        </div>
      </div>
    </div>
  )
}

Mobile (React Native) Considerations

On mobile, use react-native-reanimated for the tilt animation driven by device accelerometer via DeviceMotion sensor.

  • Use Animated.View with transform style
  • Throttle sensor updates to 60fps
  • Respect prefers-reduced-motion --- disable tilt, keep static gradient

Flip Animation

Tap the card to flip and show the back:

  • Back side: QR code for in-store scanning, barcode, member since date
  • Flip: 3D rotateY(180deg) transition, 500ms, ease-in-out
  • Use backface-visibility: hidden on both sides

Performance

Use will-change: transform and transform: translateZ(0) to promote the card to its own compositing layer for smooth animation.

Don't

  • Don't use tilt animation on low-end devices --- detect and disable via navigator.hardwareConcurrency < 4
  • Don't use real card numbers in screenshots or documentation
  • Don't place interactive elements on top of the card surface --- tilt conflicts with tap targets

RetailOS - Sistem ERP Retail Modern untuk Indonesia