A textarea with line numbers — like VS Code, but as a drop-in component. Available as a Web Component, React, and Vue component.
Each framework has its own subpath import — only import what you use.
npm install numbered-textareaimport { register } from "numbered-textarea/web";
register();<numbered-textarea placeholder="Write some code..."></numbered-textarea>Registers the custom element. Call once before using the tag. Idempotent.
The custom element class, also available for direct use:
import { NumberedTextarea } from "numbered-textarea/web";
customElements.define("my-editor", NumberedTextarea);| Attribute | Type | Description |
|---|---|---|
value |
string |
The text content |
placeholder |
string |
Placeholder text when empty |
readonly |
boolean |
Prevents editing |
disabled |
boolean |
Disables the textarea |
wrap |
string |
Wrapping behavior (off, soft, hard). Default: off |
| Property | Type | Description |
|---|---|---|
value |
string |
Get/set the text content |
lineCount |
number |
Read-only count of lines (1-based) |
| Event | Detail | Description |
|---|---|---|
nt-input |
{ value: string, lineCount: number } |
Fires on every input change |
import { NumberedTextarea } from "numbered-textarea/react";
function Editor() {
return (
<NumberedTextarea
defaultValue="const x = 1;"
onInput={(value, lineCount) => console.log(lineCount)}
style={{ width: "100%", height: "300px" }}
/>
);
}import { useRef, useState } from "react";
import { NumberedTextarea, type NumberedTextareaRef } from "numbered-textarea/react";
function Editor() {
const [lineCount, setLineCount] = useState(0);
const editorRef = useRef<NumberedTextareaRef>(null);
return (
<div>
<NumberedTextarea
ref={editorRef}
defaultValue={"function hello() {\n return 'world';\n}"}
placeholder="Write some code..."
onInput={(_value, count) => setLineCount(count)}
style={{ width: "100%", height: "300px" }}
/>
<p>Lines: {lineCount}</p>
<button onClick={() => editorRef.current?.focus()}>Focus editor</button>
</div>
);
}<NumberedTextarea
className="dark-theme"
defaultValue={'const greeting = "Hello!";\nconsole.log(greeting);'}
style={{ width: "100%", height: "200px" }}
/>.dark-theme {
--nt-bg: #1e1e1e;
--nt-color: #d4d4d4;
--nt-gutter-bg: #252526;
--nt-gutter-color: #858585;
--nt-gutter-border: 1px solid #333;
--nt-border: 1px solid #333;
--nt-font-family: "Fira Code", monospace;
}<NumberedTextarea
readOnly
value={"// This content is read-only\nconst x = 42;"}
style={{ width: "100%", height: "120px" }}
/>| Prop | Type | Description |
|---|---|---|
value |
string |
Controlled value |
defaultValue |
string |
Initial value (uncontrolled) |
placeholder |
string |
Placeholder text |
readOnly |
boolean |
Read-only mode |
disabled |
boolean |
Disabled mode |
wrap |
string |
Wrap behavior (off, soft, hard) |
onInput |
(value: string, lineCount: number) => void |
Called on every keystroke |
onChange |
(value: string, lineCount: number) => void |
Alias for onInput |
className |
string |
CSS class on host element |
style |
CSSProperties |
Inline styles on host element |
| Ref property | Type | Description |
|---|---|---|
element |
NumberedTextarea |
The underlying custom element |
lineCount |
number |
Current line count |
focus() |
() => void |
Focus the textarea |
<script setup>
import { NumberedTextarea } from "numbered-textarea/vue";
</script>
<template>
<NumberedTextarea
default-value="const x = 1;"
placeholder="Write some code..."
style="width: 100%; height: 300px"
@input="(value, lineCount) => console.log(lineCount)"
/>
</template><script setup lang="ts">
import { ref } from "vue";
import { NumberedTextarea } from "numbered-textarea/vue";
const code = ref("function hello() {\n return 'world';\n}");
const lineCount = ref(0);
const editorRef = ref<{ focus: () => void } | null>(null);
</script>
<template>
<NumberedTextarea
ref="editorRef"
v-model="code"
style="width: 100%; height: 300px"
@input="(_v: string, count: number) => (lineCount = count)"
/>
<p>Lines: {{ lineCount }}</p>
<button @click="editorRef?.focus()">Focus editor</button>
</template><NumberedTextarea
class="dark-theme"
default-value='const greeting = "Hello!";'
style="width: 100%; height: 200px"
/>.dark-theme {
--nt-bg: #1e1e1e;
--nt-color: #d4d4d4;
--nt-gutter-bg: #252526;
--nt-gutter-color: #858585;
--nt-gutter-border: 1px solid #333;
--nt-border: 1px solid #333;
--nt-font-family: "Fira Code", monospace;
}<NumberedTextarea
readonly
model-value="// Read-only content\nconst x = 42;"
style="width: 100%; height: 120px"
/>| Prop | Type | Description |
|---|---|---|
modelValue |
string |
Controlled value (use with v-model) |
defaultValue |
string |
Initial value (uncontrolled) |
placeholder |
string |
Placeholder text |
readonly |
boolean |
Read-only mode |
disabled |
boolean |
Disabled mode |
wrap |
string |
Wrap behavior (off, soft, hard) |
| Event | Payload | Description |
|---|---|---|
update:modelValue |
string |
For v-model binding |
input |
(value: string, lineCount: number) |
Called on every keystroke |
| Property | Type | Description |
|---|---|---|
element |
Ref<NTElement> |
The underlying custom element |
lineCount |
() => number |
Get current line count |
focus() |
() => void |
Focus the textarea |
All three components (Web Component, React, Vue) support the same styling options. The component uses Shadow DOM with CSS custom properties and ::part() selectors for full customization.
Set these on the element or any ancestor:
| Property | Default | Description |
|---|---|---|
--nt-font-family |
monospace |
Font family |
--nt-font-size |
14px |
Font size |
--nt-line-height |
1.5 |
Line height |
--nt-border |
1px solid #ccc |
Outer border |
--nt-border-radius |
4px |
Border radius |
--nt-bg |
#fff |
Textarea background |
--nt-color |
#333 |
Text color |
--nt-padding |
8px |
Textarea padding |
--nt-placeholder-color |
#aaa |
Placeholder color |
--nt-gutter-bg |
#f5f5f5 |
Gutter background |
--nt-gutter-color |
#999 |
Gutter text color |
--nt-gutter-border |
1px solid #ddd |
Gutter right border |
--nt-gutter-padding |
8px 12px 8px 8px |
Gutter padding |
--nt-gutter-min-width |
40px |
Gutter minimum width |
Dark theme example:
.dark-theme {
--nt-bg: #1e1e1e;
--nt-color: #d4d4d4;
--nt-gutter-bg: #252526;
--nt-gutter-color: #858585;
--nt-gutter-border: 1px solid #333;
--nt-border: 1px solid #333;
--nt-font-family: "Fira Code", monospace;
}Target Shadow DOM parts directly for full CSS control:
| Part | Element |
|---|---|
wrapper |
Outer flex container |
gutter |
Line numbers column |
textarea |
The <textarea> |
line-number |
Each line number <span> |
numbered-textarea::part(gutter) {
background: #e8f0fe;
color: #1a73e8;
font-weight: bold;
}Set width and height on the element (or via style prop in React):
numbered-textarea {
width: 100%;
height: 400px;
}See examples/ for live demos:
web-component.html— vanilla JS usage with themesreact.html— React usage with controlled/uncontrolled modesvue.html— Vue usage with v-model and events
Run locally:
npx vp dev
# then open http://localhost:5173pnpm install
pnpm test # run tests (44 tests: 17 web + 14 react + 13 vue)
pnpm run build # build the library
pnpm run check # lint + type checkMIT
