Sep 12 2025
Ellipsi is a simple JavaScript library used to create
HTML tags programmatically.
const MyButton = button(
'Click me!', // Add child elements,
on('click', () => alert('Clicked')), // event listeners,
{ id: 'my-button' }, // and attributes.
)Ellipsi has one primary function:
tag(name, ...children). This function is used to create
elements of the type HTMLElement with a clean syntax.
const Heading = tag('h1', 'Tutorial')
// -> <h1>Tutorial</h1>Shortcuts are provided for most HTML elements, in the form:
// Shortcut function:
const h1 = (...x) => tag('h1', ...x)
// Using shortcut:
const Heading = h1('Tutorial')
// -> <h1>Tutorial</h1>Since this a common pattern, there is a helper function provided:
const h1 = shortTag('h1')
const Heading = h1('Tutorial')
// -> <h1>Tutorial</h1>Many different things can be passed to tag as children —
text, attributes, event handlers, other tags, and arrays.
const MyTextInput = input({ id: 'my-text-input' })
// -> <input id="my-text-input"></input>
const MyForm = form(
label('Write your name:', { for: MyTextInput.id }),
MyTextInput,
button(
'Greet',
{ type: 'button' },
on('click', () => alert('Hello, ' + MyTextInput.value))
),
)
// -> <form>
// <label for="my-text-input">Write your name</label>
// <input id="my-text-input"></input>
// <button type="button">Greet</button>
// </form>Install the library using npm with:
npm install ellipsi
Then, in your JavaScript module:
import { tag, on, h1, p /* et cetera */ } from 'ellipsi'Alternatively, download the library directly from here and link to it locally.
tag(name, ...children)While the name of the tag should be a string, the
children given to tag can be of many different
types. These will be handled as such:
HTMLElement: Attached to the new tag as a child.Attr: Cloned and attached to the new tag as an
attribute.EventListener: Attached to the new tag using
HTMLElement.addEventListener().Shadow: Attached to the new tag using
HTMLElement.attachShadow({ mode: 'open' }).Object: Parsed as a collection of key/value pairs
representing attributes
(e.g. { id: 'my-id', class: 'my-class' }).Array: Looped through and handled same as other
children.Text and attached to the
new tag as a child.[!WARNING]
tagdoes not clone theHTMLElements passed to it — attaching the same one multiple times will simply move it around.// Improper reusable component: const MyName = span('C Wiebe', { class: 'fancy' }) const AboutMe = section( h1('About ', MyName), // Name will not appear here p('Heya! I am ', MyName), // Instead, it will be moved here )The above results in:
<section> <h1>About </h1> <p>Heya! I am <span class="fancy">C Wiebe</span></p> </section>An
HTMLElementcan be cloned withHTMLElement.cloneNode(true), though this does not preserve event listeners, and so is often bad practice.It is best to turn reusable components into functions that return a new element entirely. Reserve constant elements for times when you intend to be referencing an exact instance of an element in different places, such as an input in a form who’s value will be used later.
// Proper reusable component: const MyName = () => span('C Wiebe', { class: 'fancy' }) const AboutMe = section( h1('About ', MyName()), // Unique element p('Heya! I am ', MyName()), // Unique element )The above results in:
<section> <h1>About <span class="fancy">C Wiebe</span></h1> <p>Heya! I am <span class="fancy">C Wiebe</span></p> </section>
on(types, callback, options?)EventListeners can be created using the
on(types, callback) function, and can be attached to
HTMLElements via tag. types is a
string representing the triggering event name(s) separated by spaces,
and callback is the callback function that takes one
(optional) argument (the triggering Event). on
also takes one optional parameter, options, which can be a
JSON object of event listener options that will be passed directly to
the addEventListener function.
// Ellipsi code:
const UserInputForm = form(FormContent, on('click keydown', handleInput))
// Equivalent js code:
const UserInputForm = document.createElement('form')
UserInputForm.replaceChildren(...FormContent)
UserInputForm.addEventListener('click', handleInput)
UserInputForm.addEventListener('keydown', handleInput)shortTag(name, ...x)Shortcut functions can be created using the
shortTag(name, ...x) function, and then can be used to
create HTMLElements using the tag function
indirectly.
// Creating shortcuts:
const h1 = shortTag('h1')
const checkbox = shortTag('input', { type: 'checkbox' })
// Using shortcuts:
const Heading = h1('Tutorial')
// -> <h1>Tutorial</h1>
const UserInput = checkbox({ id: 'user-input', checked: '' })
// -> <input type="checkbox" id="user-input" checked=""></input>attr(key, value)If you don’t like JavaScript Object Notation or need to use
JavaScript’s built-in Attr class to represent your HTML
attributes, attr is provided as shorthand.
// Ellipsi code:
const Link = a(
'The wisdom of Georg',
attr('href', 'https://www.spidersge.org'),
)
// Equivalent js code:
const href = document.createAttribute('href')
href.value = 'https://www.spidersge.org'
const Link = document.createElement('a')
Link.appendChild(document.createTextNode('The wisdom of Georg'))
Link.setAttributeNode(href.cloneNode())[!NOTE]
You can also use JSON to append attritubes instead of the
attrfunction.// Ellipsi code (with JSON attributes): const Link = a( 'The wisdom of Georg', { href: 'https://www.spidersge.org' }, )
shadow(...components)[!WARNING]
Shadow roots are what I would consider an “advanced topic” for most users. They may be difficult to use with this library, because this library stays very true to their default behavior and their default behavior is difficult to use.
Shadows can be created using the
shadow(...components) function, and can be attached to
HTMLElements via tag. This function is similar
to the tag function in that it takes many different types
(and handles them in the same way), but differs in that shadow roots can
have their own stylesheets (CSSStyleSheets) and
cannot have attributes or event listeners. Attributes and event
listeners should be attached to whatever host element the shadow root is
attached to.
const documentStyles = tag('style',
'p { color: red; }'
)
const shadowStyles = tag('style',
'p { font-weight: bold; }'
)
document.head.appendChild(documentStyles)
document.body.replaceChildren(
p('I am not in the shadow DOM or in the slot'),
span(
p('I am not in the shadow DOM, but *am* in the slot'),
// As is normal shadow root behavior, the shadow will
// override non-shadow elements. Non-shadow elements
// will be hidden unless they are placed in a slot
shadow(
shadowStyles,
p('I am in the shadow DOM before the slot'),
slot(),
p('I am in the shadow DOM after the slot'),
),
),
)The above results in:
<p>I am not in the shadow DOM or in the slot</p>
<span>
#shadow-root (open)
<p>I am in the shadow DOM before the slot</p>
<slot>
<p>I am not in the shadow DOM, but *am* in the slot</p>
</slot>
<p>I am in the shadow DOM after the slot</p>
</span>