The Terminal window, from back in the days when the computers were operated only by the command line or in the present when developing even the smallest project, we all use it. If this does not ring a bell yet, here is a about terminals aka. command-line interfaces: definition from Wikipedia A ( ) processes commands to a computer program in the form of lines of text. The program which handles the interface is called a or . command-line interface CLI command-line interpreter command-line processor These terminal interfaces are used by a lot of websites like Haskell, Hyper, even Codesandbox has a terminal (console). Let’s make one with for our website. ReactJs What we will build We will build a terminal emulator that accepts a set of predefined commands that we give it. You can or . view the code on GitHub see it in action here To build this terminal, we are using (CRA) to generate a new React application through our computer terminal. You can even see a terminal window animation on the CRA website in the section “Get started in seconds“. create-react-app First things first First things first, let’s create our React project using the (CRA) CLI. We will create an application with the template, to make our code better and avoid possible mistakes like adding two strings when we don’t want to. create-react-app Typescript I will name the application so, on my computer console, I will run the following code: terminal npx create-react-app terminal --template typescript After that, I will open the newly created folder in my code editor of choice, . terminal WebStorm Let’s clean up the files a little bit. Let’s delete the files , and from the folder. From the file , let’s remove everything that is within the with the className and also delete line number 2 and 3, with the logo import. Like so: logo.svg App.css App.test.tsx src/ App.tsx div App import React from 'react'; function App() { return ( <div className="App"> We will fill this section later </div> ); } export default App; Creating the style and basic structure In the folder, let’s create another folder with the name , and inside that, create a file called one called . This will be our basic structure. src/ Terminal/ index.tsx terminal.css The terminal window is composed of two elements, the output, and the input. The output is where we will write the response, and the input is where we will write our commands. So, based on that, let’s create our terminal and add some style to it. In the , we will have the following code: index.tsx import './terminal.css'; export const Terminal = () => { return ( <div className="terminal"> <div className="terminal__line">A terminal line</div> <div className="terminal__prompt"> <div className="terminal__prompt__label">alexandru.tasica:</div> <div className="terminal__prompt__input"> <input type="text" /> </div> </div> </div> ); }; Our style will be the following: terminal.css .terminal { height: 500px; overflow-y: auto; background-color: #3C3C3C; color: #C4C4C4; padding: 35px 45px; font-size: 14px; line-height: 1.42; font-family: 'IBM Plex Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; text-shadow: 0 4px 4px rgba(0, 0, 0, 0.25); } .terminal__line { line-height: 2; white-space: pre-wrap; } .terminal__prompt { display: flex; align-items: center; } .terminal__prompt__label { flex: 0 0 auto; color: #F9EF00; } .terminal__prompt__input { flex: 1; margin-left: 1rem; display: flex; align-items: center; color: white; } .terminal__prompt__input input { flex: 1; width: 100%; background-color: transparent; color: white; border: 0; outline: none; font-size: 14px; line-height: 1.42; font-family: 'IBM Plex Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; } And also, let’s import that in our to view it in our browser: App.tsx import React from 'react'; import {Terminal} from "./Terminal"; function App() { return ( <div className="App"> <Terminal /> </div> ); } export default App; And the result is, 🥁 drums please: Good, so you’ve got where we are heading to. Basic state management Awesome! So we have the basic structure. Now we need to make users interact with it, keep our messages history and add new messages to the terminal. For that, we will write a custom hook called that will go hand-in-hand with our terminal. Let’s see the basic hook structure that we are going for: useTerminal import {useCallback, useEffect, useState} from 'react'; import {TerminalHistory, TerminalHistoryItem, TerminalPushToHistoryWithDelayProps} from "./types"; export const useTerminal = () => { const [terminalRef, setDomNode] = useState<HTMLDivElement>(); const setTerminalRef = useCallback((node: HTMLDivElement) => setDomNode(node), []); const [history, setHistory] = useState<TerminalHistory>([]); /** * Scroll to the bottom of the terminal when window is resized */ useEffect(() => { const windowResizeEvent = () => { terminalRef?.scrollTo({ top: terminalRef?.scrollHeight ?? 99999, behavior: 'smooth', }); }; window.addEventListener('resize', windowResizeEvent); return () => { window.removeEventListener('resize', windowResizeEvent); }; }, [terminalRef]); /** * Scroll to the bottom of the terminal on every new history item */ useEffect(() => { terminalRef?.scrollTo({ top: terminalRef?.scrollHeight ?? 99999, behavior: 'smooth', }); }, [history, terminalRef]); const pushToHistory = useCallback((item: TerminalHistoryItem) => { setHistory((old) => [...old, item]); }, []); /** * Write text to terminal * @param content The text to be printed in the terminal * @param delay The delay in ms before the text is printed * @param executeBefore The function to be executed before the text is printed * @param executeAfter The function to be executed after the text is printed */ const pushToHistoryWithDelay = useCallback( ({ delay = 0, content, }: TerminalPushToHistoryWithDelayProps) => new Promise((resolve) => { setTimeout(() => { pushToHistory(content); return resolve(content); }, delay); }), [pushToHistory] ); /** * Reset the terminal window */ const resetTerminal = useCallback(() => { setHistory([]); }, []); return { history, pushToHistory, pushToHistoryWithDelay, terminalRef, setTerminalRef, resetTerminal, }; }; And let’s also create a separate file that contains the type of the props and all the things we want to strongly type, like . Let’s create a file called with the definitions we have right now. Terminal History types.ts import {ReactNode} from "react"; export type TerminalHistoryItem = ReactNode | string; export type TerminalHistory = TerminalHistoryItem[]; export type TerminalPushToHistoryWithDelayProps = { content: TerminalHistoryItem; delay?: number; }; Now, after we have the types defined, let’s see a quick description of what does every function in our hook: The first state, will keep our reference to the terminal container. We will reference it later in our file. The first function is a helper function that sets the reference for that . terminalRef terminal/index.tsx div The first two will scroll done the terminal every time there is a new entry in our terminal history or every time the window is resized useEffects will push a new message to our terminal history pushToHistory will push the new message to our terminal history with a specific delay. We return a promise instead of resolving it as a function, so we can chain multiple pushes as an animation. pushToHistoryWithDelay Last, we have the which resets the terminal history. Works more like a function from the classic terminal. resetTerminal clear Awesome! Now that we have a way to store and push the messages, let’s go integrate this hook with structure. In the In the file, we will have some props that will be connected with the terminal history and also will handle the focus on our terminal input prompt. Terminal/index.tsx import './terminal.css'; import {ForwardedRef, forwardRef, useCallback, useEffect, useRef} from "react"; import {TerminalHistory, TerminalHistoryItem} from "./types"; export type TerminalProps = { history: TerminalHistory; promptLabel?: TerminalHistoryItem; }; export const Terminal = forwardRef( (props: TerminalProps, ref: ForwardedRef<HTMLDivElement>) => { const { history = [], promptLabel = '>', } = props; /** * Focus on the input whenever we render the terminal or click in the terminal */ const inputRef = useRef<HTMLInputElement>(); useEffect(() => { inputRef.current?.focus(); }); const focusInput = useCallback(() => { inputRef.current?.focus(); }, []); return ( <div className="terminal" ref={ref} onClick={focusInput}> {history.map((line, index) => ( <div className="terminal__line" key={`terminal-line-${index}-${line}`}> {line} </div> ))} <div className="terminal__prompt"> <div className="terminal__prompt__label">{promptLabel}</div> <div className="terminal__prompt__input"> <input type="text" // @ts-ignore ref={inputRef} /> </div> </div> </div> ); }); And not, let’s connect the dots and push something to the terminal. Because we accept any as a line, we can push any HTML we want. ReactNode Our will be the wrapper, this is where all the action will happen. Here we will set the messages we want to show. Here is a working example: App.tsx import React, {useEffect} from 'react'; import {Terminal} from "./Terminal"; import {useTerminal} from "./Terminal/hooks"; function App() { const { history, pushToHistory, setTerminalRef, resetTerminal, } = useTerminal(); useEffect(() => { resetTerminal(); pushToHistory(<> <div><strong>Welcome!</strong> to the terminal.</div> <div style={{fontSize: 20}}>It contains <span style={{color: 'yellow'}}><strong>HTML</strong></span>. Awesome, right?</div> </> ); }, []); return ( <div className="App"> <Terminal history={history} ref={setTerminalRef} promptLabel={<>Write something awesome:</>} /> </div> ); } export default App; And, the result so far is: On mount, we just push whatever we want, like that cool big yellow HTML text. Implementing commands Awesome! You made it almost to the end. Now let’s add the ability for the user to interact with our terminal, in writing. In our will add the option to update the input, and also the commands that every word will execute. These are the most important things in a terminal, right? Terminal/index.tsx Let’s handle the user input in the file, and listen there when the user presses the key. After that, we execute the function that he wants. Terminal/index.tsx Enter The the file will transform into: Terminal/index.tsx import './terminal.css'; import {ForwardedRef, forwardRef, useCallback, useEffect, useRef, useState} from "react"; import {TerminalProps} from "./types"; export const Terminal = forwardRef( (props: TerminalProps, ref: ForwardedRef<HTMLDivElement>) => { const { history = [], promptLabel = '>', commands = {}, } = props; const inputRef = useRef<HTMLInputElement>(); const [input, setInputValue] = useState<string>(''); /** * Focus on the input whenever we render the terminal or click in the terminal */ useEffect(() => { inputRef.current?.focus(); }); const focusInput = useCallback(() => { inputRef.current?.focus(); }, []); /** * When user types something, we update the input value */ const handleInputChange = useCallback( (e: React.ChangeEvent<HTMLInputElement>) => { setInputValue(e.target.value); }, [] ); /** * When user presses enter, we execute the command */ const handleInputKeyDown = useCallback( (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === 'Enter') { const commandToExecute = commands?.[input.toLowerCase()]; if (commandToExecute) { commandToExecute?.(); } setInputValue(''); } }, [commands, input] ); return ( <div className="terminal" ref={ref} onClick={focusInput}> {history.map((line, index) => ( <div className="terminal__line" key={`terminal-line-${index}-${line}`}> {line} </div> ))} <div className="terminal__prompt"> <div className="terminal__prompt__label">{promptLabel}</div> <div className="terminal__prompt__input"> <input type="text" value={input} onKeyDown={handleInputKeyDown} onChange={handleInputChange} // @ts-ignore ref={inputRef} /> </div> </div> </div> ); }); As you can see, we have moved the type defined here, into the separate file we created called . Let’s explore that file and see what new stuff we defined. types.ts import {ReactNode} from "react"; export type TerminalHistoryItem = ReactNode | string; export type TerminalHistory = TerminalHistoryItem[]; export type TerminalPushToHistoryWithDelayProps = { content: TerminalHistoryItem; delay?: number; }; export type TerminalCommands = { [command: string]: () => void; }; export type TerminalProps = { history: TerminalHistory; promptLabel?: TerminalHistoryItem; commands: TerminalCommands; }; Good, now that we can execute commands from the terminal, let’s define some in the . We will create only two simple ones, but you let your imagination run wild! App.tsx import React, {useEffect, useMemo} from 'react'; import {Terminal} from "./Terminal"; import {useTerminal} from "./Terminal/hooks"; function App() { const { history, pushToHistory, setTerminalRef, resetTerminal, } = useTerminal(); useEffect(() => { resetTerminal(); pushToHistory(<> <div><strong>Welcome!</strong> to the terminal.</div> <div style={{fontSize: 20}}>It contains <span style={{color: 'yellow'}}><strong>HTML</strong></span>. Awesome, right?</div> <br/> <div>You can write: start or alert , to execute some commands.</div> </> ); }, []); const commands = useMemo(() => ({ 'start': async () => { await pushToHistory(<> <div> <strong>Starting</strong> the server... <span style={{color: 'green'}}>Done</span> </div> </>); }, 'alert': async () => { alert('Hello!'); await pushToHistory(<> <div> <strong>Alert</strong> <span style={{color: 'orange', marginLeft: 10}}> <strong>Shown in the browser</strong> </span> </div> </>); }, }), [pushToHistory]); return ( <div className="App"> <Terminal history={history} ref={setTerminalRef} promptLabel={<>Write something awesome:</>} commands={commands} /> </div> ); } export default App; The constant defines the command that should be typed by the user. So if the user types and hits enter, he will see the text “ the server… Done“. commands start Starting What we’ve built Thanks for reaching the end, you are awesome! Here is what we’ve built together, hopefully, it will be helpful for your website or a client’s website. What’s next? What’s next you ask? The terminal right now is pretty simple. You don’t see the previously typed commands. If the user types an incorrect command, he will see nothing. And, of course, animations! See you in part two for these new features. Until then, you can or . view the code on GitHub see it in action here