DEV Community

Cover image for Create a passcode component from scratch in React
Keyur Paralkar
Keyur Paralkar

Posted on

Create a passcode component from scratch in React

Introduction

Have you ever wondered how the one-time password screens on an app or webpage work? These are the screens that have multiple input elements that accept only one character. All these elements behave as if they are one element and have a behavior similar to that of the normal input element. We can call this a passcode component because a one-time password is nothing but a passcode.

In this blog post, we are going to talk about how to create this passcode component. We are going to understand how this component works and the various use cases involved with it. Later in the post, we start with the implementation of the component. 

So without further ado, let us get started.

What is a Passcode component?

A Passcode component is a group of input boxes. Each character in this component has its own input element and collectively we call this the passcode component. A passcode component looks like the below:

live-demo-example

It is used by most of the mobile and web applications these days. It is a common UI practice that is being followed during any validation flow. But it is trickier to implement since it's not a simple input element but is a group of input elements. There are multiple use case scenarios that need to be managed like handling key events, pasting experience, making sure the focus is changing as expected, etc.

Now let us start with understanding the use cases involved in the passcode component. So without further ado let us get started.

Cases involved in a Passcode component

Following are the basic scenarios that we are going to cover while building the Passcode component:

  • All the input boxes should accept only one character
  • Once a character is pressed the focus should shift to the next input element
  • While backspacing the focus should shift from right to left input element

We are also going to cover one advanced scenario: Pasting experience. It involves the following sub-cases:

  • Check for user permissions
  • Focus should go to the last input element when the pasted value length is equal to the number of input elements.
  • The focus should shift to the input element that contains the last character of the pasted value when pasted value's length is less than the total number of elements.
  • The focus should still be on the last input element when the pasted value length is greater than the total number of input elements
  • If a user tries to paste the value from an input element that is in between the start and end then, the pasted value is partially filled till the end of the input elements.

Setting up the project

  • We will be using the create-react-app's typescript template to initialize our project. Run the below command to create a project. Make sure to specify the name of the project in the placeholder:

    npx create-react-app <name-of-the-project> --template typescript
    
  • Once the project is initialized, we add a couple of folders and files in it.

    ├── src
    │   ├── App.css
    │   ├── App.test.tsx
    │   ├── App.tsx
    │   ├── components
    │   │   └── Passcode.tsx
    │   ├── index.css
    │   ├── index.tsx
    │   ├── logo.svg
    │   ├── react-app-env.d.ts
    │   ├── reportWebVitals.ts
    │   ├── setupTests.ts
    │   └── utils
    │       └── index.ts
    
  • We create 2 new folders in it namely: components utils. We also create the files mentioned in all these folders. We will later take a look at what each of these files means.

  • Once these folders are created make sure to start your project by running the following command:

    yarn start
    

Building the Passcode component

To build a passcode component first we need to understand how it works. Refer to the below diagram to understand it's working.

arch-diagram

Consider the above diagram. Each box shown above references each input element in the passcode component. If you enter a value inside the first input then two operations happen:

  • The value state variable is updated and,
  • The focused state gets updated.

We make use of various event handlers such as onChange, onKeyDown, onKeyUp, etc. to manage the focus between multiple input elements and the change in the value state variable. We will take a closer look at how these event handlers are orchestrated in the next section.

At this point, both the focused index and the value props are updated. This triggers a re-render and thus renders the passcode component with the next input element to be focused.

So this is how our component is going to work in laymen's terms.

Now that we have covered the basics, we can move on to addressing each use case mentioned above

Use case implementation

We will start by creating the scaffoldings for our project. Follow along for the same:

  • First, we will create a passcode component. This component acts as a container component. It will hold the data and it will render each input component.

    const Passcode = () => {
    const [arrayValue, setArrayValue] = useState<(string | number)[]>(['', '', '','']);
    const [currentFocusedIndex, setCurrentFocusedIndex] = useState(0);
    
    return (
    <>
        <div> currentFocusedIndex: {currentFocusedIndex}</div>
    
        {arrayValue.map((value: string | number, index: number)=> (
        <input
            key={`index-${index}`}
            type="text"
            value={String(value)}
        />
        ))}
        </>
    );
    }
    
  • The passcode component created has two state variables: arrayValue and currentFocusedIndex. arrayValue contains the actual value of the passcode component. If any of the underlying input elements change their value then this state variable is also updated. We also have currentFocusedIndex that contains the current index of the input element that needs to be focused. This gets updated whenever the user clicks on the input element or types in the input element.

Case 1: All the input boxes should accept only one character

Now let us start with our first use case. To implement this case we just need to add maxLength its value being set to 1. This allows us to accept only one character.

const Passcode = () => {
    const [arrayValue, setArrayValue] = useState<(string | number)[]>(['', '', '','']);
    const [currentFocusedIndex, setCurrentFocusedIndex] = useState(0);

    return (
    <>
        <div> currentFocusedIndex: {currentFocusedIndex}</div>

        {arrayValue.map((value: string | number, index: number)=> (
        <input
            key={`index-${index}`}
            maxLength={1}
            type="text"
            value={String(value)}
        />
        ))}
        </>
    );
    }
Enter fullscreen mode Exit fullscreen mode

This is an optional step, but if you want to control what type of keyboard to be displayed when the users fill this component and only want to allow numerals to be entered then simply add the inputMode attribute and set it to numeric. This will make sure that a numeric virtual keyboard is displayed when the user types via mobile screen:

    <input  
        key={`index-${index}`}
        inputMode="numeric"
        pattern="\d{1}"
        maxLength={1}
        type="text"
        value={String(value)}
      />

Enter fullscreen mode Exit fullscreen mode

Case 2: Once a character is typed the focus should shift to the next input box

By far this is going to be the most crucial use case to solve because this use case defines the basic interaction of this component. To solve it we need to consider a couple of things:

  • To control the focus of each input element we need to have an actual control of the input element. To achieve this, we need to pass the ref attribute to each input element.
  • We are going to segregate the task of focus and updating arrayValue the attribute in different event handlers:
    • onChange - will handle the update part of arrayValue state variable
    • onKeyup - will be responsible to update the focused index state variable.
    • onKeyDown - will be responsible to prevent typing any other keys except for the numerics.
    • onFocus - will make sure that we update the current element's focus when clicked.

Let us start by adding ref attribute to each input element. To do this, we should create an array as a reference. This array will store the reference of each input element.

  • To do this we declare an array as a reference value as follows:

    const inputRefs = useRef<Array<HTMLInputElement> | []>([]);
    
  • Next, we make sure that we add a reference of each element into the array by doing the following:

        <input
            key={`index-${index}`}
            ref={(el) => el && (inputRefs.current[index] = el)} // here
            inputMode="numeric"
            maxLength={1}
            type="text"
            value={String(value)}
        />
    

We use the ref callback mechanism to get the current element. You can read more about ref callback function here. Next, we assign this current element at the ith index of inputRefs array.

In this way, we store the reference of each input element in an array of ref. Now let us take a closer look at how we are going to use these references.

A passcode component only accepts a single numeric character in each of its input elements. We just need to restrict our passcode component so as to not accept any non-numeric values. In some cases, it can also accept alphanumeric code value but that's currently out of the scope of this blogpost. On the overall component, we are trying to restrict users by only typing numeric values in each input element. What we can do is simply prevent the default behavior of the key event on specific keystrokes. Here is what we will do:

const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
  const keyCode = e.key;
        if (!(keyCode >= 0 && keyCode <= 9)) {
          e.preventDefault();
        }
      };
Enter fullscreen mode Exit fullscreen mode

The above code ensures that the default behavior, which is preventing keystrokes for all non-numeric values, is implemented. We determine whether the key pressed is numeric or not using e.key. The Keyboard events key attribute returns the value of the key pressed by the user. Since we want to allow this behavior, we do not execute e.preventDefault() for non-numeric values. Therefore, this method helps us restrict the typing to only numeric values.

Next, we understand how to update the state variable arrayValue. We update it when onChange the event occurs. Here is how you can update this state:

const onChange = (e: BaseSyntheticEvent, index: number) => {
    setArrayValue((preValue: (string | number)[]) => {
      const newArray = [...preValue];

      if (parseInt(e.target.value)) {
        newArray[index] = parseInt(e.target.value);
      } else {
        newArray[index] = e.target.value;
      }

      return newArray;
    });
  };
Enter fullscreen mode Exit fullscreen mode

Here we make sure that the value provided by the onChange event is numeric or not. If yes, we set it in the clone variable newArray.
Next, we have a look at the onKeyUp event handler. In this event handler, we update the state variable currentFocusedIndex. Below is the code that helps you update this state variable:

const onKeyUp = (e: KeyboardEvent<HTMLInputElement>, index: number) => {
    if (parseInt(e.key) && index < arrayValue.length - 1) {
      setCurrentFocusedIndex(index + 1);
      if (inputRefs && inputRefs.current && index === currentFocusedIndex) {
        inputRefs.current[index + 1].focus();
      }
    }
  };
Enter fullscreen mode Exit fullscreen mode

Here the first update that we do is to set the currentFocusedIndex value to index+1. Next, we set the next input element into the focused state with the help of the focus function. In this way, the next element is focused as well as the code is now ready to handle the same set of events again for the next input element.

Finally, we have the onFocus event handler. The purpose of this handler is to update the currentFocusedIndex index of the input element that is being manually focused by a click event.

const onFocus = (e: BaseSyntheticEvent, index: number) => {
        setCurrentFocusedIndex(index);
      };
Enter fullscreen mode Exit fullscreen mode

We stitch all these handlers together and this is how our passcode component will look like:

const Passcode = () => {
  const [arrayValue, setArrayValue] = useState<(string | number)[]>([
    "",
    "",
    "",
    ""
  ]);
  const [currentFocusedIndex, setCurrentFocusedIndex] = useState(0);
  const inputRefs = useRef<Array<HTMLInputElement> | []>([]);

  const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
  const keyCode = e.key;
        if (!(keyCode >= 0 && keyCode <= 9)) {
          e.preventDefault();
        }
      };

  const onChange = (e: BaseSyntheticEvent, index: number) => {
    setArrayValue((preValue: (string | number)[]) => {
      const newArray = [...preValue];

      if (parseInt(e.target.value)) {
        newArray[index] = parseInt(e.target.value);
      } else {
        newArray[index] = e.target.value;
      }

      return newArray;
    });
  };

  const onKeyUp = (e: KeyboardEvent<HTMLInputElement>, index: number) => {
    if (parseInt(e.key) && index <= arrayValue.length - 2) {
      setCurrentFocusedIndex(index + 1);
      if (inputRefs && inputRefs.current && index === currentFocusedIndex) {
        inputRefs.current[index + 1].focus();
      }
    }
  };

  const onFocus = (e: BaseSyntheticEvent, index: number) => {
    setCurrentFocusedIndex(index);
    e.target.focus();
  };
  return (
    <>
      <div> currentFocusedIndex: {currentFocusedIndex}</div>
      <div>{arrayValue}</div>
      {arrayValue.map((value: string | number, index: number) => (
        <input
          key={`index-${index}`}
          ref={(el) => el && (inputRefs.current[index] = el)}
          inputMode="numeric"
          pattern="\d{1}"
          maxLength={1}
          type="text"
          value={String(value)}
          onChange={(e) => onChange(e, index)}
          onKeyUp={(e) => onKeyUp(e, index)}
          onKeyDown={(e) => onKeyDown(e)}
          onFocus={(e) => onFocus(e, index)}
        />
      ))}
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Case 3: While backspacing the focus should shift from right to left

This is a pretty common use case where the user wants to clear each value that is entered with the press of the backspace key. To achieve this we need to make sure that we update the currentFocusedIndex in onKeyUp event. We also need to allow the pressing of the Backspace key, to do that we add a condition inside the onKeyDown event.

Update the onKeyUp and onKeyDown event handler as follows:

const onKeyUp = (e: KeyboardEvent<HTMLInputElement>) => {
        if (e.key === "Backspace") {
          if (index === 0) {
            setCurrentFocusedIndex(0);
          } else {
            setCurrentFocusedIndex(index - 1);
            if (
              inputRefs &&
              inputRefs.current &&
              index === currentForcusedIndex
            ) {
              inputRefs.current[index - 1].focus();
            }
          }
        } else {
          if (
            (parseInt(e.key)) &&
            index <= array.length - 2
          ) {
            setCurrentFocusedIndex(index + 1);
            if (
              inputRefs &&
              inputRefs.current &&
              index === currentForcusedIndex
            ) {
              inputRefs.current[index + 1].focus();
            }
          }
        }
      };

const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
    const keyCode = e.key;
    if (
      !(keyCode >= 0 && keyCode <= 9) &&
      keyCode !== "Backspace" 
    ) {
      e.preventDefault();
    }
  };
Enter fullscreen mode Exit fullscreen mode

Here we added a block that handles the currentFocusedIndex and the focus of the input element when the Backspace key is pressed. First, if the current index is zero we also update the currentFocusedIndex to it else we set it to index-1 i.e. the focus is set to the previous element. We also make sure that we focus on the previous element by calling the focus function of the previous element. Inside the onKeyDown event handler, we added a condition to not execute preventDefault the function when keyCode is Backspace. This makes sure that the backspace key can be pressed and performs its default behavior.

Case 4: Pasting experience

This use case has multiple sub-use cases.

a. Check for user permissions

b. Focus should go to the last input box when the pasted value length is equal to the number of input boxes

c. The focus should shift to the input box that contains the last character of the pasted value when pasted value's length is less than the total number of input elements

d. The focus should be on the last input box when the pasted value length is greater than the total number of boxes

e. If a user tries to paste the value from an input box that is in between the start and end then, the pasted value is partially filled till the end of the input boxes
Enter fullscreen mode Exit fullscreen mode

pasting-exp-cases

Now let us get started with the implementation of the above sub-use cases. The Pasting Experiences scenario is carried out entirely inside the useEffect hook. We add an event listener to the paste event.

Here is what our code will look like:

useEffect(() => {
 document.addEventListener("paste", async () => {
  // Handle all sub-usecases here
 });
}, [])
Enter fullscreen mode Exit fullscreen mode

To allow the pasting of values we need to allow the pressing of CRTL + V key combinations so that users can paste the value via the keyboard. We need to add this condition inside onKeyDown event handler to allow this key combination:

const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
    const keyCode = parseInt(e.key);
    if (
      !(keyCode >= 0 && keyCode <= 9) &&
      e.key !== "Backspace" &&
      !(e.metaKey && e.key === "v")
    ) {
      e.preventDefault();
    }
  };
Enter fullscreen mode Exit fullscreen mode

Case a: Check user permission

To check for permission of pasting we make use of the Navigator interface's permission read-only property. You can read more about this property here. We make use of the query function to get the current permission. In our scenario, we are required to get the clipboard-read permission. We use the below code to do the same:

useEffect(() => {
    document.addEventListener("paste", async () => {
      // Handle all sub-usecases here

      const pastePermission = await navigator.permissions.query({
        name: "clipboard-read" as PermissionName
      });

      if (pastePermission.state === "denied") {
        throw new Error("Not allowed to read clipboard");
      }
    });
  }, []);
Enter fullscreen mode Exit fullscreen mode

With this code, we make sure that we ask for the user's permission before pasting the data. The permission dialog box will look like the below:

permission-dialog-box

Next, we manage the focus of the passcode component while pasting the numeric value. For cases: b, c, d, and e we are going to manage them in one go since all of them are related to focus management. Before we start with pasting experience and focus management we should make sure that all the values from the clipboard are numbers. But before that, we should read the content from the clipboard:

const clipboardContent = await navigator.clipboard.readText();
Enter fullscreen mode Exit fullscreen mode

Next, we convert all the content from the clipboard to a number:

try {
        let newArray: Array<number | string> = clipboardContent.split("");
        newArray = newArray.map((num) => Number(num));
      } catch (err) {
        console.error(err);
      }
Enter fullscreen mode Exit fullscreen mode

To create a pasting experience, we update arrayValue the contents of the clipboard. We update the arrayValue but based on the currentFocusedIndex:

  • If currentFocusedIndex > 0 i.e. if pasting gets started from any input element except the first and the last input element, then we need to fill the arrayValue from the focused input element to the last input element.

    • We first calculate the number of places/input elements that are available from the currently focused index to the last input element. We call this variable as remainingPlaces.
    • Now we know that we need to fill these many places of the arrayValue, we slice the pasting array from 0 to the remaining places.
    • Now we create the new array which is a merge of: arrayValue sliced from 0 to currentFocusedIndex and then the partially filled array i.e. the sliced-pasted array.
    • The code for this looks like below:
    const lastIndex = arrayValue.length - 1;
        if (currentFocusedIndex > 0) {
          const remainingPlaces = lastIndex - currentFocusedIndex;
          const partialArray = newArray.slice(0, remainingPlaces + 1);
          setArrayValue([
            ...arrayValue.slice(0, currentFocusedIndex),
            ...partialArray
          ]);
        }
    
    • If currentFocusedIndex = 0, then we do set the arrayValue array like below:
    setArrayValue([
            ...newArray,
            ...arrayValue.slice(newArray.length - 1, lastIndex)
          ]);
    

Once we update the arrayValue field, now it is time that we update the currentFocusedIndex. We update the currentFocusedIndex and the input element based on the following condition:

  • If the pasting array's length is less than the length of the arrayValue and the current focused index is the first input element then we update the focus of that input element which contains the last element of the pasting array.
  • Else we update the focus of the last input element:
if (newArray.length < arrayValue.length && currentFocusedIndex === 0) {
          setCurrentFocusedIndex(newArray.length - 1);
          inputRefs.current[newArray.length - 1].focus();
        } else {
          setCurrentFocusedIndex(arrayValue.length - 1);
          inputRefs.current[arrayValue.length - 1].focus();
        }
Enter fullscreen mode Exit fullscreen mode

We stitch all these changes inside the useEffect hook like below:

useEffect(() => {
    document.addEventListener("paste", async () => {
      // Handle all sub-usecases here

      const pastePermission = await navigator.permissions.query({
        name: "clipboard-read" as PermissionName
      });

      if (pastePermission.state === "denied") {
        throw new Error("Not allowed to read clipboard");
      }

      const clipboardContent = await navigator.clipboard.readText();
      try {
        let newArray: Array<number | string> = clipboardContent.split("");
        newArray = newArray.map((num) => Number(num));

        const lastIndex = arrayValue.length - 1;
        if (currentFocusedIndex > 0) {
          const remainingPlaces = lastIndex - currentFocusedIndex;
          const partialArray = newArray.slice(0, remainingPlaces + 1);
          setArrayValue([
            ...arrayValue.slice(0, currentFocusedIndex),
            ...partialArray
          ]);
        } else {
          setArrayValue([
            ...newArray,
            ...arrayValue.slice(newArray.length - 1, lastIndex)
          ]);
        }

        if (newArray.length < arrayValue.length && currentFocusedIndex === 0) {
          setCurrentFocusedIndex(newArray.length - 1);
          inputRefs.current[newArray.length - 1].focus();
        } else {
          setCurrentFocusedIndex(arrayValue.length - 1);
          inputRefs.current[arrayValue.length - 1].focus();
        }
      } catch (err) {
        console.error(err);
      }
    });

    return () => {
      document.removeEventListener("paste", () =>
        console.log("Removed paste listner")
      );
    };
  }, [arrayValue, currentFocusedIndex]);
Enter fullscreen mode Exit fullscreen mode

We have completed the implementation of our passcode component. Here is what the passcode component will look like:

const Passcode = () => {
  const [arrayValue, setArrayValue] = useState<(string | number)[]>([
    "",
    "",
    "",
    ""
  ]);
  const [currentFocusedIndex, setCurrentFocusedIndex] = useState(0);
  const inputRefs = useRef<Array<HTMLInputElement> | []>([]);

  const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
    const keyCode = parseInt(e.key);
    if (
      !(keyCode >= 0 && keyCode <= 9) &&
      e.key !== "Backspace" &&
      !(e.metaKey && e.key === "v")
    ) {
      e.preventDefault();
    }
  };

  const onChange = (e: BaseSyntheticEvent, index: number) => {
    setArrayValue((preValue: (string | number)[]) => {
      const newArray = [...preValue];

      if (parseInt(e.target.value)) {
        newArray[index] = parseInt(e.target.value);
      } else {
        newArray[index] = e.target.value;
      }

      return newArray;
    });
  };

  const onKeyUp = (e: KeyboardEvent<HTMLInputElement>, index: number) => {
    if (e.key === "Backspace") {
      if (index === 0) {
        setCurrentFocusedIndex(0);
      } else {
        setCurrentFocusedIndex(index - 1);
        if (inputRefs && inputRefs.current && index === currentFocusedIndex) {
          inputRefs.current[index - 1].focus();
        }
      }
    } else {
      if (parseInt(e.key) && index < arrayValue.length - 1) {
        setCurrentFocusedIndex(index + 1);
        if (inputRefs && inputRefs.current && index === currentFocusedIndex) {
          inputRefs.current[index + 1].focus();
        }
      }
    }
  };

  const onFocus = (e: BaseSyntheticEvent, index: number) => {
    setCurrentFocusedIndex(index);
    // e.target.focus();
  };

  useEffect(() => {
    document.addEventListener("paste", async () => {
      // Handle all sub-usecases here

      const pastePermission = await navigator.permissions.query({
        name: "clipboard-read" as PermissionName
      });

      if (pastePermission.state === "denied") {
        throw new Error("Not allowed to read clipboard");
      }

      const clipboardContent = await navigator.clipboard.readText();
      try {
        let newArray: Array<number | string> = clipboardContent.split("");
        newArray = newArray.map((num) => Number(num));

        const lastIndex = arrayValue.length - 1;
        if (currentFocusedIndex > 0) {
          const remainingPlaces = lastIndex - currentFocusedIndex;
          const partialArray = newArray.slice(0, remainingPlaces + 1);
          setArrayValue([
            ...arrayValue.slice(0, currentFocusedIndex),
            ...partialArray
          ]);
        } else {
          setArrayValue([
            ...newArray,
            ...arrayValue.slice(newArray.length - 1, lastIndex)
          ]);
        }

        if (newArray.length < arrayValue.length && currentFocusedIndex === 0) {
          setCurrentFocusedIndex(newArray.length - 1);
          inputRefs.current[newArray.length - 1].focus();
        } else {
          setCurrentFocusedIndex(arrayValue.length - 1);
          inputRefs.current[arrayValue.length - 1].focus();
        }
      } catch (err) {
        console.error(err);
      }
    });

    return () => {
      document.removeEventListener("paste", () =>
        console.log("Removed paste listner")
      );
    };
  }, [arrayValue, currentFocusedIndex]);

  return (
    <>
      <div> currentFocusedIndex: {currentFocusedIndex}</div>
      <div>{arrayValue}</div>
      {arrayValue.map((value: string | number, index: number) => (
        <input
          key={`index-${index}`}
          ref={(el) => el && (inputRefs.current[index] = el)}
          inputMode="numeric"
          maxLength={1}
          type="text"
          value={String(value)}
          onChange={(e) => onChange(e, index)}
          onKeyUp={(e) => onKeyUp(e, index)}
          onKeyDown={(e) => onKeyDown(e)}
          onFocus={(e) => onFocus(e, index)}
        />
      ))}
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Finally, you can import this component into your App.tsx file like below:

import "./styles.css";
import Passcode from "./components/Passcode";

export default function App() {
  return (
    <div className="App">
      <h1>Passcode component</h1>
      <Passcode />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here is the working example of our Passcode component:

Summary

To wrap up, we have learned how to build a passcode component and solved some base case scenarios required for building this type of component.

If you would like to use this component, you can check out the library I made: react-headless-passcode. It's a headless library that allows you to pass the array value to the usePasscode hook, which takes care of all the complex scenarios mentioned above and other scenarios as well. With headless, you can focus more on building your passcode UI and styling it however you like. The entire control is given to the developer. The job of react-headless-passcode is to provide you with all the logic you need to get the passcode component up and running.

Thank you for reading!

Follow me on Twitter, GitHub, and LinkedIn.

Top comments (18)

Collapse
 
soanvig profile image
Mateusz Koteja

Just use normal input with letter-spacing and create a mask over it in CSS in whatever way. It will be simpler, and handle everything as it should.

Collapse
 
neil585456525 profile image
Neil Chen • Edited

It might will be a little bit not convenient to add border, radius , other styles and put a dash in the middle of the sentence.

Collapse
 
keyurparalkar profile image
Keyur Paralkar

Thanks for reading this article @lukeshiru. Yes the solution you provided is easier but create a mask is a bit tricker.

You can try out this library I build: react-headless-passcode. It provides a hook: usePasscode which makes it easier to use and you won't require to reinvent the wheel

Collapse
 
boredcity profile image
boredcity • Edited


Added a hacky example of input squares using inline-svg background.

Collapse
 
panayiotisgeorgiou profile image
Panayiotis Georgiou

VERY THOROUGH ARTICLE 💪 GOOD JOB!

Collapse
 
leandro_nnz profile image
Leandro Nuñez

Great article!

Collapse
 
vdelitz profile image
vdelitz

Cool article!

Collapse
 
caroline profile image
Caroline

Love this article, keep up the good work!

Collapse
 
catsarebetter profile image
Hide Shidara

Always encrypt passcode data before you store!

Just trying to save any devs out there preemptively.

 
keyurparalkar profile image
Keyur Paralkar

This Looks quite helpful. Thanks for your interest and feedback on this article. Will surely try out in that way!

Collapse
 
boredcity profile image
boredcity

Definitely an interesting approach highlighting a lot of useful React concepts 👍

But I'm still on the "just use a single input" side of the debate and so, aparently, is Google: web.dev/sms-otp-form (it's also pretty useful to learn how to auto-suggest codes from SMS).

Collapse
 
boredcity profile image
boredcity

plus here's a recent article on how (and why) to implement it: dev.to/madsstoumann/using-a-single...

Collapse
 
keyurparalkar profile image
Keyur Paralkar

@boredcity thanks for the feedback. Yes, this looks a very good approach to solve the issue. Thanks for sharing this article!

Collapse
 
kainoah profile image
Kairus Noah Tecson

What did you use for your GIF animation?

Collapse
 
xinranma profile image
Xinran-Ma

This could be a rare case but you can also include Tab action for accessibility reason

Collapse
 
boredcity profile image
boredcity

don't you get it out of the box with inputs?

Collapse
 
17edwin profile image
Thai

But do you see it was slow down in the component when typing faster and it did not fill enough numbers as my expectation

Collapse
 
keyurparalkar profile image
Keyur Paralkar

@17edwin yes, I have observed this behaviour as, I am currently working on resolving the performance issue