├── README.md ├── src ├── assets │ └── Inter.ttf ├── main.jsx ├── App.jsx ├── Cursor.jsx └── TextDistort.jsx ├── vite.config.js ├── .gitignore ├── index.html ├── package.json └── eslint.config.js /README.md: -------------------------------------------------------------------------------- 1 | # apple-glass 2 | 3 | An attempt to recreate Apple's liquid glass effect 4 | -------------------------------------------------------------------------------- /src/assets/Inter.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tejas-swiggy/apple-glass/HEAD/src/assets/Inter.ttf -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | 5 | ReactDOM.createRoot(document.getElementById("root")).render( 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Liquid Glass 7 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { Canvas } from "@react-three/fiber"; 2 | import { Environment } from "@react-three/drei"; 3 | import { useState } from "react"; 4 | 5 | // Components 6 | import Cursor from "./Cursor"; 7 | import TextDistort from "./TextDistort"; 8 | 9 | 10 | export default function App() { 11 | const [hoveringText, setHoveringText] = useState(false); 12 | 13 | return ( 14 | 15 | 16 | {/* Lighting */} 17 | 18 | 19 | {/* Scene */} 20 | 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@react-three/drei": "^10.1.2", 14 | "@react-three/fiber": "^9.1.2", 15 | "@react-three/postprocessing": "^3.0.4", 16 | "gsap": "^3.13.0", 17 | "postprocessing": "^6.37.4", 18 | "react": "^19.1.0", 19 | "react-dom": "^19.1.0", 20 | "three": "^0.177.0", 21 | "troika-three-text": "^0.52.4" 22 | }, 23 | "devDependencies": { 24 | "@eslint/js": "^9.25.0", 25 | "@types/react": "^19.1.2", 26 | "@types/react-dom": "^19.1.2", 27 | "@vitejs/plugin-react": "^4.4.1", 28 | "eslint": "^9.25.0", 29 | "eslint-plugin-react-hooks": "^5.2.0", 30 | "eslint-plugin-react-refresh": "^0.4.19", 31 | "globals": "^16.0.0", 32 | "vite": "^6.3.5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | 6 | export default [ 7 | { ignores: ['dist'] }, 8 | { 9 | files: ['**/*.{js,jsx}'], 10 | languageOptions: { 11 | ecmaVersion: 2020, 12 | globals: globals.browser, 13 | parserOptions: { 14 | ecmaVersion: 'latest', 15 | ecmaFeatures: { jsx: true }, 16 | sourceType: 'module', 17 | }, 18 | }, 19 | plugins: { 20 | 'react-hooks': reactHooks, 21 | 'react-refresh': reactRefresh, 22 | }, 23 | rules: { 24 | ...js.configs.recommended.rules, 25 | ...reactHooks.configs.recommended.rules, 26 | 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], 27 | 'react-refresh/only-export-components': [ 28 | 'warn', 29 | { allowConstantExport: true }, 30 | ], 31 | }, 32 | }, 33 | ] 34 | -------------------------------------------------------------------------------- /src/Cursor.jsx: -------------------------------------------------------------------------------- 1 | import { useThree, useFrame } from "@react-three/fiber"; 2 | import { MeshTransmissionMaterial } from "@react-three/drei"; 3 | import { useRef, useState, useEffect } from "react"; 4 | import { gsap } from "gsap"; 5 | 6 | export default function Cursor({ isHovering }) { 7 | const meshRef = useRef(); 8 | const { viewport } = useThree(); 9 | const [target, setTarget] = useState({ x: 0, y: 0 }); 10 | 11 | useFrame(() => { 12 | if (!meshRef.current) return; 13 | 14 | // Track cursor 15 | gsap.to(meshRef.current.position, { 16 | x: target.x, 17 | y: target.y, 18 | duration: 0.17, 19 | ease: "power2.out", 20 | }); 21 | }); 22 | 23 | useEffect(() => { 24 | // Convert pixel coordinates to Three coordinates 25 | const handleMouseMove = (e) => { 26 | const x = (e.clientX / window.innerWidth) * 2 - 1; 27 | const y = -(e.clientY / window.innerHeight) * 2 + 1; 28 | setTarget({ 29 | x: x * viewport.width / 2, 30 | y: y * viewport.height / 2, 31 | }); 32 | }; 33 | 34 | window.addEventListener("mousemove", handleMouseMove); 35 | return () => window.removeEventListener("mousemove", handleMouseMove); 36 | }, [viewport]); 37 | 38 | useEffect(() => { 39 | // Scale on hover 40 | if (!meshRef.current) return; 41 | gsap.to(meshRef.current.scale, { 42 | x: isHovering ? 2.5 : 1, 43 | y: isHovering ? 2.5 : 1, 44 | z: isHovering ? 2.5 : 1, 45 | duration: 0.13, 46 | ease: "cubic-bezier(.8,-0.84,.16,1.68)", 47 | }); 48 | }, [isHovering]); 49 | 50 | 51 | 52 | return ( 53 | 54 | 55 | 66 | 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/TextDistort.jsx: -------------------------------------------------------------------------------- 1 | import { Text } from "@react-three/drei"; 2 | import { useRef, useState, useEffect } from "react"; 3 | import { useFrame, useThree } from "@react-three/fiber"; 4 | import Inter from "./assets/Inter.ttf"; 5 | import * as THREE from "three"; 6 | 7 | // Damping for magnetic interaction 8 | const damping = 0.1; 9 | 10 | export default function TextDistort({ setHoveringText }) { 11 | const textRef = useRef(); 12 | const { viewport, size } = useThree(); 13 | const [isHovered, setIsHovered] = useState(false); 14 | const currentOffset = useRef(new THREE.Vector3(0, 0, 0)); 15 | const mouse = useRef({ x: 0, y: 0 }); 16 | 17 | // Convert pixel coordinates to Three 18 | useEffect(() => { 19 | const handleMouseMove = (e) => { 20 | const x = (e.clientX / size.width) * 2 - 1; 21 | const y = -(e.clientY / size.height) * 2 + 1; 22 | mouse.current.x = x * (viewport.width / 2); 23 | mouse.current.y = y * (viewport.height / 2); 24 | }; 25 | 26 | window.addEventListener("mousemove", handleMouseMove); 27 | return () => window.removeEventListener("mousemove", handleMouseMove); 28 | }, [size, viewport]); 29 | 30 | useFrame(() => { 31 | if (!textRef.current) return; 32 | 33 | const mesh = textRef.current; 34 | const offsetX = mouse.current.x * damping; 35 | const offsetY = mouse.current.y * damping; 36 | 37 | if (isHovered) { 38 | // Smoothly approach the offset from original position 39 | currentOffset.current.x += (offsetX - currentOffset.current.x) * 0.1; 40 | currentOffset.current.y += (offsetY - currentOffset.current.y) * 0.1; 41 | } else { 42 | // Smoothly return to original position 43 | currentOffset.current.x += (0 - currentOffset.current.x) * 0.1; 44 | currentOffset.current.y += (0 - currentOffset.current.y) * 0.1; 45 | } 46 | 47 | mesh.position.x = currentOffset.current.x; 48 | mesh.position.y = currentOffset.current.y; 49 | }); 50 | 51 | return ( 52 | { 67 | e.stopPropagation(); 68 | setIsHovered(true); 69 | setHoveringText(true); 70 | }} 71 | onPointerOut={(e) => { 72 | e.stopPropagation(); 73 | setIsHovered(false); 74 | setHoveringText(false); 75 | }} 76 | > 77 | Liquid Glass 78 | 79 | ); 80 | } 81 | --------------------------------------------------------------------------------