├── .babelrc ├── .gitignore ├── README.md ├── images ├── multiText.gif ├── singleTextDisplay1.gif └── textDisplay.gif ├── package-lock.json ├── package.json ├── src ├── app.css ├── index.js └── utils.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-class-properties", 5 | "@babel/plugin-transform-runtime" 6 | ], 7 | "ignore": ["./dist"] 8 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-typewriter-effect 2 | 3 | A package that gives your text a typing effect 4 | 5 | ### Use case scenario 6 | 7 | ## Description 8 | 9 | This package lets you create a typewriting effect for text elements 10 | 11 | ![Typewriter description](./images/singleTextDisplay1.gif) 12 | 13 | - Typewriter Effect animates when component is in view. By default it uses the document reference to check if component is in view or not. But you can pass the ref object which is scrollable to the scollArea props. 14 | 15 | **For example** 16 | 17 | ``` 18 | const myRef = document.querySelector('.scrollable-div') 19 | 20 | 21 | ``` 22 | 23 | **Otherwise** 24 | if scrollArea is not defined, document reference object is used. 25 | 26 | ## Set up 27 | 28 | To use package, Start by installing package 29 | 30 | - npm i react-typewriter-effect 31 | 32 | **on your react project file** 33 | 34 | ### For a single text display 35 | 36 | ``` 37 | import TypeWriterEffect from 'react-typewriter-effect'; 38 | 39 | 47 | 48 | ``` 49 | 50 | #### Output 51 | 52 | ![single text display](./images/textDisplay.gif) 53 | 54 | ### For a multiiple text display 55 | 56 | Set the muultiText props to an array of strings which are displayed sequentially 57 | 58 | ``` 59 | import TypeWriterEffect from 'react-typewriter-effect'; 60 | 61 | 80 | ``` 81 | 82 | #### Output 83 | 84 | ![Rect bar](./images/multiText.gif) 85 | 86 | ## Properties and description 87 | 88 | - text (must be a string): Required in sigle text display mode. The text in string. 89 | 90 | - multiText (array of string): Required in multi text mode 91 | 92 | - multiTextDelay (must be a number): delay before each text is erased in multi text display in milli seconds. 93 | 94 | - multiTextLoop creates a continous loop of the typewriter text (true/false) 95 | 96 | - typeSpeed (must be a number): Speed of typing in milli seconds, 97 | 98 | - startDelay (must be a number): Delay before animation starts in milli seconds 99 | 100 | - hideCursorAfterText (a boolean): it removes cursor after typing. 101 | 102 | - cursorColor (must be a string): color of the cursor 103 | 104 | - textStyle (must be an object): custom css styles can be applied to the text in this object. 105 | 106 | - scrollArea (must be a dom element): the scrollable area. By default it is document 107 | 108 | -------------------------------------------------------------------------------- /images/multiText.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevoese/react-typewriter-effect/f8baf3310eaa5079cea1c228b48ff66203d0fa1a/images/multiText.gif -------------------------------------------------------------------------------- /images/singleTextDisplay1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevoese/react-typewriter-effect/f8baf3310eaa5079cea1c228b48ff66203d0fa1a/images/singleTextDisplay1.gif -------------------------------------------------------------------------------- /images/textDisplay.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevoese/react-typewriter-effect/f8baf3310eaa5079cea1c228b48ff66203d0fa1a/images/textDisplay.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-typewriter-effect", 3 | "version": "1.1.0", 4 | "description": "creating a typewriter effect using react", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "webpack --watch", 9 | "build": "rimraf dist && webpack" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/kevoese/react-typewriter-effect.git" 14 | }, 15 | "author": "Kelvin Esegbona", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/kevoese/react-typewriter-effect/issues" 19 | }, 20 | "homepage": "https://github.com/kevoese/react-typewriter-effect#readme", 21 | "dependencies": { 22 | "react": "^16.10.2", 23 | "style-loader": "^1.0.0", 24 | "webpack": "^4.41.2" 25 | }, 26 | "files": [ 27 | "dist/*" 28 | ], 29 | "peerDependencies": { 30 | "react": "^16.10.2" 31 | }, 32 | "devDependencies": { 33 | "@babel/core": "^7.6.4", 34 | "@babel/plugin-proposal-class-properties": "^7.5.5", 35 | "@babel/plugin-transform-runtime": "^7.6.2", 36 | "@babel/preset-env": "^7.6.3", 37 | "@babel/preset-react": "^7.6.3", 38 | "@babel/runtime": "^7.6.3", 39 | "babel-loader": "^8.0.6", 40 | "css-loader": "^3.2.0", 41 | "mini-css-extract-plugin": "^0.8.0", 42 | "webpack-cli": "^3.3.9" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | .react-typewriter-text { 2 | padding: 0; 3 | margin: 0; 4 | text-align: left; 5 | } 6 | 7 | .react-typewriter-pointer { 8 | background-color: black; 9 | display: inline; 10 | padding: 0 1px; 11 | } 12 | 13 | .add-cursor-animate { 14 | animation: blink 1s step-end infinite; 15 | } 16 | 17 | @keyframes blink { 18 | 0%, 100% { 19 | opacity: 1; 20 | } 21 | 50% { 22 | opacity: 0; 23 | } 24 | } 25 | 26 | .hide-typing-cursor { 27 | padding: 0; 28 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import { delay, propTypeValidation, contentInView } from './utils'; 3 | import './app.css'; 4 | 5 | class TypeWriterEffect extends Component { 6 | state = { 7 | text: '', 8 | blink: false, 9 | hideCursor: true, 10 | animate: false, 11 | typeSpeedDelay: null, 12 | multiTextDelay: null, 13 | eraseSpeedDelay: null, 14 | startDelay: null, 15 | scrollAreaIsSet: null, 16 | multiTextLoop: false, 17 | }; 18 | 19 | myRef = createRef(); 20 | 21 | multiTextDisplay = async (arr) => { 22 | for (let e = 0; e < arr.length; e++) { 23 | await this.runAnimation(arr[e], arr.length - e - 1); 24 | } 25 | if (this.props.multiTextLoop) { 26 | await this.eraseText(arr[arr.length - 1]); 27 | this.multiTextDisplay(arr); 28 | } 29 | }; 30 | 31 | runAnimation = async (str, erase) => { 32 | const textArr = typeof str == 'string' && str.trim().split(''); 33 | if (textArr) { 34 | this.setState({ 35 | blink: false, 36 | }); 37 | let text = ''; 38 | const typeSpeedDelay = new delay(this.props.typeSpeed || 120); 39 | const multiTextDelay = 40 | this.props.multiText && new delay(this.props.multiTextDelay || 2000); 41 | this.setState({ 42 | typeSpeedDelay, 43 | multiTextDelay, 44 | }); 45 | for (let char = 0; char < textArr.length; char++) { 46 | await typeSpeedDelay.getPromise(); 47 | text += textArr[char]; 48 | this.setState({ 49 | text, 50 | }); 51 | } 52 | this.setState({ 53 | blink: true, 54 | }); 55 | this.props.multiText && (await multiTextDelay.getPromise()); 56 | erase > 0 && (await this.eraseText(text)); 57 | } 58 | }; 59 | 60 | eraseText = async (str) => { 61 | const textArr = typeof str == 'string' && str.trim().split(''); 62 | this.setState({ 63 | blink: false, 64 | }); 65 | let text = str.trim(); 66 | const eraseSpeedDelay = new delay(50); 67 | this.setState({ 68 | eraseSpeedDelay, 69 | }); 70 | for (let char = 0; char < textArr.length; char++) { 71 | await eraseSpeedDelay.getPromise(); 72 | text = text.slice(0, -1); 73 | this.setState({ 74 | text, 75 | }); 76 | } 77 | this.setState({ 78 | blink: true, 79 | }); 80 | }; 81 | 82 | animateOnScroll = async () => { 83 | try { 84 | if (!this.state.animate && contentInView(this.myRef.current)) { 85 | this.setState({ 86 | animate: true, 87 | }); 88 | const startDelay = 89 | this.props.startDelay && new delay(this.props.startDelay); 90 | this.setState({ 91 | hideCursor: false, 92 | startDelay, 93 | }); 94 | this.props.startDelay && (await startDelay.getPromise()); 95 | this.props.multiText 96 | ? await this.multiTextDisplay(this.props.multiText) 97 | : await this.runAnimation(this.props.text); 98 | 99 | this.props.hideCursorAfterText && 100 | this.setState({ 101 | hideCursor: true, 102 | }); 103 | } 104 | } catch (error) {} 105 | }; 106 | 107 | componentDidMount() { 108 | this.animateOnScroll(); 109 | this.setState({ scrollAreaIsSet: false }); 110 | } 111 | 112 | componentDidUpdate() { 113 | if (!this.state.scrollAreaIsSet) { 114 | this.setState({ scrollAreaIsSet: true }); 115 | this.props.scrollArea && typeof this.props.scrollArea == 'object' 116 | ? this.props.scrollArea.addEventListener('scroll', this.animateOnScroll) 117 | : document.addEventListener('scroll', this.animateOnScroll); 118 | } 119 | } 120 | 121 | componentWillUnmount() { 122 | // unsubscribe from timeouts and events 123 | this.props.scrollArea && typeof this.props.scrollArea == 'object' 124 | ? this.props.scrollArea.removeEventListener( 125 | 'scroll', 126 | this.animateOnScroll 127 | ) 128 | : document.removeEventListener('scroll', this.animateOnScroll); 129 | this.state.startDelay && this.state.startDelay.cancel(); 130 | this.state.eraseSpeedDelay && this.state.eraseSpeedDelay.cancel(); 131 | this.state.typeSpeedDelay && this.state.typeSpeedDelay.cancel(); 132 | this.state.multiTextDelay && this.state.multiTextDelay.cancel(); 133 | } 134 | 135 | render() { 136 | return ( 137 |
138 |

142 | {this.state.text} 143 |
149 |

150 |
151 | ); 152 | } 153 | } 154 | 155 | TypeWriterEffect.propTypes = propTypeValidation; 156 | 157 | export default TypeWriterEffect; 158 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export class delay { 2 | constructor(time) { 3 | this.time = time; 4 | this.timeout = null; 5 | this.close = null; 6 | } 7 | 8 | getPromise() { 9 | return new Promise((resolve, reject) => { 10 | this.close = reject; 11 | this.timeout = setTimeout(() => { 12 | resolve(); 13 | }, this.time); 14 | }); 15 | } 16 | cancel() { 17 | this.timeout && clearTimeout(this.timeout); 18 | this.close && this.close('unmounted'); 19 | return { isCanceled: true }; 20 | } 21 | } 22 | 23 | export class makeCancelable { 24 | constructor(promise) { 25 | this.promise = promise; 26 | this.close = null; 27 | } 28 | 29 | getPromise() { 30 | return new Promise((resolve, reject) => { 31 | this.close = reject('rejected promise'); 32 | this.promise() 33 | .then((val) => resolve(val)) 34 | .catch((err) => reject(err)); 35 | }); 36 | } 37 | cancel() { 38 | this.close && this.close(); 39 | // throw new Error('error'); 40 | return { isCanceled: true }; 41 | } 42 | } 43 | 44 | export const propTypeValidation = { 45 | multiTextDelay: (props, propName) => { 46 | if (props[propName] && typeof props[propName] != 'number') 47 | return new Error( 48 | `Invalid ${propName} supplied to react-type-writer-component component.` 49 | ); 50 | if (!props['multiText'] && props[propName]) 51 | return new Error( 52 | `Invalid!. multiText props must be provided to use ${propName} .` 53 | ); 54 | }, 55 | typeSpeed: (props, propName) => { 56 | if (props[propName] && typeof props[propName] != 'number') 57 | return new Error( 58 | `Invalid ${propName} supplied to react-typeWriter component.` 59 | ); 60 | }, 61 | startDelay: (props, propName) => { 62 | if (props[propName] && typeof props[propName] != 'number') 63 | return new Error( 64 | `Invalid ${propName} supplied to react-typeWriter component.` 65 | ); 66 | }, 67 | text: (props, propName) => { 68 | if (!props['multiText'] && typeof props[propName] != 'string') 69 | return new Error( 70 | `Invalid ${propName} supplied to react-typeWriter component!` 71 | ); 72 | }, 73 | cursorColor: (props, propName) => { 74 | if (props[propName] && typeof props[propName] != 'string') 75 | return new Error( 76 | `Invalid ${propName} supplied to react-typeWriter component!` 77 | ); 78 | }, 79 | textStyle: (props, propName) => { 80 | if (props[propName] && typeof props[propName] != 'object') 81 | return new Error( 82 | `Invalid ${propName} supplied to react-typeWriter component!` 83 | ); 84 | }, 85 | multiText: (props, propName) => { 86 | if (props[propName] && typeof props[propName] == 'object') { 87 | for (let i = 0; i < props[propName].length; i++) { 88 | if (typeof props[propName][i] != 'string') 89 | return new Error( 90 | `Invalid element: ${props[propName][i]} for ${propName} supplied to react-typeWriter component!` 91 | ); 92 | } 93 | } else if (props[propName] && typeof props[propName] !== 'object') 94 | return new Error( 95 | `Invalid ${propName} supplied to react-typeWriter component!` 96 | ); 97 | }, 98 | scrollArea: (props, propName) => { 99 | if (props[propName] && typeof props[propName] != 'object') 100 | return new Error(`Invalid ${propName} supplied to typewriter component!`); 101 | }, 102 | multiTextLoop: (props, propName) => { 103 | if (props[propName] && typeof props[propName] != 'boolean') 104 | return new Error( 105 | `Invalid ${propName} supplied to react-typeWriter component.` 106 | ); 107 | }, 108 | }; 109 | 110 | export const contentInView = (element) => { 111 | const scroll = window.scrollY || window.pageYOffset; 112 | const elementPositionProps = element.getBoundingClientRect(); 113 | const elementTopPosition = elementPositionProps.top + scroll; 114 | 115 | const viewport = { 116 | top: scroll, 117 | bottom: scroll + window.innerHeight, 118 | }; 119 | 120 | const elementPosition = { 121 | top: elementTopPosition, 122 | bottom: elementTopPosition + elementPositionProps.height, 123 | }; 124 | return ( 125 | (elementPosition.bottom >= viewport.top && 126 | elementPosition.bottom <= viewport.bottom) || 127 | (elementPosition.top <= viewport.bottom && 128 | elementPosition.top >= viewport.top) 129 | ); 130 | }; 131 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 3 | 4 | module.exports = { 5 | mode: "production", 6 | entry: "./src/index.js", 7 | output: { 8 | path: path.resolve(__dirname, "dist"), 9 | filename: "index.js", 10 | libraryTarget: "commonjs2" // THIS IS THE MOST IMPORTANT LINE! :mindblow: I wasted more than 2 days until realize this was the line most important in all this guide. 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.(js|jsx)$/, 16 | exclude: /node_modules/, 17 | use: ["babel-loader"] 18 | }, 19 | { 20 | test: /\.css$/, 21 | include: path.resolve(__dirname, "src"), 22 | exclude: /node_modules/, 23 | use: ["style-loader", "css-loader"] 24 | }, 25 | { 26 | test: /\.(jpe?g|png|gif|mp3|svg|ico)$/, 27 | use: { 28 | loader: "file-loader", 29 | options: {} 30 | } 31 | } 32 | ] 33 | }, 34 | plugins: [ 35 | new MiniCssExtractPlugin({ 36 | filename: "style.css" 37 | }) 38 | ], 39 | externals: { 40 | react: { 41 | commonjs: "react", 42 | commonjs2: "react", 43 | amd: "React", 44 | root: "React" 45 | }, 46 | } 47 | }; 48 | --------------------------------------------------------------------------------