Lightweight, extensible code editor component for the web using Prism.
There are multiple fully featured code editors for the web such as Monaco, Ace and CodeMirror. While these are awesome, they have a large footprint and are likely overkill for code examples, forms, playgrounds or anywhere you won't display large documents.
This library overlays syntax highlighted code over a <textarea>
. Libraries like CodeFlask, react-simple-code-editor, and many others have been doing this for years, but this library offers some distinct advantages:
npm i prism-code-editor
The library includes 4 different setups you can import. These will automatically import the necessary styles and scope them with a shadow root, add various extensions and import all language specific behavior. There are also web components wrapping these setups if that's preferred.
These setups are very cumbersome to customize and are therefore only recommended while getting started.
import { minimalEditor, basicEditor, fullEditor, readonlyEditor } from "prism-code-editor/setups"
// Importing Prism grammars
import "prism-code-editor/prism/languages/markup"
const editor = basicEditor(
"#editor",
{
language: "html",
theme: "github-dark",
},
() => console.log("ready"),
)
Note: You might want to add display: grid
to the container the editor is added to.
With little effort, you can fully customize which extensions are added and how they're loaded. This won't use a shadow root which makes the editor much easier to style and customize.
// index.ts
import "prism-code-editor/prism/languages/markup"
import "prism-code-editor/prism/languages/css-extras"
import "prism-code-editor/prism/languages/javascript"
import { createEditor } from "prism-code-editor"
import { matchBrackets } from "prism-code-editor/match-brackets"
import { indentGuides } from "prism-code-editor/guides"
// Importing styles
import "prism-code-editor/layout.css"
import "prism-code-editor/scrollbar.css"
import "prism-code-editor/themes/github-dark.css"
export const editor = createEditor(
"#editor",
{ language: "html" },
indentGuides(),
matchBrackets(),
)
import("./extensions")
To minimize your main JavaScript bundle, you can dynamically import all extensions (but some probably shouldn't be).
// extensions.ts
import "prism-code-editor/search.css"
import "prism-code-editor/copy-button.css"
import "prism-code-editor/languages/html"
import "prism-code-editor/languages/clike"
import "prism-code-editor/languages/css"
import { searchWidget, highlightSelectionMatches } from "prism-code-editor/search"
import { defaultCommands, editHistory } from "prism-code-editor/commands"
import { cursorPosition } from "prism-code-editor/cursor"
import { copyButton } from "prism-code-editor/copy-button"
import { matchTags } from "prism-code-editor/match-tags"
import { highlightBracketPairs } from "prism-code-editor/highlight-brackets"
import { editor } from "./index"
editor.addExtensions(
highlightSelectionMatches(),
searchWidget(),
defaultCommands(),
copyButton(),
matchTags(),
highlightBracketPairs(),
cursorPosition(),
editHistory(),
)
This library has been rewritten for React and SolidJS. These rewrites better integrate with their respective framework than any wrapper ever could and are highly recommended if you're already using React or SolidJS.
Name | Type | Description |
---|---|---|
language |
string |
Language used for syntax highlighting. Defaults to text . |
tabSize |
number |
Tab size used for indentation. Defaults to 2 . |
insertSpaces |
boolean |
Whether the editor should insert spaces for indentation. Defaults to true . Requires the defaultCommands() extension to work. |
lineNumbers |
boolean |
Whether line numbers should be shown. Defaults to true . |
readOnly |
boolean |
Whether the editor should be read only. Defaults to false . |
wordWrap |
boolean |
Whether the editor should have word wrap. Defaults to false . |
value |
string |
Initial value to display in the editor. |
rtl |
boolean |
Whether the editor uses right to left directionality. Defaults to false . Requires extra CSS from prism-code-editor/rtl-layout.css to work. |
onUpdate |
(value: string => void |
Function called after the editor updates. |
onSelectionChange |
(selection: InputSelection, value: string) => void |
Function called when the editor's selection changes. |
onTokenize |
(tokens: TokenStream, language: string, value: string) => void |
Function called before the tokens are stringified to HTML. |
There are three different event both you and extensions can listen to: tokenize
, update
and selectionChange
.
The tokenize
event is dispatched after the code is tokenized, which makes it possible to change the tokens before the HTML string is created.
The update
event is dispatched after the syntax highlighted DOM has been updated.
The selectionChange
event is dispatched right after the update
event or when the user changes the selection.
If you want to add your own language to Prism or perform syntax highlighting outside of an editor, this is where to import from:
import {
// Functions
highlightText,
highlightTokens,
tokenizeText,
withoutTokenizer,
// Record storing loaded languages
languages,
// Symbols used in grammars
tokenize,
rest,
// Token class
Token
} from "prism-code-editor/prism"
// Utilities used by grammars
import {
clone, insertBefore, extend, embeddedIn
} from "prism-code-editor/prism/utils"
// To add your own language, just mutate the languages record
languages["my-language"] = {
// ...
}
For more information about these exports, read the API documentation.
Note: CRLF and CR line breaks are not supported. So before highlighting, you might need to normalize line breaks using something like text.replace(/\r\n?/g, "\n")
.
As you might've seen from the examples, prism grammars are imported from prism-code-editor/prism/languages/*
. Importing a grammar will automatically register it through side effects. If you're importing multiple grammars, import order usually won't matter. The exception comes when grammars modify other grammars. Take this example:
import "prism-code-editor/prism/languages/typescript"
import "prism-code-editor/prism/languages/js-templates"
This won't add js-templates
features to typescript
because it extended javascript
before js-templates
was added. Swapping the import order fixes the issue.
If you need access to many languages, you can import the following entry points:
prism-code-editor/prism/languages
for all languages (~180kB)prism-code-editor/prism/languages/common
for 42 common languages (~30kB)Take this simple markdown editor as an example. Here, only the markdown grammar is required initially. The common languages are dynamically imported and once they load, the editor is updated, which will highlight all markdown code blocks.
import "prism-code-editor/prism/languages/markdown"
import { createEditor } from "prism-code-editor"
const editor = createEditor("#editor", { language: "markdown" })
import("prism-code-editor/prism/languages/common").then(() => editor.update())
The entry points prism-code-editor/prism
, prism-code-editor/prism/utils
and the grammars from prism-code-editor/prism/languages/*
can all run on Node.js for those who want to generate HTML with it.
height: auto
without layout shiftsMost behavior isn't included by default and must be imported. This is to keep the core small for those who don't need the extra functionality. See advanced usage for how to add extensions.
There are extensions adding:
The default commands extension includes:
And it includes these commands:
import { matchBrackets } from "prism-code-editor/match-brackets"
import { matchTags } from "prism-code-editor/match-tags"
import { indentGuides } from "prism-code-editor/guides"
import {
searchWidget,
highlightSelectionMatches,
highlightCurrentWord,
showInvisibles
} from "prism-code-editor/search"
import { defaultCommands, editHistory } from "prism-code-editor/commands"
import { cursorPosition } from "prism-code-editor/cursor"
import { copyButton } from "prism-code-editor/copy-button"
import { highlightBracketPairs } from "prism-code-editor/highlight-brackets"
import {
readOnlycodeFolding,
markdownFolding,
blockCommentFolding
} from "prism-code-editor/code-folding"
import { autoComplete } from "prism-code-editor/autocomplete"
// And CSS
import "prism-code-editor/search.css"
import "prism-code-editor/copy-button.css"
import "prism-code-editor/code-folding.css"
import "prism-code-editor/invisibles.css"
import "prism-code-editor/autocomplete.css"
import "prism-code-editor/autocomplete-icons.css"
import { Extension } from "prism-code-editor"
interface MyExtension extends Extension {}
const myExtension = (): MyExtension => {
// Add local variables and functions in the closure
return {
update(editor, options) {
// Called when the extension is added to an editor
// And when the options change
},
}
}
You can also write a class with an update method if that's preferred.
You can also use a plain function as an extension. This function won't be called when the editor's options change.
import { BasicExtension, createEditor } from "prism-code-editor"
const myExtension = (): BasicExtension => {
return (editor, options) => {
// This won't be called when the options change
}
}
createEditor("#editor", {}, (editor, options) => {
// This will be called before the first render
})
If you're adding the default commands to your editor, the tab key is used for indentation. If this isn't wanted, you can change the behavior.
Users can at any time toggle tab capturing with Ctrl+M / Ctrl+Shift+M (Mac).
import { setIgnoreTab } from "prism-code-editor/commands"
setIgnoreTab(true)
There are currently 13 different themes you can import, one of them being from prism-code-editor/themes/github-dark.css
.
You can also dynamically import themes into your JavaScript. This is used by the demo website.
import { loadTheme } from "prism-code-editor/themes"
const isDark = matchMedia("(prefers-color-scheme: dark)").matches
loadTheme(isDark ? "github-dark" : "github-light").then(theme => {
console.log(theme)
})
If you're using the setups or web components, you can override the existing themes or add new ones. The example below might be different if you're not using Vite as your bundler.
import { registerTheme } from "prism-code-editor/themes"
registerTheme("my-theme", () => import("./my-theme.css?inline"))
You can import a stylesheet that will give a custom scrollbar to desktop Chrome and Safari.
import "prism-code-editor/scrollbar.css"
You can change the color of the scrollbar thumb using the custom property --editor__bg-scrollbar
. Different alphas will be set based on the state of the scrollbar thumb.
.prism-code-editor {
/* Values are: Hue, saturation, lightness */
--editor__bg-scrollbar: 210, 10%, 40%;
}
If you're not using any of the setups, the styles aren't scoped using a shadow root, which makes them easy to change. If you want to change color, background, font, line-height or similar, you can do it on .prism-code-editor
with CSS.
Default padding is 0.75em
left/right and 0.5em
top/bottom. If you want to change it, you can use the custom property --padding-inline
for left/right. Padding top and bottom can changed by changing the margin on .pce-wrapper
.
There are many classes added to .prism-code-editor
you can use to style the editor based on its state:
pce-has-selection
if the textarea has a selection, and pce-no-selection
if notpce-focus
if the textarea is focusedshow-line-numbers
if line numbers are enabledpce-wrap
if word wrap is enabled, and pce-nowrap
if notpce-readonly
if the editor is read-onlyIt's likely that none of the themes perfectly fit your website. A great solution is to modify one of the included themes to better suit your website. Alternatively, you can import one of the themes and override some of the styles in your on stylesheets.
Below is some additional styling information:
.pce-line::before
will match a line number.pce-line.active-line
matches the line with the cursor.prism-code-editor::before
is the background for the line numbers--number-spacing
is the spacing to the right of the line numbers which defaults to 0.75em
If you're not using the setups, automatic indentation, toggling comments, and automatic closing of tags won't work. You'll need to import the behavior or define it yourself.
The easiest way to get this working, is to import all languages. This will add comment toggling, etc. to almost all Prism languages at ~3.6kB gzipped. It's recommended to dynamically import this since it's usually not needed before the page has loaded.
import("prism-code-editor/languages")
You can also import prism-code-editor/languages/common
instead to support a subset of common languages at less than 2kB gzipped.
Lastly, if you know exactly which languages you need, you can import behavior for individual languages. Refer to the list below for which imports adds comment toggling, etc. to which language(s).
The import for ada
would be prism-code-editor/languages/ada
for example. This list is NOT for Prism grammars.
Import | Languages/aliases added |
---|---|
abap | abap |
abnf | abnf |
actionscript | actionscript |
ada | ada |
agda | agda |
al | al |
antlr4 | g4 and antlr4 |
apacheconf | apacheconf |
apex | apex |
apl | apl |
applescript | applescript |
aql | aql |
arduino | ino and arduino |
arff | arff |
arturo | art and arturo |
asciidoc | adoc and asciidoc |
asm | arm-asm , armasm , asm6502 , asmatmel and nasm |
aspnet | aspnet |
autohotkey | autohotkey |
autoit | autoit |
avisynth | avs and avisynth |
avro-idl | avdl and avro-idl |
awk | awk |
bash | sh , shell and bash |
basic | basic |
batch | batch |
bbj | bbj |
bicep | bicep |
birb | birb |
bison | bison |
bqn | bqn |
brightscript | brightscript |
bro | bro |
bsl | bsl |
cfscript | cfscript |
chaiscript | chaiscript |
cil | cil |
cilk | cilk-c , cilkc , cilk , cilk-cpp and cilkcpp |
clike | clike , js , javascript , ts , typescript , java , cs , csharp , c , cpp , go , d , dart , flow and haxe |
clojure | clojure |
cmake | cmake |
cobol | cobol |
coffeescript | coffee and coffeescript |
concurnas | conc and concurnas |
cooklang | cooklang |
coq | coq |
cshtml | razor and cshtml |
css | css , less , scss and sass |
cue | cue |
cypher | cypher |
dataweave | dataweave |
dax | dax |
dhall | dhall |
django | jinja2 and django |
dns-zone-file | dns-zone and dns-zone-file |
docker | dockerfile and docker |
dot | gv and dot |
ebnf | ebnf |
editorconfig | editorconfig |
eiffel | eiffel |
ejs | ejs |
elixir | elixir |
elm | elm |
erb | erb |
erlang | erlang |
etlua | etlua |
excel-formula | xlsx , xls and excel-formula |
factor | factor |
false | false |
firestore-security-rules | firestore-security-rules |
fortran | fortran |
fsharp | fsharp |
ftl | ftl |
gap | gap |
gcode | gcode |
gdscript | gdscript |
gettext | gettext |
gherkin | gherkin |
git | git |
glsl | glsl and hlsl |
gml | gamemakerlanguage and gml |
gn | gni and gn |
go-module | go-mod and go-module |
gradle | gradle |
graphql | graphql |
groovy | groovy |
haml | haml |
handlebars | mustache , hbs and handlebars |
haskell | idr , idris , hs , haskell , purs and purescript |
hcl | hcl |
hoon | hoon |
html | markup , html , markdown and md |
ichigojam | ichigojam |
icon | icon |
iecst | iecst |
ignore | npmignore , hgignore , gitignore and ignore |
inform7 | inform7 |
ini | ini |
io | io |
j | j |
jolie | jolie |
jq | jq |
json | json , json5 and jsonp |
jsx | jsx and tsx |
julia | julia |
keepalived | keepalived |
keyman | keyman |
kotlin | kts , kt and kotlin |
kumir | kumir |
kusto | kusto |
latex | context , tex and latex |
latte | latte |
lilypond | ly and lilypond |
linker-script | ld and linker-script |
liquid | liquid |
lisp | emacs-lisp , emacs , elisp and lisp |
livescript | livescript |
llvm | llvm |
lolcode | lolcode |
lua | lua |
magma | magma |
makefile | makefile |
mata | mata |
matlab | matlab |
maxscript | maxscript |
mel | mel |
mermaid | mermaid |
metafont | metafont |
mizar | mizar |
mongodb | mongodb |
monkey | monkey |
moonscript | moon and moonscript |
n1ql | n1ql |
n4js | n4jsd and n4js |
nand2tetris-hdl | nand2tetris-hdl |
naniscript | nani and naniscript |
neon | neon |
nevod | nevod |
nginx | nginx |
nim | nim |
nix | nix |
nsis | nsis |
objectivec | objc and objectivec |
ocaml | ocaml |
odin | odin |
opencl | opencl |
openqasm | qasm and openqasm |
oz | oz |
parigp | parigp |
parser | parser |
pascal | pascaligo , objectpascal and pascal |
peoplecode | pcode and peoplecode |
perl | perl |
php | php |
plant-uml | plantuml and plant-uml |
powerquery | mscript , pq and powerquery |
powershell | powershell |
processing | processing |
prolog | prolog |
promql | promql |
properties | properties |
protobuf | protobuf |
psl | psl |
pug | pug |
puppet | puppet |
pure | pure |
purebasic | pbfasm and purebasic |
python | rpy , renpy , py and python |
q | q |
qml | qml |
qore | qore |
qsharp | qs and qsharp |
r | r |
reason | reason |
rego | rego |
rescript | res and rescript |
rest | rest |
rip | rip |
roboconf | roboconf |
robotframework | robot and robotframework |
ruby | crystal , rb and ruby |
rust | rust |
sas | sas |
scala | scala |
scheme | racket and scheme |
smali | smali |
smalltalk | smalltalk |
smarty | smarty |
sml | smlnj and sml |
solidity | sol and solidity |
solution-file | sln and solution-file |
soy | soy |
splunk-spl | splunk-spl |
sqf | sqf |
sql | plsql and sql |
squirrel | squirrel |
stan | stan |
stata | stata |
stylus | stylus |
supercollider | sclang and supercollider |
swift | swift |
systemd | systemd |
tcl | tcl |
textile | textile |
toml | toml |
tremor | trickle , troy and tremor |
tt2 | tt2 |
turtle | rq , sparql , trig and turtle |
twig | twig |
typoscript | tsconfig and typoscript |
unrealscript | uc , uscript and unrealscript |
uorazor | uorazor |
v | v |
vala | vala |
vbnet | vbnet |
velocity | velocity |
verilog | verilog |
vhdl | vhdl |
vim | vim |
visual-basic | vba , vb and visual-basic |
warpscript | warpscript |
wasm | wasm |
web-idl | webidl and web-idl |
wgsl | wgsl |
wiki | wiki |
wolfram | nb , wl , mathematica and wolfram |
wren | wren |
xeora | xeoracube and xeora |
xml | xml , ssml , atom , rss , mathml and svg |
xojo | xojo |
xquery | xquery |
yaml | yml and yaml |
yang | yang |
zig | zig |
import { languageMap } from "prism-code-editor"
languageMap.whatever = {
comments: {
line: "//",
block: ["/*", "*/"]
},
getComments(editor, position) {
// Method called when a user executes a comment toggling command
// Useful if a language uses different comment tokens in different contexts
// Currently used by JSX so {/* */} is used to toggle comments in JSX contexts
},
autoIndent: [
// Whether to indent
([start], value) => /[([{][^\n)\]}]*$/.test(code.slice(0, start)),
// Whether to add an extra line
([start, end], value) => /\[]|\(\)|{}/.test(code[start - 1] + code[end])
],
autoCloseTags([start, end, direction], value) {
// Function called when the user types ">", intended to auto close tags.
// If a string is returned, it will get inserted behind the cursor.
}
}
Adding an editor in the middle of your page will cause layout shifts. This is bad for UX and should ideally be mitigated. One way is to reserve space for the editor by giving its container a fixed height or a grid layout. This works well enough for editors with vertical scrolling.
A second solution is to have a placeholder element which gets replaced by your editor. This is the ideal solution for read-only code examples where you want height: auto
instead of a vertical scroll bar. To make this easier, there's a wrapper around createEditor
intended for exactly this which replaces your element instead of appending the editor to it. The placeholder element's textContent
will be used as the editor's code unless options.value
is defined.
import { editorFromPlaceholder } from "prism-code-editor"
const editor = editorFromPlaceholder("#editor", { language: "javascript" })
If you know the height of the editor, your placeholder could be as simple as a div with a fixed height. If not, the placeholder element should have a very similar layout to the editor, i.e., same padding
, font-size
, font-family
, white-space
, line-height
. How this achieved doesn't matter, but a solution is to use similar markup to the editor itself. Here's an example of this.
There's a utility to display tooltips above or below the cursor that can be imported from prism-code-editor/tooltips
.
const addTooltip = (editor: PrismEditor, element: HTMLElement, fixedWidth?: boolean): [ShowTooltip, HideTooltip]
const [show, hide] = addTooltip(editor, element)
If you want the tooltip to always be visible when the user scrolls horizontally, add position: sticky
along with the left
and/or right
CSS properties to your tooltip.
import { addOverscroll, removeOverscroll } from "prism-code-editor/tooltips"
addOverscroll(editor)
This will allow users to scroll until the last line is at the top of the editor.
RTL support is disabled by default. To enable it, you need to import an extra stylesheet, only then will the rtl
option do something.
import "prism-code-editor/rtl-layout.css"
RTL support is currently experimental due to multiple browser bugs being present:
If you want to use RTL directionality, you should be aware of these bugs and use spaces instead of tabs for indenting.
Editing key commands is as simple as mutating the keyCommandMap
for the editor. If you're adding the default commands to the editor, be sure to mutate the command map after adding that extension.
// Adding a Ctrl+Alt shortcut without removing the default enter functionality
const oldEnterCallback = editor.keyCommandMap.Enter
editor.keyCommandMap.Enter = (e, selection, value) => {
if (e.altKey) {
// Shortcut code goes here
// returning true will automatically call e.preventDefault()
return true
}
return oldEnterCallback?.(e, selection, value)
}
// Removing the default backspace command
editor.keyCommandMap.Backspace = null
Changing editor.inputCommandMap
will work the exact same way.
The library includes a custom element wrapper for each of the 4 setups you can import.
import { addBasicEditor, PrismEditorElement } from "prism-code-editor/web-component"
// Adds a web component with the specified name
addBasicEditor("prism-editor")
const editorElement = document.querySelector<PrismEditorElement>("prism-editor")
// Add a listener for when the editor finishes loading
editorElement.addEventListener("ready", () => console.log("ready"))
// The editor can be accessed from the element
console.log(editorElement.editor)
Attributes include language
, theme
, tab-size
, line-numbers
, word-wrap
, readonly
, insert-spaces
and rtl
. These attributes are also writable properties on the element.
<prism-editor
language="javascript"
theme="vs-code-dark"
tab-size="4"
line-numbers
insert-spaces
word-wrap
>
The editors initial code goes here
</prism-editor>
All the code is tokenized each time for simplicity's sake. Even though only lines that change are updated in the DOM, the editor slows down as more code is added, although not as quickly as with zero optimizations.
Once you start approaching 1000 LOC, the editor will start slowing down on most hardware. If you need to display that much code, consider a more robust/heavy library.
This has been tested to work in the latest desktop and mobile versions of both Safari, Chrome, and Firefox. It should work in slightly older browsers too, but there will be many bugs present in browsers that don't support beforeinput
events.
This library does not support any Prism plugins since Prism hooks have been removed. Behavior like the Highlight Keywords plugin is included.
Some grammars have had small changes, most notably markup tags' grammar. So Prism themes will work to style the tokens, but there can be some slight differences.
PrismJS automatically adds the global regex flag to the pattern of greedy tokens. This has been removed, so if you're using your own Prism grammars, you might have to add the global flag to the greedy tokens.
This library is made possible thanks to Prism.
Feature requests, bug reports, optimizations and potentially new themes and extensions are all welcome.
To test your changes during development, install dependencies:
cd package
pnpm install
And run the development server:
pnpm run dev
Generated using TypeDoc