Documentation
EJS is a simple templating language that lets you generate HTML markup with plain JavaScript. It runs on the server (Node.js) and in the browser, ships with a CLI, and has zero dependencies.
Getting Started
Install
Add EJS to your project with npm:
npm install ejsImport it
Pick the module system that fits your project.
// ESMimport ejs from 'ejs';// CommonJSconst ejs = require('ejs');Bundler and runtime compatibility
As of v6, the published package is verified against the following bundlers and alternate runtimes on every release:
| Tool | Supported |
|---|---|
| Rollup | ✓ |
| Rolldown | ✓ |
| tsdown | ✓ |
| esbuild | ✓ |
| Webpack | ✓ |
| Vite | ✓ |
| Browserify | ✓ |
| Bun | ✓ |
| Deno | ✓ |
For Browserify, pass --node so it picks the main entry rather than the
prebuilt UMD bundle.
Browser-targeted bundlers that support conditional exports get the prebuilt
browser bundle through the browser export condition. That bundle compiles and
renders template strings; loading templates from the filesystem with
renderFile needs the Node entry point.
Render a string
Pass EJS a template string and some data. You get back HTML.
import ejs from 'ejs';
const people = ['geddy', 'neil', 'alex'];const html = ejs.render('<%= people.join(", "); %>', { people });// => "geddy, neil, alex"Render a file
renderFile reads a template from disk. The third argument is an options
object; the callback receives the rendered string.
import ejs from 'ejs';
ejs.renderFile('./template.ejs', { people }, (err, html) => { if (err) throw err; console.log(html);});You can also await it when you omit the callback:
const html = await ejs.renderFile('./template.ejs', { people });Use it with Express
EJS complies with the Express view system, so it works out of the box: just set the view engine.
import express from 'express';
const app = express();app.set('view engine', 'ejs');
app.get('/', (req, res) => { res.render('index', { title: 'Home', people });});As of v7, EJS doesn’t read its options from Express’s render data,
app.locals, or app.set('view options'). To set EJS options for an Express
app, register a render function. Pass along Express’s views and view cache
settings too, since a custom render function replaces the default handling of
them:
const ejsOptions = { delimiter: '?', views: [].concat(app.get('views')), cache: app.enabled('view cache'),};app.engine('ejs', (path, data, cb) => { ejs.renderFile(path, data, ejsOptions, cb);});Tags
EJS templates are HTML with JavaScript embedded inside delimiter tags. By
default the tags open with <% and close with %>. Each variant controls
whether code runs, whether its result is printed, and how it is escaped.
Tag reference
| Tag | Name | What it does |
|---|---|---|
<% |
Scriptlet | Runs control-flow JavaScript; produces no output. |
<%_ |
Whitespace-slurping scriptlet | Like <%, but strips the spaces and tabs before it. |
<%= |
Escaped output | Prints the value into the template, HTML-escaped. |
<%- |
Unescaped output | Prints the raw value into the template (no escaping). |
<%# |
Comment | Does nothing and prints nothing. |
<%% |
Literal | Outputs a literal <%. |
%> |
Closing tag | Plain close. |
-%> |
Newline-trimming close | Trims the newline immediately after the tag. |
_%> |
Whitespace-slurping close | Strips the spaces and tabs after the tag, then one newline. |
Scriptlets and output
Use a scriptlet (<%) for logic and an output tag (<%=) to print a value:
<% if (user) { %> <h2><%= user.name %></h2><% } %>
<ul> <% items.forEach(item => { %> <li><%= item %></li> <% }); %></ul>Escaped vs. unescaped output
<%= escapes HTML so user-supplied values can’t inject markup. Use <%- only
when you intend to emit raw HTML (for example, the result of an
include):
<%# value is "<b>hi</b>" %><%= value %> <%# renders: <b>hi</b> %><%- value %> <%# renders: <b>hi</b> %>Escaping and output context
<%= escapes for HTML: specifically, for HTML content and quoted attribute
values. That covers most templates, because most templates generate HTML.
EJS can generate any kind of text, though, and it has no idea where your output ends up. If you’re generating something else (SQL, shell scripts, plain-text email, config files), swap in escaping that fits:
ejs.render(template, data, { escape: myEscapeFunction });To escape just one value, use the raw tag: <%- myEscapeFunction(name) %>. To
set it for a whole Express app, see Use it with Express.
One spot to watch: inside a <script> tag, browsers don’t decode HTML
entities, so <%= doesn’t do what you want there. Put the value in an
attribute and JSON.parse it from your script instead.
Comments
<%# ... %> lets you annotate templates. The contents are never executed and
never appear in the output.
<%# This block explains the markup below — it won't render %><p>Visible to the reader.</p>Controlling whitespace
The slurping tags help you keep generated HTML tidy. -%> removes the newline
directly after a tag. <%_ strips the spaces and tabs before it, and _%>
strips the spaces and tabs after it, then one newline:
<ul><% items.forEach(item => { -%> <li><%= item %></li><% }); -%></ul>For broader cleanup across the whole template, see the
rmWhitespace option.
Includes
Includes let you pull one template into another: headers, footers, list
items, anything reusable. Paths are resolved relative to the template that
calls include().
Basic usage
Call include() inside an unescaped output tag (<%-) so the partial’s
HTML isn’t double-escaped:
<ul> <% users.forEach(user => { %> <%- include('user/show', { user: user }); %> <% }); %></ul>user/show.ejs receives the data you pass as the second argument:
<%# user/show.ejs %><li><%= user.name %></li>The raw-output tag
include() returns a string of already-rendered HTML. Print it with the
escaping tag <%= and every <, >, and & gets escaped, so the markup shows
up as literal text. The raw-output tag <%- emits it as-is.
Path resolution and filename
Relative include paths need to know where the calling template lives, so EJS
requires the filename option to be set. When you
use ejs.renderFile() or Express, filename is set automatically. If you call
ejs.render() on a raw string and use includes, set it yourself:
const html = ejs.render(template, data, { filename: '/path/to/template.ejs',});To resolve includes from a set of base directories (or with absolute paths
like /partials/header), use the views and
root options.
Preprocessor include (legacy)
Older EJS supported a literal <% include user/show %> form. It is deprecated
and has no caching benefits. Prefer the include() function shown above.
Custom Delimiters
The default delimiters are <% and %>. You can change the inner character,
the opening character, and the closing character, either per render or
globally.
Per-template
Pass delimiter (the inner character, default %) in the options object:
import ejs from 'ejs';
const people = ['geddy', 'neil', 'alex'];const html = ejs.render('<?= people.join(", "); ?>', { people }, { delimiter: '?',});// uses <? ... ?> instead of <% ... %>You can also change the outer characters with openDelimiter and
closeDelimiter:
const html = ejs.render('[?= people.join(", "); ?]', { people }, { delimiter: '?', openDelimiter: '[', closeDelimiter: ']',});Globally
Set the defaults on the ejs object once, and every subsequent render uses
them:
import ejs from 'ejs';
ejs.delimiter = '?';ejs.openDelimiter = '[';ejs.closeDelimiter = ']';On the command line
The CLI exposes the same controls via -m, -p, and
-c:
ejs ./template.ejs -m '?' -o ./output.htmlSee the Options reference for the full list of delimiter-related settings.
Layouts
EJS does not have a dedicated layout or block-inheritance system. Instead, you compose pages from partials using includes, which is flexible enough to cover the common header/content/footer pattern.
Header and footer
Wrap each page’s content between a shared header and footer:
<%# page.ejs %><%- include('header'); -%>
<h1><%= title %></h1><p>This is the page body.</p>
<%- include('footer'); -%><%# header.ejs %><!DOCTYPE html><html> <head> <title><%= title %></title> </head> <body><%# footer.ejs %> </body></html>