Example of a code input
Enter or paste characters into the cells below, once it reaches the maximum characters it will de-focus.
HTML
Note the use of autocorrect
and spellcheck
to avoid red warning lines on the input.
<div class="code-container">
<input
id="code-input"
type="text"
name="code-input"
class="code-input"
maxlength="6"
title="Input code"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
pattern="[a-z][A-Z][0-9]"
autofocus
/>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
</div>
CSS
The CSS uses a monotype font with the use of ch
as a character measurement.
.code-container {
display: grid;
grid-template-columns: repeat(6, 2ch);
grid-template-rows: 1fr;
gap: 0px 1ch;
position: relative;
font-family: monospace;
font-size: 2.5rem;
height: 100px;
align-items: center;
}
.code-input {
position: absolute;
top: 0;
left: 0.5ch;
right: -2ch;
height: 100px;
font-family: inherit;
font-size: inherit;
border: none;
outline: none;
padding: 0;
letter-spacing: 2ch;
color: grey;
background: transparent;
width: 18ch;
user-select: none;
opacity: 0.5;
transition: opacity 0.2s ease-in-out;
}
.code-input:focus {
opacity: 1;
color: rebeccapurple;
}
.code-input::selection {
background-color: transparent;
}
.cell {
outline: 2px solid grey;
width: 2ch;
height: 3ch;
z-index: -1;
border-radius: 5px;
}
TypeScript
Blurring the input box when the 6th character has been entered avoids a weird overflow.
const handleInput = (event: InputEvent) => {
const codeInput = event.target as HTMLInputElement;
if (codeInput.value.length >= 6) {
codeInput.blur(); // Blur the input so cursor doesn't hang outside boxes
}
};
const codeInput = document.querySelector("#code-input");
codeInput?.addEventListener("input", (event: Event) => {
handleInput(event as InputEvent);
});
codeInput?.addEventListener("paste", () => {
setTimeout(handleInput, 0);
});
The full web component code
class CodeInput extends HTMLElement {
#shadow: ShadowRoot | null = null;
constructor() {
super();
this.#shadow = this.attachShadow({ mode: "open" });
}
handleInput(event: InputEvent) {
const codeInput = event.target as HTMLInputElement;
if (codeInput.value.length >= 6) {
codeInput.blur(); // Blur the input so cursor doesn't hang outside boxes
}
}
connectedCallback() {
if (this.#shadow === null) return;
this.#shadow.innerHTML = `
<style>
.cells {
display: grid;
grid-template-columns: repeat(6, 2ch);
grid-template-rows: 1fr;
gap: 0px 1ch;
position: relative;
font-family: monospace;
font-size: 2.5rem;
height: 100px;
align-items: center;
}
.code-input {
position: absolute;
top: 0;
left: .5ch;
right: -2ch;
height: 100px;
font-family: inherit;
font-size: inherit;
border: none;
outline: none;
padding: 0;
letter-spacing: 2ch;
color: grey;
background: transparent;
width: 18ch;
user-select: none;
opacity: .5;
transition: opacity .2s ease-in-out;
}
.code-input:focus {
opacity: 1;
color: white;
}
.code-input::selection {
background-color: transparent;
}
.cell {
outline: 2px solid grey;
width: 2ch;
height: 3ch;
z-index: -1;
border-radius: 5px;
}
</style>
<div class="cells">
<input
id="code-input"
type="text"
name="code-input"
class="code-input"
maxlength="6"
title="Input code"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
pattern="[a-z][A-Z][0-9]"
autofocus
/>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
</div>
`;
const codeInput = this.#shadow.querySelector("#code-input");
codeInput?.addEventListener("input", (event: Event) => {
this.handleInput(event as InputEvent);
});
codeInput?.addEventListener("paste", () => {
setTimeout(this.handleInput, 0);
});
}
}
customElements.define("code-input", CodeInput);