HTML-First UI

HTML First UI Components (HF-UI)

A set of HTML First web components for use in any Framework or Language.

⚠️ Work in Progress ⚠️

Goals

HF-UI components work especially well with Enhance . The initial work was inspired by MDash . Icons used are from Phosphor . See Acknowledgements for other related projects and resources.

Getting Started

For an Enhance project the quickest way to try out the components in your project is:

  1. Install the components
  1. Copy the base css file to your public folder
  1. Add the css reference to your head.mjs
  2. Add a component or all the components to your app elements folder ( usage )

Usage

Add all components to an Enhance project

The ssr-elements can be added to an Enhance project with the element.mjs file.
Add the following /app/element.mjs file to your app directory:

// /app/elements.mjs
import { elements } from '@html/ui' 
export default elements

Use individual components in Enhance

Individual can be used as follows:

// /app/elements/hf-button.mjs
import { hfButton } from '@htmlfirst/ui/dist/elements' 
export default hfButton

Server-side and Client-side Rendering

HF-UI components can be rendered on the server or on the client. If a component is server side rendered in a page it inserts the needed styles and behavior(JavaScript). If that same component is later client-side rendered. If the component has not been server-side rendered first the component definition needs to be sent with the page as a script. That script will make sure the styles are inserted on the client.

There are three formats for each component in the /dist folder.

Client side (self rendering)

When client side rendered the HF-UI components render themselves. When the components is first defined it will add its styles to the document head. This only happens once when the component is defined so that if the same component was already server side rendered the styles do not get added twice. Then when that component is added to the DOM at any point it will render its own contents inside the connectedCallback .

Since these components do not use the Shadow DOM the content does not use live slots. The content that is present when the component is connected to the DOM is expanded/rendered. In practice this is usually not a problem. The components are built to repond to attribute changes as needed. The only common edge case where this needs to be considered is when setting innerHTML on an element. It is somewhat common to const thing = document.createElement('my-thing') and then set thing.innerHTML = "<div>Something</div> . Here thing will render the contents before adding the content. There are many ways around this but one options is in the second step to set thing.outerHTML = <my-thing><div>Something</div></my-thing> . This way the component will be added with the content already in place.

To use a CSR component in the browser with an Enhance project it can be copied directly to the public folder. They are dependency free so no build step is required. Just import them from the page as <script type=module src="/_public/hf-button.mjs"></script> .

If you want to use multiple components and combine them into one script you can use the app/browser folder to bundle them into the public/browser folder as follows:

// app/browser/my-page.mjs
import {hfButton,hfCard} from '@htmlfirst/ui/components.js'
<!-- My Page -->
 <body>
   <div>stuff</div>
   <script type=module src="/_public/browser/my-page.mjs"></script>
  </body>

Customize with Custom Properties

The components can be customized by setting values on a set of custom properties. This gives some flexibility without being too overwhelming. For an Enhance project copy the public/hf-custom-properties.css file into your projects public folder. Or if using another framework put the file wherever static assets are served from. Most of the properties are colors with a few sizes for fonts and spacing.

Use with other style libraries

The HF-UI components can be used with other style systems. They work especially well alongside utility style systems like Paramour (formerly called Enhance styles). They can be used with Tailwind or plain old CSS.

Use with other frameworks or languages

These components can be used with almost any framework that is HTML friendly. Meaning if HTML tags are respected and passed around without being turned into some JavaScript representation you should be able to use it. Some examples are included in the accompanying Github repository and others are shown in the Github Enhance-Dev repositories.

Some supported frameworks and languages

Color Themes (Light and Dark Mode)

The components are built with theming in mind. By choosing appropriate colors and adjusting custom properties a working light and dark can be applied. The following recommendations show options for choosing a theme based on OS default or user selection. This preference can be persisted through local storage.

The primary way that components adapt to a light and dark them is by use of the scale of Gray (or other neutral) color.
The user configuration requires custom property values for the neutral color from --hf-color-gray-0 to --hf-color-gray-10 .
For light mode this range should be set with light on the low end and dark on the high end.
For dark mode the range is reversed.
Other changes to the primary color and other supporting colors may need to be made depending on the theme.

  /* Light Theme */
  --hf-color-gray-0:  hsl(0, 0%, 95%);
  --hf-color-gray-1:  hsl(0, 0%, 90%);
  --hf-color-gray-2:  hsl(0, 0%, 80%);
  --hf-color-gray-3:  hsl(0, 0%, 70%);
  --hf-color-gray-4:  hsl(0, 0%, 60%);
  --hf-color-gray-5: hsl(0, 0%, 50%);
  --hf-color-gray-6: hsl(0, 0%, 40%);
  --hf-color-gray-7: hsl(0, 0%, 30%);
  --hf-color-gray-8: hsl(0, 0%, 20%);
  --hf-color-gray-9: hsl(0, 0%, 10%);
  --hf-color-gray-10: hsl(0, 0%, 7%); 


  /* Dark Theme */
  --hf-color-gray-0: hsl(0, 0%, 7%); 
  --hf-color-gray-1: hsl(0, 0%, 10%);
  --hf-color-gray-2: hsl(0, 0%, 20%);
  --hf-color-gray-3: hsl(0, 0%, 30%);
  --hf-color-gray-4: hsl(0, 0%, 40%);
  --hf-color-gray-5: hsl(0, 0%, 50%);
  --hf-color-gray-6:  hsl(0, 0%, 60%);
  --hf-color-gray-7:  hsl(0, 0%, 70%);
  --hf-color-gray-8:  hsl(0, 0%, 80%);
  --hf-color-gray-9:  hsl(0, 0%, 90%);
  --hf-color-gray-10:  hsl(0, 0%, 95%);

These custom properties can be applied at an application level in many possible ways.
The following is a suggested approach that allows for operating system defaults to be used and overridden by if desired.

To persist the users preference while avoiding a flash of the wrong color theme as the site is initially loaded requires some effort.
This example persists the user preference in local storage.
Because it uses JavaScript it is a progressive enhancement.
If JavaScript fails to load the fallback default theme is still a working theme.

To avoid the flash of the wrong theme the following script should be in the docuemnt head, or somewhere before any of the body begins to load.
This checks local storage for a theme preference and applies a class to the document ( <html> ) element.
This local storage preference is set by the theme picker show further down.
This script is intentionally minimal to make sure if a preference is stored it is used before any content is shown.

<script>
  const darkLightTheme = window.localStorage.getItem('dark-light-theme');
  if (darkLightTheme === 'dark') { document.documentElement.classList.add('dark-mode'); } 
</script>
/* If theme class is set for Dark it has high priority by the specificity */
:root.dark-mode:not(#id-for-high-specificity) {
      /* Dark Theme */
      --hf-color-gray-0:  hsl(0, 0%, 7%);
}

/* If no theme class is set on the document the theme toggle 
   can be used to set theme or leave the theme as auto       */
:root:has(input[name=color-scheme][value=dark]:checked) {
  #color-scheme::after{ 
    content:'Dark Mode';
  }
      /* Dark Theme */
      --hf-color-gray-0: hsl(0, 0%, 7%); 
}
:root:has(input[name=color-scheme][value=light]:checked) {
  #color-scheme::after{ 
    content:'Light Mode';
  }
  /* Light Theme */
  --hf-color-gray-0:  hsl(0, 0%, 95%);
}
/* If the theme switcher auto is set it no specific theme 
   values are set allowing the default values to take over */
:root:has(input[name=color-scheme][value=auto]:checked) {
  #color-scheme::after{ 
    content:'Auto Mode';
  }
}

/* Default theme values are set for when nothing is chosen */
:root {
  /* Colors */
  color-scheme: light dark;

  /* Default Light Theme */
  --hf-color-gray-0:  hsl(0, 0%, 95%);
}

/* A media query handles the OS default theme if set */
  @media ( prefers-color-scheme: dark ) {
    :root {
      /* Dark Theme */
      --hf-color-gray-0: hsl(0, 0%, 7%); 
    }
  }

How It Works

For SSR render HTML

<!-- hf-blockquote in HTML form -->
<style> hf-blockquote { color: blue; } </style>
<blockquote><slot></slot></blockquote>
<script type=module>
  class HfBlockquote extends HTMLElement{
    constructor(){ super(); }
    connectedCallback(){ }
  }
  if (!customElements.get("hf-blockquote")){ customElements.define('hf-blockquote',HfBlockquote) }
</script>

For SSR followed by CSR

<!-- hf-blockquote in HTML form -->
<style> hf-blockquote { color: blue; } </style>
<blockquote><slot></slot></blockquote>
<script type=module>
  class HfBlockquote extends HTMLElement{
    constructor(){ super(); }
    connectedCallback(){ this.clientRender(); }
    clientRender() {
      const isEnhanced = this.hasAttribute("enhanced")
      if (!isEnhanced){
        // Render on the client as needed
        this.setAttribute("enhanced","client")
      }
    }

  }
  if (!customElements.get("hf-blockquote")){ customElements.define('hf-blockquote',HfBlockquote) }
</script>

For CSR without SSR

<script type=module>
  class HfBlockquote extends HTMLElement{
    constructor(){ super(); }
    connectedCallback(){ this.clientRender(); }
    clientRender() {
      const isEnhanced = this.hasAttribute("enhanced")
      if (!isEnhanced){
        // Render on the client as needed
        this.setAttribute("enhanced","client")
      }
    }

    

  }
  if (!customElements.get("hf-blockquote")){ 
    // Insert style into head
    const style = document.createElement('style');
    style.textContent = 'hf-blockquote { color: blue; }'
    document.head.appendChild(style);

    customElements.define('hf-blockquote',HfBlockquote) 
  }
</script>

Note: For serverside rendered and clientside rendered components that otherwise would not need a custom element defined we can use it as a way to make sure the styles only get added once. If it is serverside rendered it will register the custom element and if the clientside component is added it will not run again because the custom element definition has already run.