html-elements

sidewayss/html-elements == sideways-elements

<input-num> has its own app page because it has a lot to test and demonstrate.

The other three share a test/demo app and a base class.

Summary

It’s a collection of autonomous custom HTML elements that can be graphically customized at the site level and/or by page. It fetches the template(s) during the page load process, and import.meta allows you to specify the template file or directory as import options.

The templates use SVG for the graphics. CSS styling features for custom elements, in particular ::part, depend on the baseline browser version you intend to support.

It has a very limited selection of four elements and no external dependencies. There is room to grow.

Usage

Release 1.0 has both source modules and tranpiled, minified bundles. The bundles are in the dist/ directory. The source is in src/ in the NPM package, and in the root directory in the repository (which makes it easier to create dist/ versions of the test/demo apps for testing the dist/ modules).

For example, to import <input-num>:

<head>
    <script src="<your-path>/dist/input-num.js" type="module"></script>
</head>

File Names

There are 4 elements and 4 root file names:

Those root names apply to src/ and dist/ module files, template files, the sample CSS files, as well as <template id="root-name"> when you bundle your templates into a single file.

There is one more importable module, which bundles all the elements together: elements.js. The distributable is bundled, transpiled, and minified. The source is a barrel module with no external dependencies. Note that you cannot use import.meta options with this source module because the browsers’ module-load order prevents it.

Import Options

For example. to set the template directory to /html-templates (the trailing slash is optional):

<head>
    <script type="module">
      import "<your-path>/input-num.js?templateDir=/html-templates/";
    </script>
</head>

Which fetches /html-templates/input-num.html as the template file.

If you bundle your templates into one file, then each <template> must have the correct id. For example:

<template id="input-num">...</template>

NOTE: If you are using import options and getting an error about an element/tag already being registered, you might need to add the same import options to your JavaScript import statements, so that they match your HTML <script> imports. This happens sometimes because the browser imports them as two separate files, for reasons unknown to me.

Managing Template Files

There are built-in template files in the templates/ directory. They serve two purposes:

These files are part of the repository, so you don’t want to be editing them in-place unless you’re planning to submit a pull request. Instead, you should create your own directory, wherever convenient, and store your template files there. You might start by copying the built-in files there and working within their structures.

NOTE: The part attribute and CSS ::part are new enough that it’s worth reviewing the current support grid (the two grids are identical). The built-in templates have fallback styles to support older browsers. Remember that ::part overrides the element’s style unless you specify !important.

Also note: As of the start of 2025, using the part attribute inside <defs> is unreliable across browsers. Firefox doesn’t recognize it. Chrome doesn’t allow hyphenated part names. I haven’t gotten past those two yet. The built-in template files avoid doing this, which limits their internal structure and complicates their external styling. Of course removing the fallback styles from inside the template helps simplify external styling too, if you can afford to do that.

NOTE: The CSS files in the css sub-directory are samples, examples. They are used in the test/demo apps, but not by the elements themselves.

DOMContentLoaded

If you have code that runs during the page load process, there are two sets of promises that you might need to reference:

Browser Compatibility

Compatibility across the browsers is good. The issues are with older versions of iOS/Safari. Currently, the dist/ files support iOS 12.2 as the oldest version. Support for iOS 12 goes back to iPhone 5s, which is the first 64bit iPhone. See here and here for details of changes to the code for backwards compatibility. Support could go back as far as iOS 10.3, when support for custom HTML elements began, but the pre-12.2 issue requires changes to the template structure, which would create a backward compatibility issue within the project. Please submit a GitHub issue or pull request if you really need 10.3 support.

Custom Elements Manifest

The root directory contains a custom-elements.json file for the full set of elements. It is auto-generated by @custom-elements-manifest/analyzer. The code does not use JSDoc, for now the docs are here. I don’t use IntelliSense or anything else that might read the manifest, so please let me know if it’s not working.

The Elements

When there are corresponding/reflected DOM attributes and JavaScript properties, they will be written as:

A quick glossary entry of note: A boolean attribute is an attribute whose JavaScipt value is determined by hasAttribute() not getAttribute().

JavaScript Superclasses

These classes manage common attributes/properties and behaviors across elements. All the attributes/properties listed are inherited by the sub-classes.

class BaseElement extends HTMLElement

BaseElement is the top level class. It is the parent class for InputNum and StateBtn. See base-element.js.` It manages two global DOM attributes/properties:

and one read-only property:

NOTE: HTMLElement does not have a disabled attribute/property. It only has tabindex/tabIndex. BaseElement observes the disabled and tabindex attributes and exposes the properties. To do so requires the disabled attribute to manage tabindex too, because setting disabled should set tabindex="-1". This is complicated when you set tabindex instead of relying on the default tab order. It’s internally manageable except for this one case:

This is because the DOM runs attributeChangedCallback() in a FIFO queue and when disabled sets the tabindex it goes to the end of the queue, after the tabindex setting in the HTML file.

Also note: Though it’s allowed, setting tabindex to anything other than 0 or -1 is sternly recommended against by MDN (scroll down the page to the first Warning block).

class StateBtn

StateBtn is the class for <state-btn> and the parent of CheckBase.

There is no input event, just change. For these types of <input> they’re the same anyway. If you desire it for compatibility reasons, submit an issue or a pull request.

class CheckBase

CheckBase (check-base.js) is the parent class for <check-box> and <check-tri>.

<check-box>

I created <check-box> because I needed <check-tri> and I wanted all my checkboxes to look and act alike. It’s the same as <input type="checkbox"> except:

<check-trie>

<input type="checkbox"> has an indeterminate property (not an attribute) that is independent from checked. I don’t have a use for that. I needed a third, “indeterminate” value in addition to, and mutually exclusive from, true and false. I wanted that value to cause checked to fall back to a user-determined default boolean value. So checked remains boolean, but value can be true, false, or null. It’s null, not undefined, because that’s what getAttribute() returns when an attribute is unset.

To set value as an attribute, I use "1" for true and you must use "" for false.

Additional Attributes / Properties

multi-check.html

<check-box> and <check-tri> share a template file. The built-in template is potentially reusable because the shapes are simple and they are externally styleable with ::part. This is the <template>:

<template>
  <svg id="shapes" part="shapes" viewbox="0 0 18 15" width="18" height="15">
    <defs> <!-- this template doesn't define #false -->
      <rect id="box" x="0.5" y="1.5" width="13" height="13" rx="2" ry="2"/>
      <path id="true" d="M3.5,8.5 L6,11 10,4.5"/>
      <path id="null" d="M4,8 H10"/>
    </defs>
    <!-- #mark and #default-mark must be refer to #false -->
    <g id="default" part="default" pointer-events="none">
      <use href="#box" part="default-box"/>
      <use href="#false" id="default-mark" part="default-mark"/>
    </g>
    <g part="check">
      <use href="#box" part="box"/>
      <use href="#false" id="mark" part="mark"/>
    </g>
  </svg>
  <pre id="label" part="label"></pre>
</template>

It may be called a “checkbox”, but #box is optional. Here it’s a <rect>, but it can be any element or missing. In a tic-tac-toe design, where X is true and &cir; is false, there’s no need for a box.

The other optional element with an id is #false. The built-in template doesn’t use it because it’s modeled on a standard checkbox, where false is an empty box. Technically, #true is not required either, but unless you’re building a topsy-turvy app where “checked” is empty, you’ll need to include it. For your result to make sense, you must define at least one of #true|false for <check-box>, and at least two of #true|false|null for <check-tri>.

#null and the entire #default group are only used by <check-tri>. If you only plan on using <check-box>, you can omit them.

#true, #false, and #null can be any SVG element type, and they must be inside a <defs>.

#default must be a <g>. #default-mark must be a <use> and a child of #default.

#mark is the “check mark”. It must be a <use>. #mark and #default-mark must refer to #false in the template because both checked and value can be left unset in HTML and the JavaScript doesn’t force a default reference.

#label can be any element type that displays text and is not focusable. This template uses <pre> along with a fixed-width font. <label> is not recommended because SVG elements are not labelable.

The part attributes are all optional.

NOTE: With this template, the element is a flex container, relying on the default flex-direction:row.

check-box, check-tri {
  display: flex;
  align-items: center;
}

An alternative is to put a flex container inside the template, as the template for <input-num> does.

<state-btn>

<state-btn> is an open-ended toggle, allowing you to define:

Additional Attributes / Properties (and a method)

NOTE: The default value on initial page load is the first state defined. I see no need to set the value attribute in HTML. The toggle order is completely user-controlled, so just make your default state the first one. If you really need to declare the value in HTML, you must do it after states or the value won’t validate. I did not see the value in adding code to make it order-independent.

state-btn.html

The built-in template is a pair of playback buttons: play and stop. It’s not nearly as reusable as multi-check.html, but this is a more raw, open-ended kind of element. It requires customization to match its flexibility.

There is one template for all your buttons. One ‘use’ element and as many definitions as you need.

The templates requires a <defs> that contains an element with a matching btn-id for every state id you define. Here is some sample HTML, where the second element of each array is the state id:

<state-btn id="play" class="row" states='[[0,"play"], [1,"pause"], [2,"resume"]]'></state-btn>
<state-btn id="stop" class="row" states='[[0,"stop"], [1,"reset"]]' disabled></state-btn>

To match this template:

<template>
  <style>
    :host { --square:calc(1rem + var(--9-16ths)) }
    svg   { width:var(--square); height:var(--square); }
  </style>
  <svg viewbox="0 0 25 25">
    <defs>
      <!-- 3 images for #play button -->
      <path id="btn-play"   d="M3,2 L22,12.5 3,23 Z"/>
      <g    id="btn-pause">
        <rect x= "3" y="3"  width="7" height="19" rx="1" ry="1"/>
        <rect x="15" y="3"  width="7" height="19" rx="1" ry="1"/>
      </g>
      <path id="btn-resume" d="M3,3 V22 M8,3 L22,12.5 8,22 Z"/>

      <!-- 2 images for #stop button -->
      <rect id="btn-stop"   x="3" y="3" width="19" height="19" rx="2" ry="2"/>
      <path id="btn-reset"  d="M3,3 V22 M22,3 L8,12.5 22,22 Z"/>
    </defs>
    <use id="btn" href="#"/>
  </svg>
</template>

<input-num>

Based on an informal survey and my own repeated frustrations, I came to the conclusion that <input type="number"/> isn’t just the worst HTML input, it’s a total waste of time. I needed an alternative. I spent over a decade programming for finance executives and financial analysts, so I got to know Microsoft Excel. Regardless of the brand, spreadsheets all use the same, well-established paradigm for inputting and displaying numbers. I decided to create a custom element that imitates a spreadsheet, while maintaining consistency with the default <input type="number"/> as implemented by the major browsers (which implement it somewhat inconsistently).

Features

Spreadsheet Emulation

<input type="number"/> Emulation and Variation

Additional Features

Keyboard Navigation

HTML Attributes / JavaScript Properties

There are several inverted boolean attributes / properties, where the attribute value is the opposite of the property value. They are all for turning features off. The negative makes sense for the boolean attribute name, but it’s clumsy for the property. The attribute names all start with no-. The default is always:

DOM Attributes

Property name same as attribute. String as attribute / Number as property.

Behavior

Spinner

Element Formatting

Number Formatting

NOTE: When you combine a currency symbol with units, it displays as currency per unit. For example:

inputNum.locale   = "es-MX";  // Español, Mexico
inputNum.currency = "MXN";    // Mexican Peso
inputNum.units    = "kg";     // kilogram

displays 100 as: $100/kg

Miscellaneous JavaScript Properties and Methods:

Events

The only event that you can listen to is change. I don’t see a need for any other events. If you need some, e.g. input, keydown, or keyup attached to the inner <input type="text"/>, then please submit an issue or a pull request.

The only event I’ve considered adding is an endspin event for when spinning ends. It would fire on mouseup or keyup when spinning. It could be useful for throttling external changes based on the value.

The change event fires every time the value changes:

When the user inputs via the spinner, the event object has two additional properties:

The validate property allows you to insert your own validation and/or transformation function before the value is committed and the change event is fired. Because it runs before committing the value, it runs before the internal !isNaN() validation. The function takes two arguments: value and isSpinning:

To indicate an invalid value, return false. Otherwise return the value itself, transformed or not. Transforms are for those rare occasions when you want to round to the nearest prime number, or whatever transformation or restriction that can’t be defined solely by max and min.

Styling

You can obviously style the element itself, but you can also style its parts via the ::part pseudo-element. Remember that ::part overrides the shadow DOM elements’ style. You must use !important if you want to override ::part.

The available parts:

My preference, as illustrated in the sample css/input-num.css file, is to not display the spinner or confirm buttons on devices that can’t hover:

@media (hover:none) {
  input-num::part(buttons) { display:none }
}

Those devices are all touchscreen, and focusing the element will focus the <input>, which will display the appropriate virtual keyboard. Touch and hold has system meanings on touch devices, which conflicts with spinning. And at font-size:1rem the buttons are smaller than recommended for touch. So unless you create oversized buttons or use a much larger font size, it’s best not to display them at all.

NOTE: Auto-sizing only works if the element is displayed. If the element or any of it’s ancestors is set to display:none, the element and its shadow DOM have a width and height of zero. During page load, don’t set display:none until after your elements have resized.

NOTE: If you load the font for your element in JavaScript using document.fonts.add(), it will probably not load before the element. So resize() won’t be using the correct font, and you’ll have to run it again after the fonts have loaded. Something like this:

document.addEventListener("DOMContentLoaded", load);
function load() {
    const whenDefined = customElements.whenDefined("in-number");
    Promise.all([whenDefined, document.fonts.ready]).then(() => {
        for (const elm of document.getElementsByTagName("input-num"))
            elm.resize();
    });
}

If you are doing this and want to be more efficient, set the no-resize attribute on the element:

<input-num no-resize></input-num>

Then turn on the autoResize property prior to calling resize() :

for (const elm of document.getElementsByTagName("input-num")) {
    elm.autoResize = true;
    elm.resize();
}

NOTE: If you set no-spin and no-confirm, you should probably also set the element’s tabindex="-1", and put any border or padding on ::part(input) not the element itself. Otherwise the outer element will still be in the tab order or clickable for focus, and that serves no purpose with those two attributes set.

input-num.html

The core of the template is a flex <div>, with three children:

Do not modify:

Everything else is user-configurable, though you’ll probably want to keep the flex container:

<defs> defines the shapes, and <style> formats them. There are two pairs of button shapes, each pair consisting of top/bottom:

The actual buttons, <rect> elements that handle events, have the ids “top”/”bot”. The definitions are setup as a single block containing each pair. This allows you to create a single image that responds differently when interacting with the top or bottom button. That kind of design makes more sense for the spinner than the confirm buttons…

The def ids are built in two or three segments separated by hyphens (idle only has two segments).There are a total of 14 ids: | Pair | State | #id/href | | —- | —– | ——– | | spinner | idle | #spinner-idle | | spinner | keydown*| #spinner-key-top
#spinner-key-bot | | spinner | hover | #spinner-hover-top
#spinner-hover-bot | | spinner | active | #spinner-active-top
#spinner-active-bot | | spinner | spin | #spinner-spin-top
#spinner-spin-bot | | confirm | idle | #confirm-idle | | confirm | hover | #confirm-hover-top
#confirm-hover-bot | | confirm | active | #confirm-active-top
#confirm-active-bot |

* ArrowUp or ArrowDown key, initial image is state:key, full-speed spin uses spin-
spin-top and spin-bot are the full-speed spin images, used after delay expires