In this tutorial we will create a TextField that follows Material Design principles for TextFields. It will look and feel like the MUI outlined variant TextField. We will use React-Bootstrap but this could be done in a similar way with vanilla Bootstrap.
Here is what we will create:

We’ll start with a React-Bootstrap InputGroup and Form.Control, add a few divs for the label and the notched outline, and end with a slick animation.
Full code for this tutorial is in the Resources section.
Customize InputGroup and Form.Control in React-Bootstrap TextField
The first things we need to create a Bootstrap TextField with a border gap are state values that track when the field is focused or has a value. I will collectively call these states ‘activated’ and create a state value for them.
We also need to track the actual value of the TextField so that when the TextField loses focus it only de-notches the border if there is no value in the TextField.
const [activated, setActivated] = React.useState(false);
const [value, setValue] = React.useState("");
Then in our InputGroup we add our onFocus, onBlur, and onChange handlers for detecting changes and handling appropriately.
<InputGroup
className="m-3"
onFocus={() => { setActivated(true) }}
onBlur={() => { setActivated(Boolean(value)) }}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { setValue(e.target.value) }}
>
Next we need to customize our Form.Control component. We need to remove the default box-shadow applied when the Form.Control has focus. I will also enhance the border:
.form-control {
border: 2px solid;
border-color: $primary;
}
.form-control:focus {
box-shadow: none;
}
In this post I dive deep into focus styling on the Bootstrap Accordion.
React-Bootstrap TextField with Animated Label
In order to create a “notched outline” style TextField in Bootstrap, we need to start with a label element inside of our InputGroup.

Then we need to move it to the top left part of the border when the TextField has focus or has a value. This can be accomplished by conditionally applying CSS classes based on the activated state of the TextField:
<label
className={`${activated ? "textfield-label-activated" : "textfield-label-unactivated"} textfield-label`}
>
First Name
</label>
Now we need to determine what values should be in the CSS classes. Here are the requirements:
- Absolutely position the label in the same way every
- Transition when the element is moved to the border
- Make sure the element is over the border
Here’s the code:
.textfield-label {
position: absolute;
transition: top 0.2s, left 0.2s;
line-height: 1rem;
}
.textfield-label-unactivated {
left: 1rem;
top: 0.5rem;
}
.textfield-label-activated {
left: 0.75rem;
top: -0.5rem;
// opacity: 1;
z-index: 9999;
padding: 0px 2px;
// background-color: white;
}
Notice that even when the TextField is not activated I have some positioning values. This is because I have the TextField permanently position: absolute
.
In the activated class, I add a z-index. That is probably intuitive. What might not make sense is the opacity and background-color that are commented out. If we add these here, the effect is close but not quite right when the TextField’s container has a background color. See what I mean below:

Instead, we will add another element that only exists to create the notched effect in the Input border.
React-Bootstrap TextField Notched Outline
We will create a second div immediately after the label element. The purpose of this div is to perfectly overlay the border of the TextField in the size and location we need for the label.
The JavaScript looks similar to the label code. Notice, however, that we have a dynamic width.
<div
className={`${activated ? "textfield-label-cover-activated" : "textfield-label-cover-unactivated"} textfield-label-cover`}
style={{ width: coverWidth }}
/>
This width is calculated from a ref on the label and a useEffect
function that detects the label’s width, plus a little extra width for padding:
const labelRef = React.useRef<HTMLLabelElement>(null);
const [coverWidth, setCoverWidth] = React.useState(0);
React.useEffect(() => {
setCoverWidth(labelRef.current ? (labelRef.current.getBoundingClientRect().width + 6) : 0);
}, [labelRef]);
Now we need to add the CSS. It is similar to the CSS for the label:
.textfield-label-cover {
position: absolute;
transition: top 0.2s, left 0.2s;
line-height: 1rem;
}
.textfield-label-cover-unactivated {
left: 1rem;
top: 0.5rem;
}
.textfield-label-cover-activated {
left: 0.75rem;
opacity: 1;
z-index: 9998;
background-color: white;
padding: 0px 2px;
height: 2px;
}
There are a few important differences. First, the z-index is a little lower. Second, it has a fixed height that matches the Form.Control border. Finally, the top
values split the line-height
so that the div perfectly overlays the border.
Resources
Here’s a good resources on aligning items in Bootstrap, and here’s a great tutorial showing how customizable tooltips are.
Here is the full code for this component. You can put it in its own file and then import it wherever its needed in your app. I’ll work on packaging it and putting it on NPM soon.
//TextField.scss
@import "bootstrap/scss/bootstrap";
.form-control {
border: 2px solid;
border-color: $primary;
}
.form-control:focus {
box-shadow: none;
}
.textfield-label {
position: absolute;
transition: top 0.2s, left 0.2s;
line-height: 1rem;
}
.textfield-label-unactivated {
left: 1rem;
top: 0.5rem;
}
.textfield-label-activated {
left: 0.75rem;
top: -0.5rem;
z-index: 9999;
padding: 0px 2px;
}
.textfield-label-cover {
position: absolute;
transition: top 0.2s, left 0.2s;
line-height: 1rem;
}
.textfield-label-cover-unactivated {
left: 1rem;
top: 0.5rem;
}
.textfield-label-cover-activated {
left: 0.75rem;
opacity: 1;
z-index: 9998;
background-color: white;
padding: 0px 2px;
height: 2px;
}
//TextField.tsx
import React from "react";
import InputGroup from "react-bootstrap/InputGroup";
import Form from 'react-bootstrap/Form';
import "./TextField.scss";
export default function TextField() {
const [activated, setActivated] = React.useState(false);
const [value, setValue] = React.useState("");
const labelRef = React.useRef<HTMLLabelElement>(null);
const [coverWidth, setCoverWidth] = React.useState(0);
React.useEffect(() => {
setCoverWidth(labelRef.current ? (labelRef.current.getBoundingClientRect().width + 6) : 0);
}, [labelRef]);
return (
<InputGroup
className="m-3"
onFocus={() => { setActivated(true) }}
onBlur={() => { setActivated(Boolean(value)) }}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { setValue(e.target.value) }}
>
<Form.Control className="rounded-end" />
<label
className={`${activated ? "textfield-label-activated" : "textfield-label-unactivated"} textfield-label`}
ref={labelRef}
>
First Name
</label>
<div
className={`${activated ? "textfield-label-cover-activated" : "textfield-label-cover-unactivated"} textfield-label-cover`}
style={{ width: coverWidth }}
/>
</InputGroup >
)
}