React animated cursor is a React component that creates a custom cursor
experience. You can craft a variety of cursor types, and animate
movement, hover and clicking properties.
Hover over these links and see how that animated cursor does it's
thing. Kinda nifty, right? Not applicable to most projects, but a nice
move for more interactive/immersive stuff... if you're into that kinda
thing? Here's another link to nowhere.
Essentially, the cursor consists:
An inner dot (cursorInner)
An outer, outlining circle (cursorOuter), with slight
opacity based on the dot/primary color
An inversely scaling effect between the inner and outer cursor parts
on click or link hover
Style props exist for in the inner and outer cursor allow you to easily
create unique cursor types. Play with css variables to influence
the cursor, cursor outline size, and amount of scale on target hover.
>
)
}
/**
* AnimatedCursor
* Calls and passes props to CursorCore if not a touch/mobile device.
*/
function AnimatedCursor({
children,
clickables,
color,
innerScale,
innerSize,
innerStyle,
outerAlpha,
outerScale,
outerSize,
outerStyle,
showSystemCursor,
trailingSpeed
}: AnimatedCursorProps) {
const isTouchdevice = useIsTouchdevice()
if (typeof window !== 'undefined' && isTouchdevice) {
return <>>
}
return (
{children}
)
}
export default AnimatedCursor
================================================
FILE: lib/AnimatedCursor.types.ts
================================================
import { CSSProperties, ReactNode } from 'react'
export interface AnimatedCursorOptions {
children?: ReactNode
color?: string
innerScale?: number
innerSize?: number
innerStyle?: CSSProperties
outerAlpha?: number
outerScale?: number
outerSize?: number
outerStyle?: CSSProperties
}
export type Clickable = string | ({ target: string } & AnimatedCursorOptions)
export interface AnimatedCursorProps extends AnimatedCursorOptions {
clickables?: Clickable[]
showSystemCursor?: boolean
trailingSpeed?: number
}
export interface AnimatedCursorCoordinates {
x: number
y: number
}
================================================
FILE: lib/helpers/find.ts
================================================
export default function findInArray(
arr: T[],
callback: (element: T, index: number, array: T[]) => boolean,
...args
): T | undefined {
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function')
}
const list = Object(arr)
// Makes sure it always has a positive integer as length.
const length = list.length >>> 0
const thisArg = args[2]
for (let i = 0; i < length; i++) {
const element = list[i]
if (callback.call(thisArg, element, i, list)) {
return element
}
}
return undefined
}
================================================
FILE: lib/hooks/useEventListener.ts
================================================
import { useEffect, useRef } from 'react'
type AllEventMaps = HTMLElementEventMap & DocumentEventMap & WindowEventMap
export function useEventListener(
type: K,
listener: (event: HTMLElementEventMap[K]) => void,
element: HTMLElement
): void
export function useEventListener(
type: K,
listener: (event: DocumentEventMap[K]) => void,
element: Document
): void
export function useEventListener(
type: K,
listener: (event: WindowEventMap[K]) => void,
element?: Window
): void
export function useEventListener(
type: K,
listener: (event: AllEventMaps[K]) => void,
element?: HTMLElement | Document | Window | null
) {
const listenerRef = useRef(listener)
useEffect(() => {
listenerRef.current = listener
})
useEffect(() => {
const el = element === undefined ? window : element
const internalListener = (ev: AllEventMaps[K]) => {
return listenerRef.current(ev)
}
el?.addEventListener(
type,
internalListener as EventListenerOrEventListenerObject
)
return () => {
el?.removeEventListener(
type,
internalListener as EventListenerOrEventListenerObject
)
}
}, [type, element])
}
================================================
FILE: lib/hooks/useIsTouchdevice.ts
================================================
import { useEffect, useState } from 'react'
const useIsTouchdevice = (): boolean => {
const [isTouchdevice, setIsTouchdevice] = useState()
useEffect(() => {
if (typeof window !== 'undefined') {
setIsTouchdevice(window.matchMedia('(hover: none)').matches)
}
}, [])
return isTouchdevice
}
export default useIsTouchdevice
================================================
FILE: lib/index.ts
================================================
import AnimatedCursor from './AnimatedCursor'
export default AnimatedCursor
================================================
FILE: package.json
================================================
{
"name": "react-animated-cursor",
"version": "2.11.1",
"description": "An animated custom cursor component in React.",
"author": "Stephen Scaff ",
"homepage": "https://stephenscaff.github.io/react-animated-cursor/",
"main": "dist/index.js",
"module": "dist/index.es.js",
"types": "dist/index.d.ts",
"browser": "dist/index.umd.js",
"files": [
"dist/index.js",
"dist/index.es.js",
"dist/index.umd.js",
"dist/index.d.ts"
],
"targets": {
"main": false,
"module": false,
"browser": false,
"types": false
},
"scripts": {
"clean": "rm -rf ./dist",
"build": "rollup -c",
"dev": "parcel ./docs/src/index.html --dist-dir ./docs/dist",
"demo:clean": "rm -rf ./docs/dist",
"demo:start": "parcel ./docs/src/index.html --dist-dir ./docs/dist",
"demo:build": "parcel build ./docs/src/index.html --dist-dir ./docs/dist --public-url ./",
"demo:deploy": "npm run demo:build && gh-pages -d ./docs/dist",
"prepare": "npm run build",
"prepublish": "rm -rf ./dist && npm run build",
"lint": "eslint \"lib/**/*.+(ts|tsx)\" --fix ",
"format": "prettier --write \"lib/**/*.+(ts|tsx)\""
},
"keywords": [
"react cursor",
"custom cursor",
"animated cursor"
],
"license": "ISC",
"repository": {
"type": "git",
"url": "https://github.com/stephenscaff/react-animated-cursor"
},
"bugs": {
"url": "https://github.com/stephenscaff/react-animated-cursor/issues"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-typescript": "^11.1.1",
"@types/react": "^18.2.6",
"@types/react-dom": "^18.2.4",
"@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.36.2",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"gh-pages": "^5.0.0",
"parcel": "^2.3.2",
"prettier": "^2.0.5",
"process": "^0.11.10",
"react": "18.2.0",
"react-dom": "18.2.0",
"rollup": "^3.22.0",
"rollup-plugin-dts": "^5.3.0",
"rollup-plugin-peer-deps-external": "^2.2.2",
"typescript": "^5.0.4"
}
}
================================================
FILE: readme.md
================================================
# React Animated Cursor
A React component that replaces the native cursor with a custom animated [jawn](https://www.urbandictionary.com/define.php?term=Jawn). Available options and props allow you to easily craft a unique cursor experience.
## Contents
1. [📌 Features](#-features)
2. [🎯 Quickstart](#-quickstart)
3. [🤖 Commands](#-commands)
4. [🧬 Options](#-options)
5. [🕹️ Usage](#-usage)
6. [🎨 Cursor Types](#-cursor-types)
7. [📓 Notes](#-notes)
8. [📅 To Dos](#-to-dos)
## 📌 Features
### The custom cursor is comprised of
- An inner dot (`cursorInner`)
- An outer, outlining circle (`cursorOuter`), with slight opacity based on the dot/primary color
- A slight trailing animation of the outer outline
- An inversely scaling effect between the inner and outer cursor parts on click or link hover
Options exist for modifying the color and scaling of the cursor elements (see props/options below). Style props for in the inner and outer cursor allow you to easily create unique cursor types.
[Live Demo→](https://stephenscaff.github.io/react-animated-cursor/)
## 🎯 Quickstart
### Install package from npm
`npm i react-animated-cursor`
### Add to you project
Add to a global location, like `_app.js`
```
import React from "react";
import AnimatedCursor from "react-animated-cursor"
export default function App() {
return (
);
}
```
## 🤖 Commands
**Install** `npm i react-animated-cursor`
**Build**: `npm run build`
**Dev**: `npm run dev`
**Demo Run**: `npm run demo:start`
**Demo Build**: `npm run demo:build`
**Demo Clean**: `npm run demo:clean`
### Demo
The demo is bundled with [`Parcel.js`](https://parceljs.org/) and served up at [http://localhost:1234/](http://localhost:1234/).
### Dist
On build, `lib` populates `dist` with commonjs, es, umd versions of the component.
## 🕹️ Usage
```
import React from "react";
import AnimatedCursor from "react-animated-cursor"
export default function App() {
return (
);
}
```
### Example Usage - with options
```
import React from "react";
import AnimatedCursor from "react-animated-cursor"
export default function App() {
return (
);
}
```
### Example Usage - with simple options and custom config for one class
```
import React from "react";
import AnimatedCursor from "react-animated-cursor"
export default function App() {
return (
);
}
```
### Client Components, Next.js, SSR
In previous versions of the component, integration with Next's SSR environment required using a `Dynamic Import`.
However, as of version `2.10.1`, **you _should_ be good to go with a simple `import`.**
Relevant updates:
- Included module directive `'use client'` to indicate a client side component.
- Updated `useEventListener` hook with `window` checks.
- Wrapped the `document` use in a check.
However, if you do run into any issues, you could try including with Dynamic Import.
**Next's Dynamic Import**
```
'use client'; // indicates Client Component
// Import with next's dynamic import
import dynamic from 'next/dynamic';
const AnimatedCursor = dynamic(() => import('react-animated-cursor'), {
ssr: false,
});
```
## 🧬 Options
| Option | Type | Description | Default |
| ---- | ---- | -------- | -------|
| `clickables` | array | Collection of selectors cursor that trigger cursor interaction or object with single target and possibly the rest of the options listed below | `['a', 'input[type="text"]', 'input[type="email"]', 'input[type="number"]', 'input[type="submit"]', 'input[type="image"]', 'label[for]', 'select', 'textarea', 'button', '.link']` |
| `color` | string | rgb value | `220, 90, 90` |
| `innerScale` | number | amount dot scales on click or link hover | `0.7` |
| `innerSize` | number | Size (px) of inner cursor dot | `8` |
| `innerStyle` | object | provides custom styles / css to inner cursor | `null` |
| `outerAlpha` | number | amount of alpha transparency for outer cursor dot | `0.4` |
| `outerScale` | number | amount outer dot scales on click or link hover | `5` |
| `outerSize` | number | Size (px) of outer cursor outline | `8` |
| `outerStyle` | object | provides custom styles / css to outer cursor | `null` |
| `showSystemCursor` | boolean | Show system/brower cursor | `false` |
| `trailingSpeed` | number | Outer dot's trailing speed | `8` |
## 🎨 Cursor Types
You can use the `innerStyle` and `outerStyle` props to provide custom styles and create a variery of custom cursor types. Additionally, you can pass custom styles and css vars to create unique cursors or update style based on events.
### Dynamic Styles
Use CSS variables with `innerStyle` and `outerStyle` props to create dynamic styles that you can easily update.
For example, perhaps you have a light and dark mode experience and what your cursor to also adapt it's colors.
**CSS Vars**
```
html {
--cursor-color: #333
}
html.dark-mode {
--cursor-color: #fff
}
```
**Pass CSS Var as Style Props**
```
```
### Donut Cursor
A donut style cursor basically resembles a donut. You can easily create on by applying using the `outerStyle` props to apply an outer border
```
```
[Donut Demo→](https://stephenscaff.github.io/react-animated-cursor?cursor=donut)
### Blend Mode Cursor
You can use CSS mix-blend-mode with the style props to create an intersting cursor effect on hover that inverts the content's color. Works best with white / black cursors.
```
```
[Blend Mode Demo→](https://stephenscaff.github.io/react-animated-cursor?cursor=blendmode)
## 📓 Notes
### Mobile / Touch
`helpers/isDevice.js` uses UA sniffing to determine if on a common device so we can avoid rendering cursors. Yes... I know, there are other and probably better ways to handle this. Whatevers.
## 📅 To Dos
- ~~Either remove on mobile, or provide touch events.~~
- ~~Separate click and hover scalings to provide a different scaling when clicking on links/clickables~~
- ~~Fix transform blur in Safari, which may mean migrating from `scale` to a `width` &`height` update~~ 4/4/23
- ~~Make clickables (cursor targets / selectors) a prop~~
- ~~Add PropType checks~~
- ~~Open cursor styles as props~~
- ~~Add ability to maintain system cursor for the squeamish~~ 4/4/23
- ~~Migrate to TS~~
- ~~Allow for different behavior based on the element hovered~~
- Options to control cursor transition speed and bezier
- Solution for impacting state during route changes
- Add some proper tests
Have fun ya'll.
================================================
FILE: rollup.config.mjs
================================================
import commonjs from '@rollup/plugin-commonjs'
import external from 'rollup-plugin-peer-deps-external'
import resolve from '@rollup/plugin-node-resolve'
import typescript from '@rollup/plugin-typescript'
import dts from 'rollup-plugin-dts'
import pkg from './package.json' assert { type: 'json' }
const umdGlobals = {
react: 'React',
'react-animated-cursor': 'AnimatedCursor',
'react/jsx-runtime': 'jsxRuntime'
}
const config = [
{
external: ['react', 'react-dom'],
input: 'lib/index.ts',
output: [
{
file: pkg.main,
format: 'cjs',
banner: "'use client';"
},
{
file: pkg.module,
format: 'esm',
banner: "'use client';"
},
{
file: pkg.browser,
format: 'umd',
name: 'AnimatedCursor',
globals: umdGlobals,
banner: "'use client';"
}
],
plugins: [
external(),
resolve(),
commonjs(),
typescript({
exclude: 'node_modules'
})
]
},
{
external: ['react', 'react-dom'],
input: 'lib/index.ts',
output: [{ file: pkg.types, format: 'es' }],
plugins: [external(), resolve(), dts()]
}
]
export default config
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"jsx": "react-jsx",
"moduleResolution": "node",
"target": "es5",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"allowJs": true
},
"include": ["lib"],
"exclude": ["node_modules"]
}