DEV Community

Cover image for Animate the caret in an input field
Phuoc Nguyen
Phuoc Nguyen

Posted on • Originally published at phuoc.ng

Animate the caret in an input field

When users interact with a text field, they expect to see a blinking caret indicating where they're typing. This tiny detail can make a big difference in the user experience. You've probably seen it before when entering digits in an OTP (One-Time Password) field.

In this post, I'll show you how to add this feature to your website or application using CSS and JavaScript. It's a simple way to make your website or application more interactive and engaging.

Markup

When you see an input field on a webpage, it usually comes with a default caret, which is that little blinking vertical line that shows you where you can type. But did you know that you can customize the color and shape of the caret using CSS styles?

For example, if you want to change the caret color to a shade of gray and make its shape an underscore, you can use the following code:

input[type="text"] {
    caret-color: rgb(15 23 42);
    caret-shape: underscore;
}
Enter fullscreen mode Exit fullscreen mode

While we have the ability to customize the caret, we are limited to certain possibilities and animations. Unfortunately, we cannot add any additional customization beyond that.

To work around this limitation, we can use an extra element to represent the caret instead of relying on the default caret. We can achieve this by using a markup consisting of three elements: an outer element containing the input field and the caret element.

<div class="container">
    <input class="container__input" type="text" id="input" />
    <div class="container__caret" id="caret"></div>
</div>
Enter fullscreen mode Exit fullscreen mode

Basic styles

First of all, we need to hide the default caret. To do this, we can set the caret-color (as mentioned earlier) to transparent.

.container__input {
    caret-color: transparent;
}
Enter fullscreen mode Exit fullscreen mode

To position the caret absolutely within its container, we need to set the position property of both the container and caret elements to relative and absolute, respectively. Then, we can use the CSS properties left and top to specify the caret's position relative to its container.

For example, in the code snippet below, we set the container's position to relative and use absolute positioning for the caret. We then use the left property to align the caret with the left edge of its container. To center the caret vertically within its container, we use the top and transform properties together.

.container {
    position: relative;
}

.container__caret {
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
}
Enter fullscreen mode Exit fullscreen mode

Initially, the custom caret is invisible. However, we need to make it visible when a user focuses on the input field. One way to achieve this is by using the opacity CSS property. In the code snippet below, we use the :focus pseudo-class and the ~ selector to change the opacity of the caret when the input field gets focus.

.container__caret {
    opacity: 0;
}
.container__input:focus ~ .container__caret {
    opacity: 1;
}
Enter fullscreen mode Exit fullscreen mode

Positioning the caret

Let's talk about caret positioning in custom input fields. With absolute positioning, we can control exactly where the caret appears on the input field. When users enter text, we need to move the caret to the end of the input.

To do this, we handle the input event, which triggers when the user changes the input's value. In this function, we measure the length of the current text and adjust the caret position accordingly.

Measuring text

To measure text, we use the measureWidth() utility function, which measures the width of a given text string. It takes two arguments: text, which is the string to be measured, and font, which specifies the font used to render the text.

const measureWidth = (text, font) => {
    const canvasEle = document.createElement('canvas');
    const context = canvasEle.getContext('2d');
    context.font = font;
    const metrics = context.measureText(text);
    return metrics.width;
};
Enter fullscreen mode Exit fullscreen mode

Here's how it works: we create a canvas element using the document.createElement() method, and then get the 2D rendering context for the canvas using its getContext() method. We set the font of this context to match that passed in as an argument, and then use its measureText() method to get metrics for our text string. The resulting object contains various properties related to the dimensions of our rendered text. Finally, we return just one of those properties - specifically, metrics.width - which gives us the width of our rendered text in pixels.

Now that we can measure text, we need to determine the font used by the input element. We can use the window.getComputedStyle() method to get all the CSS properties applied to this element, and then extract the font size and font family using the getPropertyValue() method. Finally, we concatenate these two values to get the complete font style of the input element.

const inputStyles = window.getComputedStyle(inputEle);
const font = `${inputStyles.getPropertyValue('font-size')} ${inputStyles.getPropertyValue('font-family')}`;
Enter fullscreen mode Exit fullscreen mode

Moving the cursor to a new position

To adjust the caret position based on the input value, we need to calculate its distance from the left side of the container. We do this by adding the width of our text (which we measured earlier) to the input's padding-left value. If this sum plus the width of our caret is less than the total width of our input field (as determined by getBoundingClientRect().width), we can translate the caret horizontally by that sum.

For example, let's say our text is 50 pixels wide and our input's padding-left value is 10 pixels. In this case, we'll position our caret 62 pixels from the left edge of its container (i.e., 50 + 10 + 2).

We can use the translate() function to achieve this. It's important to keep in mind that we need to translate the caret 50% to the top vertically to ensure that it remains centered.

Here's an example of what the input event handler could look like:

const inputEle = document.getElementById('input');
const caretEle = document.getElementById('caret');

const updateCaretPosition = () => {
    const text = inputEle.value;
    const inputStyles = window.getComputedStyle(inputEle);
    const font = `${inputStyles.getPropertyValue('font-size')} ${inputStyles.getPropertyValue('font-family')}`;
    const paddingLeft = parseInt(inputStyles.getPropertyValue('padding-left'), 10) + 2;
    const caretWidth = caretEle.getBoundingClientRect().width;

    const textWidth = measureWidth(text, font) + paddingLeft;
    const inputWidth = inputEle.getBoundingClientRect().width;

    if (textWidth + caretWidth < inputWidth) {
        caretEle.style.transform = `translate(${textWidth}px, -50%)`;
    }
};

inputEle.addEventListener('input', updateCaretPosition);
Enter fullscreen mode Exit fullscreen mode

Animating the transition

Adding an animation when updating the caret position is now a breeze. We can simply use the transition property.

For example, when the caret updates its position, we can smoothly transition it to its new spot over 200 milliseconds. Here are the styles you'll need for the caret element:

.container__caret {
    transition: transform 0.2s;
}
Enter fullscreen mode Exit fullscreen mode

Take a look at the results of the steps we've been following.

Note

It's important to note that we don't want to create a new canvas element and compute the styles of the input element for every keystroke. Doing so can slow down the app.

To avoid this, we can create and reuse the canvas element. Similarly, we can determine the input styles such as font size, font family, and padding left only once.

You may notice that when entering or removing characters using the Backspace key, the caret follows the corresponding position. However, if you try to move the cursor inside the input using the arrow keys, you'll see that the caret doesn't move to the desired position.

Don't worry, we'll address this issue in the next section.

Moving the cursor

In order to update the cursor position when users move it within the input field, we need to handle the selectionchange event. This event is triggered whenever there is any selection change within the document, such as when a user selects text on a webpage or moves the cursor within an input field.

Inside the event handler, we first check if the user is currently focused on the input by comparing the current active element (document.activeElement) against the input element. If the user is focused on the input, we update the caret position based on the cursor position, which can be retrieved from the selectionStart property.

const handleSelectionChange = () => {
    if (document.activeElement === inputEle) {
        updateCaretPosition(inputEle.selectionStart);
    }
};

document.addEventListener('selectionchange', handleSelectionChange);
Enter fullscreen mode Exit fullscreen mode

We have a function called updateCaretPosition, which takes the desired target position as its only parameter and updates the caret position. This function is similar to the input event handler that we implemented in the previous section.

const updateCaretPosition = (position) => {
    const text = inputEle.value.substr(0, position);
    const textWidth = measureWidth(text, font) + paddingLeft;
    const inputWidth = inputEle.getBoundingClientRect().width;
    if (textWidth + caretWidth < inputWidth) {
        caretEle.style.transform = `translate(${textWidth}px, -50%)`;
    }
};
Enter fullscreen mode Exit fullscreen mode

Note that the selectionchange event is also triggered when users change the input value, so we no longer need to handle the input event. However, we still need to allow users to remove characters from the input using the Backspace key. We can achieve this using the keydown event:

 const handleKeyDown = (e) => {
    if (e.key === 'Backspace') {
        updateCaretPosition(inputEle.value.length - 1);
    }
};

inputEle.addEventListener('keydown', handleKeyDown);
Enter fullscreen mode Exit fullscreen mode

In this example, we check if the user presses the Backspace key by checking the key property. If this happens, we move the caret position to the end of the input.

Now, it's time to check out the final demo. Try moving the cursor around using the left or right arrow keys, and you'll see that the caret moves to the desired position correctly.

Conclusion

By following these steps, you can create a simple and effective animation for the caret in a text field. This can significantly improve the user experience and give your website a professional and polished feel.

See also


If you want more helpful content like this, feel free to follow me:

Top comments (0)