The exercise this is trying to recreate
Most of us learned to write the same way: a worksheet with a faded, dotted letter, and a pencil tracing directly over it until the hand remembers the shape on its own. It’s a strange exercise to try to explain in the abstract — copying is normally the opposite of learning — but tracing works precisely because the guide is right there, under the pencil, at the exact place the hand needs to be. You’re not recalling the shape from memory and hoping it’s close. You’re placing your own version directly on top of the correct one, and the gap between them is what teaches you.
That exercise doesn’t really exist for adults learning a new script — Arabic, in this case. Typing “practice” apps usually separate the example and the input box into two different places on the screen, which throws away the exact mechanism that made tracing work as a kid: the overlay. This component puts them back in the same place, literally the same pixels, using nothing but CSS grid and a native HTML attribute.
The trick: two elements sharing one grid cell
The entire mechanism is four lines of CSS:
.cc-container {
display: grid;
}
.original-text,
.input-area {
grid-area: 1 / 1;
}
display: grid on the container combined with grid-area: 1 / 1 on both children is the whole trick. CSS Grid doesn’t require every child to occupy a distinct cell — when two elements are explicitly placed in the same grid area, they stack directly on top of each other, in source order, like layers. This is a much lighter-weight way to get an overlay than the more common position: absolute approach, which requires setting up a positioning context on the parent and manually pinning each child with top/left. Grid stacking gets the same result with two words of CSS and no positioning context to manage.
The faded original text sits in the back layer:
.original-text {
color: #ccc;
outline: 1px solid rgb(0, 0, 0);
pointer-events: none;
user-select: none;
}
pointer-events: none and user-select: none are doing quiet but important work here — without them, clicking on the guide text would try to select or interact with it instead of passing the click through to the editable layer underneath. With them, the guide text becomes purely visual: it’s there to be seen and traced over, and structurally invisible to anything but the eye.
The input layer sits on top, transparent, so the guide text shows through it exactly where the learner is meant to write over it:
.input-area {
background: transparent;
color: black;
caret-color: #222;
cursor: text;
}
Because both elements share the same font size, line height, padding, and box-sizing, every character the learner types lands in the same visual position as the character behind it. That alignment is what makes it feel like tracing rather than “two boxes near each other.”
Why contenteditable instead of a <textarea>
The input layer isn’t a form control at all — it’s a plain <div contenteditable="true">. This is the second half of the “no JavaScript” claim, and it’s worth explaining why it’s the right tool here rather than an accidental simplification.
A <textarea> would work for capturing text, but it comes with its own box model, its own scrollbar behavior, and platform-specific default styling that has to be reset to make it disappear visually into the grid stack. contenteditable on a plain <div> sidesteps all of that — the element only exists as a rectangle of styleable, focusable, editable text, with none of a form control’s baked-in chrome to fight against. Since this exercise is purely about visual overlay and typed characters, and not about form submission or validation, contenteditable is a closer match to what’s actually needed rather than an equivalent capable of more than the job requires.
It’s also what makes the desktop/mobile parity work without extra code. contenteditable is a standard part of the HTML5 spec that every mobile browser already implements identically to desktop — tapping it brings up the on-screen keyboard the same way tapping any text input does, with no separate mobile handling required. A textarea would have gotten this too, but it would have brought unnecessary baggage along with it.
Focus feedback, done in pure CSS
The last interactive detail is what happens when the learner taps into the box — a visible outline confirms where the cursor is, purely through the :focus pseudo-class:
.input-area:focus {
outline: 0.06rem solid #000;
}
No JavaScript event listener, no focus/blur handlers, no state to track. The browser already knows when a contenteditable element has focus, and :focus is the same pseudo-class that’s applied to any focusable element — CSS handles the entire feedback loop on its own.
Why “no JavaScript” is the actual feature, not a constraint
It would be easy to read “zero JS” as a limitation accepted for simplicity’s sake. It’s closer to the opposite: every piece of behavior this component needs — text input, focus state, on-screen keyboard activation, cross-device consistency — is already built into the browser’s handling of contenteditable and CSS pseudo-classes. Reaching for JavaScript here would mean re-implementing behavior the platform already provides for free, with more code and more surface area for something to break across browsers. The constraint that produced the simplest version of this component wasn’t “avoid JavaScript” — it was “use exactly what the task requires,” and it turned out the task required none.