Skip to content

Latest commit

 

History

History
343 lines (270 loc) · 15.3 KB

README.md

File metadata and controls

343 lines (270 loc) · 15.3 KB

<theme-switch> a modal dialog web component

Switch your themes with style

theme-switch-light-dark-frog

About

<theme-switch> is a modal dialog which enables users to switch between themes. It is build as a web compoment with Lit 🔥 by using this this starter project.

Features

  • Configurable UI
  • a11y friendly keyboard navigation
  • Focus restoration
  • Event driven communication between host and component
theme-switch-2022-01-20_23.29.56.mp4

Installation

If you would like to use this component in your project, you can install it from npm

npm i theme-switch-component

Configuration

You can configure <theme-switch> by using the availableThemes attribute, slots and some CSS variables. Its events are this components way of communication.

Attribute availableThemes

Name Required Values Default Description
availableThemes No Any string or emoji.
"light", "🐸"
"auto", "light", "dark"

auto: use the current OS theme which is either a light or dark theme
light: usually light and friendly colors
dark: darker and more comfy shades to reduce eye strain (or to be cool)
These values
will be communicated
to anyone who listens
to the ThemeEvent

A selected theme can be saved if the corresponding setting has been set - the default is not setting anything. If you do decide to save the selected theme, two keys are set in your localStorage:

  • save-selection: true or false
  • theme-preference: a string value of the theme which is saved (e. g. frog-green-theme)

You can delete these by unchecking the option or by hand.

Slots

Slots allow you to define placeholders in your template that can be filled with any markup fragment you want when the element is used in the markup. They are identified by their name attribute. These placeholder are meant to be used to display a heading, a sub-heading and a "read more" link but you could place anything you would like to.

Name Meaning Example value
heading The "title" of the dialog <h2 slot="heading">Theme Selection</h2>
sub-heading Short explanation <span slot="sub-heading">Choose a theme for your site</span>
read-more Additional information <a slot="read-more" href="/privacy-statement" target="_blank" title="What data will be saved?">?</a>
close-caption Close button caption <span slot="close-caption">Schließen</span>

Slot Examples

Using the default values

<theme-switch>
  <h2 slot="heading">Theme Selection</h2>
  <span slot="sub-heading">Choose a theme for your site</span>
  <a
    slot="read-more"
    href="/privacy-statement"
    id="read-more"
    target="_blank"
    title="What data will be saved?"
  >
    ? 
  </a>
  <span slot="close-caption">Close</span>
</theme-switch>

Using your own set of themes

<theme-switch availableThemes='["🐢", "🦕", "🐸"]'>
  <!-- Slots as above -->
</theme-switch>

Events

Events are used to open the dialog and to restore focus after closing it again. This components also communicates the selected theme to anything listening to this event.

DialogEvent

Used to open the dialog and restore focus on the opener element when closing it again.

class DialogEvent extends Event {
  static eventName = 'dialog-event';
  targetElement = '';

  constructor(targetElement) {
    super(DialogEvent.eventName, { bubbles: true });
    this.targetElement = targetElement;
  }
}

Define an EventListener to dispatch the event

document
  .querySelector('#btn-theme-selection')
  .addEventListener('click', (event) => {
    const { target } = event;
    window.dispatchEvent(new DialogEvent(target));
  });

ThemeEvent

Transports the name of a theme so that a host can react to it accordingly. For example one could use it to set attributes or CSS classes.

window.addEventListener('theme-event', (themeEvent) => {
  const { themeName } = themeEvent;

  document.documentElement.setAttribute('theme-preference', themeName);
});

CSS variables

Variable Purpose Default value
--base-gap Spacing for paddings, margins & gaps 8px
--base-radius Border radius for different elements 8px
--blur-amount Amount for blurring the dialog backdrop 5px
--backdrop-color Color of the dialog backdrop hsla(0, 0, 78%, 0.1)
--text-color-1 Text color for the heading, controls & labels --purple-950: #2f0050
--text-color-2 Text color for sub-heading --purple-900: #581c87
--outline-color Outline color for :focus #000
Dialog
--dialog-bg-color Dialog background color --purple-50: #faf5ff
--dialog-border-color Dialog border color --purple-500: #a855f7
Radio buttons
--themes-border-color Themes wrapper border color --purple-400: #c084fc
--circle-bg-color Radio button background color --purple-100: #f3e8ff
--circle-bg-color-checked Radio button background color when checked --purple-300: #d8b4fe
--circle-border-color Radio button border color --purple-500: #a855f7
Control elements
--control-color Color for control elements (buttons, links) --purple-300: #d8b4fe
--control-interaction-color ^when using :hover or :focus --purple-400: #c084fc
Checkbox
--checkbox-bg-color Checkbox background color --purple-50: #faf5ff
--checkbox-bg-color-checked Checkbox background color when checked --purple-200: #e9d5ff
--checkbox-border-color Checkbox border color --purple-500: #a855f7
--checkmark-color Checkmark color --purple-900: #581c87

The default set of colors comes from Tailwind's color palettes.

Customizing the UI using CSS variables

The overall styling of my personal website and this component is based on this awesome article (Building a color scheme) by Adam Argyle. The basic idea is that you have to define a set of CSS variables for different kinds of surfaces, colors and shadows and swap them with another set when a condition is met. This could be your device changing its theme or you using this component.

  1. Define a set of CSS variables for each theme:
* {
  /* Light theme */
  --surface-1-light: hsla(281deg 55% 55% / 100%);
  --surface-2-light: hsla(281deg 55% 74% / 100%);
  /* Dark theme */
  --surface-1-dark: hsla(281deg 56% 10% / 100%);
  --surface-2-dark: hsla(281deg 60% 15% / 100%);
}
  1. Then define a set of CSS variables for your UI and its default state (this could be your light theme). Using an attribute selector like I did is one way when changing the theme but you are free to use a class instead - see ThemeEvent for more details.
:root,
:root[theme-preference='light'] {
    color-scheme: light;
    --surface-1: var(--surface-1-light);
    --surface-2: var(--surface-2-light);
}
  1. And another one when your theme should change to a dark theme
@media (prefers-color-scheme: dark) {
  :root {
      color-scheme: dark;
      --surface-1: var(--surface-1-dark);
      --surface-2: var(--surface-2-dark);
  }
}
/* ^ a media query for a light color scheme is not needed as it is the default already */

:root[theme-preference='dark'] {
  color-scheme: dark;
  --surface-1: var(--surface-1-dark);
  --surface-2: var(--surface-2-dark);
}
  1. Apply these variables to this component if you'd like to
:root[theme-preference='dark'] theme-switch {
  --text-color-1: #fff;
  --text-color-2: #fff;
  /* Dialog */
  --dialog-bg-color: var(--surface-5);
  --dialog-border-color: var(--surface-2);
  /* Radio Buttons */
  --circle-bg-color: #2a1d5445;
  --circle-bg-color-checked: var(--surface-3-dark);
  --circle-border-color: #eedcf530;
  /* Control elements */
  --control-color: var(--surface-3);
  --control-interaction-color: var(--surface-2);
}
  1. Don't forget to load a saved theme as soon as possible to avoid page flickering:
<!DOCTYPE html>

<html>
  <head>
    <!-- ... -->
    <script>
      // Reade theme preference from localStorage
      const themePreference = localStorage.getItem('theme-preference');
      // Set theme if present in localStorage
      if (themePreference !== null) {
        document
          .querySelector('html')
          .setAttribute('theme-preference', themePreference);
      }
    </script>
  </head>
  <!-- ... -->
</html>

Setup

Install dependencies:

npm i

Build

This project uses the TypeScript compiler to produce JavaScript that runs in modern browsers.

To build the JavaScript version of this component youd would need to run:

npm run build

To watch files and rebuild when the files are modified, run the following command in a separate shell:

npm run build:watch

Both the TypeScript compiler and lit-analyzer are configured to be very strict. You may want to change tsconfig.json to make them less strict.

Dev Server

<theme-switch> uses modern-web.dev's @web/dev-server for previewing the project without additional build steps. Web Dev Server handles resolving Node-style "bare" import specifiers, which aren't supported in browsers. It also automatically transpiles JavaScript and adds polyfills to support older browsers. See modern-web.dev's Web Dev Server documentation for more information.

To run the dev server and open the project in a new browser tab:

npm run serve

There is a development HTML file located at /dev/index.html that you can view at http://localhost:8000/dev/index.html. Note that this command will serve your code using Lit's development mode (with more verbose errors). To serve your code against Lit's production mode, use npm run serve:prod.

Editing

If you use VS Code, it is highly recommend to have the lit-plugin extension installed, which enables some extremely useful features for lit-html templates:

  • Syntax highlighting
  • Type-checking
  • Code completion
  • Hover-over docs
  • Jump to definition
  • Linting
  • Quick Fixes

The project is setup to recommend lit-plugin to VS Code users if they don't already have it installed.

Linting

Linting of TypeScript files is provided by ESLint and TypeScript ESLint. In addition, lit-analyzer is used to type-check and lint lit-html templates with the same engine and rules as lit-plugin.

The rules are mostly the recommended rules from each project, but some have been turned off to make LitElement usage easier. The recommended rules are pretty strict, so you may want to relax them by editing .eslintrc.json and tsconfig.json.

To lint the project run:

npm run lint

Formatting

Prettier is used for code formatting. It has been pre-configured according to the Lit's style. You can change this in .prettierrc.json.

Prettier has not been configured to run when committing files, but this can be added with Husky and and pretty-quick. See the prettier.io site for instructions.

To format the project run:

npm run format

Static Site

This project includes a simple website generated with the eleventy static site generator and the templates and pages in /docs-src. The site is generated to /docs and intended to be checked in so that GitHub pages can serve the site from /docs on the main branch.

To enable the site go to the GitHub settings and change the GitHub Pages "Source" setting to "main branch /docs folder".

To build the site, run:

npm run docs

To serve the site locally, run:

npm run docs:serve

To watch the site files, and re-build automatically, run:

npm run docs:watch

The site will usually be served at http://localhost:8000.

Bundling and minification

This component doesn't include any build-time optimizations like bundling or minification. It is recommended to publish components as unoptimized JavaScript modules, and performing build-time optimizations at the application level. This gives build tools the best chance to deduplicate code, remove dead code, and so on.

For information on building application projects that include LitElement components, see Build for production on the Lit site or at the open web components site.

Useful resources