Skip to content

Instantly share code, notes, and snippets.

@Potherca
Last active October 13, 2023 13:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Potherca/0a04271c143bc51862a6cc40a0a74b2f to your computer and use it in GitHub Desktop.
Save Potherca/0a04271c143bc51862a6cc40a0a74b2f to your computer and use it in GitHub Desktop.
An automated HTML element overview page. https://gist.pother.ca/0a04271c143bc51862a6cc40a0a74b2f

HTML Element Reference

There are some great resources out there that document HTML elements.

Such as HTML Elements on MDN and htmlreference.io.

However, for me, most of these resources are either too exhaustive (like MDN) or too brief (like htmlreference.io).

What I want is a list of all available HTML elements, to get an idea of what the element is, without having to click through to another page. But also with enough information to know what the element is used for and how it is used.

So I built one.

This page pulls its information directly from the living HTML standard, so it is guaranteed to be up-to-date.

It can be seen in action at https://gist.pother.ca/0a04271c143bc51862a6cc40a0a74b2f

Used Techniques

CSS

Some CSS features this page uses:

  • Dark Mode in 3 Lines of CSS as described by Mads Stoumann

    :root {
      --background-color: Canvas;
      --text-color: CanvasText;
      color-scheme: light dark;
    }
  • Dotted Underline A regular dotted underline (using text-decoration-style: dotted), is rater clunky. By setting text-decoration-thickness to 1px it becomes a lot more subtle.

    .category-anchor {
      text-decoration: underline;
      text-decoration-style: dotted;
      text-decoration-thickness: 1px;
    }
  • Smooth Scrolling When an element is linked to (using page anchors, i.e. the URL hash), the page will smoothly scroll to the target element. This is done using scroll-behavior: smooth;. As there is a header on top of the page, scroll-margin-top is used to offset the scroll position.

    html {
      scroll-behavior: smooth;
    }

    The javascript counterpart to this is: element.scrollIntoView({behavior: 'smooth'}).

Client-side JS

As fetch has a .json() function, but not a .html() function DOMParser is used to get the HTML from the response.

fetch('https://example.com')
  .then(response => response.ok ? response.text() : Promise.reject(response)
  .then(text => new DOMParser().parseFromString(text, 'text/html')))
  .then(document => document.querySelector('.selector'))
  .then(element => /* Do stuff with the element */)

Server-side JS

As the information I wanted does not have the correct CORS headers, I used my personal CORS proxy to fetch the data.

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<title>HTML Reference</title>
<link rel="icon" type="image/svg+xml" href="https://favicon.potherca.workers.dev/38">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&family=Noto+Sans+Mono&family=Noto+Sans&display=swap" rel="stylesheet">
<style>
/*=======( Variables )====================================================*/
:root {
--background-color: Canvas;
--text-color: CanvasText;
}
/*=======( General Styling )==============================================*/
a {
color: inherit;
}
body {
background-color: var(--background-color);
color-scheme: light dark;
color: var(--text-color);
font-family: 'Noto Sans', sans-serif;
line-height: 1.5;
margin: 0;
padding: 0;
scroll-behavior: smooth;
}
/*=======( Footer Styling )===============================================*/
footer {
padding: 2rem;
text-align: center;
}
/*=======( Header Styling )===============================================*/
header {
position: sticky;
top: 0;
z-index: 10;
background: linear-gradient(to bottom, var(--background-color) 50%, rgba(0, 0, 0, 0) 100%);
}
/*-------( Title )--------------------------------------------------------*/
h1 {
text-align: center;
}
/*=======( Main Styling )=================================================*/
/*-------( Elements List )------------------------------------------------*/
.elements {
list-style: none;
margin: 0 auto;
max-width: 80rem;
padding: 0 2rem;
}
.element {
border-radius: 0.25rem;
border: 1px solid #ccc;
left: 4em;
margin: 0.85rem 0;
max-width: calc(100% - 8em);
padding: 0.35rem;
position: relative;
}
.element:hover {
box-shadow: 0 0 2em #ee8;
}
.element:hover .tag-anchor {
visibility: visible;
}
.tag-anchor {
display: block;
position: absolute;
right: 0;
text-decoration: none;
top: 0;
visibility: hidden;
}
/*-------( Individual Tags )----------------------------------------------*/
.tag::after {
content: '>';
}
.tag::before {
content: '<';
}
.tag * {
display: inline;
}
.no-categories {
display: none;
}
.element:hover .tag-categories,
.element:hover .tag-children,
.element:hover .tag-parents,
.element:hover .no-children,
.element:hover .no-parents {
height: initial;
}
.element:hover .tag-categories::before,
.element:hover .tag-children::before,
.element:hover .tag-parents::before,
.element:hover .no-children::before,
.element:hover .no-parents::before,
.tag-categories,
.tag-children,
.tag-parents,
.no-children,
.no-parents {
height: 0;
overflow: hidden;
border: none;
}
.tag-categories,
.tag-children,
.tag-parents,
.no-children,
.no-parents {
margin: 0;
text-indent: 2rem;
}
.tag-children::before,
.tag-parents::before,
.no-children::before,
.no-parents::before {
background-color: #5c5;
border-radius: 100%;
border: 1px solid black;
color: #fff;
content: " ";
display: block;
font-family: "Noto Sans Mono", monospace;
font-size: 0.75rem;
font-weight: bold;
height: 1.35em;
line-height: 1.35em;
position: absolute;
text-align: center;
text-indent: 0;
text-shadow: 0 0 0.1em #000;
top: 1.5em;
width: 1.35em;
}
.no-children::before,
.no-parents::before {
background-color: #c55;
}
.tag-children::before, .no-children::before {
content: "C";
left: -1.35rem;
}
.tag-parents::before, .no-parents::before {
content: "P";
left: -2.7rem;
}
/*.......( Tag Attributes )...............................................*/
.tag-attributes {
display: inline-block;
list-style: none;
margin: 0 0 0 1rem;
padding: 0;
text-align: justify;
}
.tag-attribute::after {
content: '=""';
opacity: 0.35;
}
/*.......( Tag Description )..............................................*/
.tag-description {
font-style: italic;
}
/*-------( Category Tooltip )------------------------------------------------*/
.category-anchor {
cursor: help;
text-decoration: underline;
text-decoration-style: dotted;
text-decoration-thickness: 1px;
}
/*-------( Tag Highlight )------------------------------------------------*/
.highlight {
border-color: #ee8;
border-radius: 0.25rem;
box-shadow: 0 0 2em #ee8;
scroll-margin-top: 5rem;
}
/*-------( Category List )------------------------------------------------*/
.category-elements {
list-style: none;
text-align: justify;
}
.category-element {
display: inline-block;
margin: 0 0.35rem;
}
.category-element:hover a {
text-decoration: underline
}
.category-element a {
text-decoration: none;
}
</style>
</head>
<body>
<header>
<h1>HTML Reference</h1>
</header>
<main>
<section>
<h2>HTML Elements</h2>
<ul data-js="elements" class="elements"></ul>
</section>
<section>
<h2>Categories</h2>
<ul data-js="categories" class="categories"></ul>
</section>
</main>
<footer></footer>
<script>
function addLinks(subject, tagList, categoryList) {
let html = ''
subject.sort().forEach((item, index) => {
if (index > 0) {
html += ', '
}
if (tagList.has(item)) {
html += `<a href="#${item}">&lt;${item}&gt;</a>`
} else if (typeof categoryList[item] !== 'undefined') {
html += `<a class="category-anchor" href="#${item}" title="&lt;${categoryList[item].join('&gt;&#10;&lt;')}&gt;">${item}</a>`
} else {
html += item
}
})
return html
}
function buildHtml({elements, tagList, categoryList}) {
const elementsHtml = []
const categoryHtml = []
elements.forEach(tag => {
let attributeHtml = ''
if (tag.attributes.length > 0) {
attributeHtml = `<ul class="tag-attributes"><li class="tag-attribute"> ${tag.attributes.join('</li><li class="tag-attribute"> ')}</li></ul>`
}
const categories = tag.categories.filter(i => i !== 'none')
const children = tag.children.filter(i => i !== 'none')
const parents = tag.parents.filter(i => i !== 'none')
let categoriesHtml = ''
let childrenHtml = ''
let parentsHtml = ''
if (categories.length > 0) {
categoriesHtml = `Categories: ${addLinks(categories, tagList, categoryList)}`
}
if (children.length > 0) {
childrenHtml = `Children allowed: ${addLinks(children, tagList, categoryList)}`
/* Some elements are described as "transparent", meaning it is derived from its parent element. */
if (parents.length > 0) {
childrenHtml = childrenHtml.replace(/transparent/g, `<span class="tag-transparent">${addLinks(parents, tagList, categoryList)}</span>`)
}
}
if (parents.length > 0) {
parentsHtml = `Child of: ${addLinks(parents, tagList, categoryList)}`
}
elementsHtml.push(`
<li id="${tag.name}" class="element">
<hgroup class="tag">
<h2 class="tag-name">${tag.name}</h2>
${attributeHtml}
</hgroup>
<a
class="tag-anchor"
href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/${tag.name}"
target="_blank"
title="Go to MDN documentation for &lt;${tag.name}&gt;"
>MDN ↗️</a>
<p class="tag-description">${tag.description}</p>
<p class="${parents.length > 0 ? 'tag-parents' : 'no-parents'}">${parentsHtml}</p>
<p class="${children.length > 0 ? 'tag-children' : 'no-children'}">${childrenHtml}</p>
<p class="${categories.length > 0 ? 'tag-categories' : 'no-categories'}">${categoriesHtml}</p>
</li>
`)
})
Object.entries(categoryList).forEach(([category, elements]) => {
elements.sort()
const items = `
<li id="${category}" class="category">
<h3 class="category-name">${category}</h3>
<ul class="category-elements">
${elements.map(element => `<li class="category-element"><a href="#${element}">&lt;${element}&gt;</a></li>`).join('')}
</ul>
</li>
`
categoryHtml.push(items)
})
return {
categories: categoryHtml,
elements: elementsHtml,
}
}
function handleHashChange(event) {
document.querySelectorAll('.highlight').forEach(element => {
element.classList.remove('highlight')
})
const hash = document.location.hash
if (hash) {
const element = document.querySelector(hash)
if (element) {
element.classList.add('highlight')
element.scrollIntoView({behavior: 'smooth', block: 'center'})
}
}
}
function parseString(string, delimiter = ';') {
return string
.split(delimiter)
.sort()
.map(s => s.trim())
.filter(Boolean)
}
function parseTable(table) {
const categoryList = {}
const elements = []
const tagList = new Set()
table.querySelectorAll('tbody tr').forEach(row => {
const rows = [...row.querySelectorAll('th, td')].map(td => td.textContent.replace(/\*+/g, '').trim())
if (rows[0].toLowerCase() !== 'autonomous custom elements') {
const name = rows[0].split(' ').pop()
const categories = parseString(rows[2])
tagList.add(name)
categories.forEach(category => {
if (categoryList[category] === undefined) {
categoryList[category] = []
}
categoryList[category].push(name)
})
elements.push({
attributes: parseString(rows[5].replace(/^globals;?/, '')),
categories,
children: parseString(rows[4]
.replace(/^empty/, '')
.replace(/ elements/, '')
.replace(/ one /, ''),
),
description: rows[1],
interface: rows[6],
name,
parents: parseString(rows[3]),
})
}
})
return {
categoryList,
elements,
tagList,
}
}
const fetchOptions = {
headers: {'Authorization': 'BybpTN5cK4JCREbh'},
}
const container = document.querySelector('[data-js="elements"]')
fetch('https://curse.potherca.workers.dev/https://html.spec.whatwg.org/multipage/indices.html', fetchOptions)
.then(response => response.ok ? response.text() : Promise.reject(response))
.then(text => new DOMParser().parseFromString(text, 'text/html'))
.then(document => document.querySelector('table'))
.then(parseTable)
.then(buildHtml)
.then(html => {
container.insertAdjacentHTML('beforeend', html.elements.join(''))
document.querySelector('[data-js="categories"]').insertAdjacentHTML('beforeend', html.categories.join(''))
window.addEventListener('hashchange', handleHashChange)
handleHashChange()
})
.catch(error => {
container.insertAdjacentHTML('afterbegin', `<li class="error">${error}</li>`)
console.error(error)
})
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment