├── src ├── test.ts ├── react-app-env.d.ts └── index.tsx ├── example ├── README.md ├── src │ ├── index.js │ ├── App.css │ └── App.js ├── public │ ├── manifest.json │ └── index.html └── package.json ├── .travis.yml ├── tsconfig.test.json ├── .gitignore ├── rollup.config.js ├── tsconfig.json ├── README.md └── package.json /src/test.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 9 4 | - 8 5 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | ReactDOM.render(, document.getElementById("root")); 6 | -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "use-typewriter", 3 | "name": "use-typewriter", 4 | "start_url": "./index.html", 5 | "display": "standalone", 6 | "theme_color": "#000000", 7 | "background_color": "#ffffff" 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | dist 10 | .rpt2_cache 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | Typewriter Hook 14 | 15 | 16 | 17 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-typewriter-example", 3 | "homepage": "https://gielcobben.github.io/use-typewriter", 4 | "version": "0.0.0", 5 | "license": "MIT", 6 | "private": true, 7 | "dependencies": { 8 | "react": "link:../node_modules/react", 9 | "react-dom": "^16.8.0", 10 | "react-scripts": "^2.1.3", 11 | "use-typewriter": "link:.." 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test --env=jsdom", 17 | "eject": "react-scripts eject" 18 | }, 19 | "browserslist": [ 20 | ">0.2%", 21 | "not dead", 22 | "not ie <= 11", 23 | "not op_mini all" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "rollup-plugin-typescript2"; 2 | import commonjs from "rollup-plugin-commonjs"; 3 | import external from "rollup-plugin-peer-deps-external"; 4 | import resolve from "rollup-plugin-node-resolve"; 5 | import url from "rollup-plugin-url"; 6 | 7 | import pkg from "./package.json"; 8 | 9 | export default { 10 | input: "src/index.tsx", 11 | output: [ 12 | { 13 | file: pkg.main, 14 | format: "cjs", 15 | exports: "named", 16 | sourcemap: true 17 | }, 18 | { 19 | file: pkg.module, 20 | format: "es", 21 | exports: "named", 22 | sourcemap: true 23 | } 24 | ], 25 | plugins: [ 26 | external(), 27 | url({ exclude: ["**/*.svg"] }), 28 | resolve(), 29 | typescript({ 30 | rollupCommonJSResolveHack: true, 31 | clean: true 32 | }), 33 | commonjs() 34 | ] 35 | }; 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build", 4 | "module": "esnext", 5 | "target": "es5", 6 | "lib": [ 7 | "es6", 8 | "dom", 9 | "es2016", 10 | "es2017" 11 | ], 12 | "sourceMap": true, 13 | "allowJs": false, 14 | "jsx": "preserve", 15 | "declaration": true, 16 | "moduleResolution": "node", 17 | "forceConsistentCasingInFileNames": true, 18 | "noImplicitReturns": true, 19 | "noImplicitThis": true, 20 | "noImplicitAny": true, 21 | "strictNullChecks": true, 22 | "suppressImplicitAnyIndexErrors": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "skipLibCheck": true, 26 | "esModuleInterop": true, 27 | "allowSyntheticDefaultImports": true, 28 | "strict": true, 29 | "resolveJsonModule": true, 30 | "isolatedModules": true, 31 | "noEmit": true 32 | }, 33 | "include": [ 34 | "src" 35 | ], 36 | "exclude": [ 37 | "node_modules", 38 | "build", 39 | "dist", 40 | "example", 41 | "rollup.config.js" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hook: Typewriter 2 | 3 | ## Installation 4 | 5 | Install using [Yarn](https://yarnpkg.com): 6 | 7 | ```sh 8 | yarn add use-typewriter 9 | ``` 10 | 11 | or NPM: 12 | 13 | ```sh 14 | npm install use-typewriter --save 15 | ``` 16 | 17 | ## Usage 18 | 19 | ### Basic 20 | 21 | ```tsx 22 | import React from "react"; 23 | import useTypewriter from "use-typewriter"; 24 | 25 | const Component = () => { 26 | const currentWord = useTypewriter({ 27 | words: ["Hello World"] 28 | }); 29 | 30 | return
{currentWord}
; 31 | }; 32 | ``` 33 | 34 | ## API 35 | 36 | ### Options 37 | 38 | | Name | Type | Default | Required | Description | 39 | | ---------- | -------- | ------- | -------- | ---------------------------------------------- | 40 | | words | string[] | [] | Yes | An array of words you want to be typed. | 41 | | min | number | 10 | No | Minimum amount in ms of delay between letters. | 42 | | max | number | 80 | No | Maximum amount in ms of delay between letters. | 43 | | wordDelay | number | 2000 | No | Delay in ms between words in the array. | 44 | | eraseDelay | number | 1000 | No | Delay in ms before earsing the word | 45 | 46 | ## License 47 | 48 | **use-typewriter** is MIT licensed. 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-typewriter", 3 | "version": "0.0.3", 4 | "description": "Typewriter Hook", 5 | "author": "gielcobben ", 6 | "license": "MIT", 7 | "repository": "git@github.com:gielcobben/use-typewriter.git", 8 | "main": "dist/index.js", 9 | "module": "dist/index.es.js", 10 | "jsnext:main": "dist/index.es.js", 11 | "engines": { 12 | "node": ">=8", 13 | "npm": ">=5" 14 | }, 15 | "scripts": { 16 | "test": "cross-env CI=1 react-scripts test --env=jsdom", 17 | "test:watch": "react-scripts test --env=jsdom", 18 | "build": "rollup -c", 19 | "start": "rollup -c -w", 20 | "prepare": "yarn run build", 21 | "predeploy": "cd example && yarn install && yarn run build", 22 | "deploy": "gh-pages -d example/build" 23 | }, 24 | "dependencies": {}, 25 | "peerDependencies": { 26 | "react": "^16.8.0" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.2.2", 30 | "@babel/runtime": "^7.3.1", 31 | "@types/jest": "^23.3.13", 32 | "@types/react": "^16.7.22", 33 | "cross-env": "^5.2.0", 34 | "gh-pages": "^2.0.1", 35 | "react": "^16.8.0", 36 | "react-scripts": "^2.1.3", 37 | "rollup": "^1.1.2", 38 | "rollup-plugin-babel": "^4.3.2", 39 | "rollup-plugin-commonjs": "^9.2.0", 40 | "rollup-plugin-node-resolve": "^4.0.0", 41 | "rollup-plugin-peer-deps-external": "^2.2.0", 42 | "rollup-plugin-typescript2": "^0.19.2", 43 | "rollup-plugin-url": "^2.1.0", 44 | "typescript": "^3.2.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export type typewriterProps = { 4 | words: string[]; 5 | min?: number; 6 | max?: number; 7 | wordDelay?: number; 8 | eraseDelay?: number; 9 | }; 10 | 11 | const delay = (ms: number) => { 12 | return new Promise(resolve => setTimeout(resolve, ms)); 13 | }; 14 | 15 | const nextFrame = () => { 16 | return new Promise(resolve => { 17 | requestAnimationFrame(resolve); 18 | }); 19 | }; 20 | 21 | const randomDelay = async ({ min, max }: { min: number; max: number }) => { 22 | const delay = Math.random() * (max - min) + min; 23 | const startTime = performance.now(); 24 | 25 | while (performance.now() - startTime < delay) { 26 | await nextFrame(); 27 | } 28 | }; 29 | 30 | export default ({ 31 | words = [], 32 | min = 10, 33 | max = 80, 34 | wordDelay = 2000, 35 | eraseDelay = 1000 36 | }: typewriterProps) => { 37 | const [currentWord, setCurrentWord] = React.useState(0); 38 | const [word, setWord] = React.useState(""); 39 | const string = words[currentWord]; 40 | 41 | React.useEffect(() => { 42 | const erase = async () => { 43 | for (let i = 0; i < string.length; i++) { 44 | await randomDelay({ min, max }); 45 | setWord((word: string) => word.slice(0, word.length - 1)); 46 | 47 | if (i === string.length - 1) { 48 | await delay(eraseDelay); 49 | setCurrentWord(currentWord => 50 | currentWord === words.length - 1 ? 0 : currentWord + 1 51 | ); 52 | } 53 | } 54 | }; 55 | 56 | (async () => { 57 | for (let i = 0; i < string.length; i++) { 58 | await randomDelay({ min, max }); 59 | setWord((word: string) => word + string.charAt(i)); 60 | 61 | if (i === string.length - 1) { 62 | await delay(wordDelay); 63 | erase(); 64 | } 65 | } 66 | })(); 67 | }, [currentWord]); 68 | 69 | return word; 70 | }; 71 | -------------------------------------------------------------------------------- /example/src/App.css: -------------------------------------------------------------------------------- 1 | ::selection { 2 | color: #FFF; 3 | background: rgba(0, 85, 255, 0.99); 4 | } 5 | 6 | ::-moz-selection { 7 | color: #FFF; 8 | background: rgba(0, 85, 255, 0.99); 9 | } 10 | 11 | html { 12 | box-sizing: border-box; 13 | } 14 | 15 | *, 16 | *::before, 17 | *::after { 18 | box-sizing: inherit; 19 | } 20 | 21 | 22 | html, 23 | body { 24 | width: 100%; 25 | height: 100%; 26 | } 27 | 28 | html { 29 | line-height: 1.15; 30 | -webkit-text-size-adjust: 100%; 31 | } 32 | 33 | body { 34 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 35 | font-size: 18px; 36 | background: #000; 37 | color: #fff; 38 | margin: 0; 39 | } 40 | 41 | body > div { 42 | width: 100%; 43 | height: 100%; 44 | } 45 | 46 | a { 47 | opacity: 0.5; 48 | transition: opacity 0.2s ease; 49 | } 50 | 51 | a:hover { 52 | opacity: 1; 53 | } 54 | 55 | ul { 56 | display: flex; 57 | align-items: center; 58 | list-style: none; 59 | margin: 0 auto; 60 | padding: 0; 61 | } 62 | 63 | li { 64 | margin: 0 20px; 65 | } 66 | 67 | .wrapper { 68 | display: flex; 69 | align-items: center; 70 | justify-content: space-between; 71 | flex-direction: column; 72 | min-height: 100%; 73 | padding: 200px 40px; 74 | } 75 | 76 | .typewriter { 77 | display: flex; 78 | align-items: center; 79 | font-weight: bold; 80 | font-size: 5vw; 81 | height: 5vw; 82 | } 83 | 84 | @keyframes blink { 85 | from, to { 86 | opacity: 1; 87 | } 88 | 50% { 89 | opacity: 0; 90 | } 91 | } 92 | 93 | .cursor { 94 | border-radius: 0.3vw; 95 | width: 0.3vw; 96 | height: 5vw; 97 | background: #05F; 98 | animation: blink 1s linear infinite forwards; 99 | } 100 | 101 | .controls { 102 | max-width: 400px; 103 | display: flex; 104 | flex-direction: column; 105 | margin: 100px auto; 106 | } 107 | 108 | .slider { 109 | margin: 20px; 110 | display: flex; 111 | align-items: center; 112 | justify-content: space-between; 113 | } 114 | 115 | label { 116 | width: 150px; 117 | } 118 | 119 | input[type="range"] { 120 | appearance: none; 121 | height: 2px; 122 | border-radius: 2px; 123 | outline: none; 124 | width: 250px; 125 | } 126 | 127 | input[type="range"]::-webkit-slider-thumb { 128 | appearance: none; 129 | width: 30px; 130 | height: 30px; 131 | border-radius: 20px; 132 | background: #fff; 133 | } -------------------------------------------------------------------------------- /example/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import useTypewriter from "use-typewriter"; 3 | import "./App.css"; 4 | 5 | const App = () => { 6 | const [min, setMin] = useState(10); 7 | const [max, setMax] = useState(80); 8 | const [wordDelay, setWordDelay] = useState(2000); 9 | const [eraseDelay, setEraseDelay] = useState(1000); 10 | 11 | const currentWord = useTypewriter({ 12 | words: ["Hello", "World", "This is", "a hook"], 13 | min, 14 | max, 15 | wordDelay, 16 | eraseDelay 17 | }); 18 | 19 | return ( 20 |
21 |
22 | {currentWord} 23 |
24 |
25 |
26 | 27 | setMin(event.target.value)} 37 | /> 38 |
39 |
40 | 41 | setMax(event.target.value)} 51 | /> 52 |
53 |
54 | 55 | setWordDelay(event.target.value)} 67 | /> 68 |
69 |
70 | 71 | setEraseDelay(event.target.value)} 83 | /> 84 |
85 |
86 | 132 |
133 | ); 134 | }; 135 | 136 | export default App; 137 | --------------------------------------------------------------------------------