Skip to main content

Catalog · Site-wide

Color mode by time of day

Adds the time of day to the color mode: a reader picks a moment you declare, or drags a slider through the hours, and Auto stays the system's.

Fits best: any site — a site with a color mode control that wants a look for each part of the day

Example

The slider of the day as the bar builds it: alone, which is the default, and with every optional part (show="time place saved moments"). They are pictures: they are inert and no script runs on them, so they do not change this page. The working bar is on the home page, and the moments of this site are the ones its footer declares: the working bar picks one, while Auto stays the system's.

The slider alone

show="time place saved moments"

Without JavaScript there is no bar and the moments are not shown. Core's radios still change the mode.

Known issues

What to expect:

  • The script for the head ignores latitude and longitude: with a place, the element corrects the moment after the first paint, so it can change once near sunrise or sunset.
  • The moments are written twice: in the markup and in data-moments of the script for the head. Nothing checks that they agree, so change both together.
  • Without data-luz-label-scrub the slider has no name, and without data-luz-label-saved the note is silent: no English fallback.
  • Only the first <luz-theme-time> with moments owns the page.
  • It has not been tested with a screen reader.

Code

Copy the markup

Bundler
import "@luz-ds/catalog/components/theme-time.css";
HTML
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@luz-ds/catalog@0.3.0/dist/components/theme-time.min.css" integrity="sha384-/WCzE4qOwaMLwa1OiKNhEtfjsdMiAEvBH1CD0NyQC54KfWSvfCtlVtGxG9Womi9X" crossorigin="anonymous">

Wrap core's <luz-theme-mode> in <luz-theme-time> and add the moments of the day after it, one radio each, in the order of the day. Its value is the name of the moment; its label is what a reader sees.

HTML
<script type="module" src="https://cdn.jsdelivr.net/npm/@luz-ds/core@0.3.0/dist/luz.min.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@luz-ds/catalog@0.3.0/dist/catalog.min.js"></script>

<luz-theme-time>
  <luz-theme-mode>
    …the fieldset and the four radios of core's page…
  </luz-theme-mode>
  <fieldset class="luz-theme-time__moments">
    <legend class="luz-theme-time__legend">Time of day</legend>
    <label class="luz-theme-time__moment"><input class="luz-theme-time__input" type="radio" name="luz-theme-moment" value="dawn" data-from="05:00" data-theme="dark">Dawn</label>
    <label class="luz-theme-time__moment"><input class="luz-theme-time__input" type="radio" name="luz-theme-moment" value="morning" data-from="07:30" data-theme="light">Morning</label>
    <label class="luz-theme-time__moment"><input class="luz-theme-time__input" type="radio" name="luz-theme-moment" value="afternoon" data-from="12:00" data-theme="light" data-default>Afternoon</label>
    <label class="luz-theme-time__moment"><input class="luz-theme-time__input" type="radio" name="luz-theme-moment" value="sunset" data-from="17:30" data-theme="light">Sunset</label>
    <label class="luz-theme-time__moment"><input class="luz-theme-time__input" type="radio" name="luz-theme-moment" value="dusk" data-from="20:30" data-theme="dark">Dusk</label>
    <label class="luz-theme-time__moment"><input class="luz-theme-time__input" type="radio" name="luz-theme-moment" value="night" data-from="22:30" data-theme="dark" data-default>Night</label>
  </fieldset>
</luz-theme-time>

catalog.min.js loads theme-time.min.js when the browser is idle and the page has the element. To load it on its own, import @luz-ds/catalog/theme-time and link components/theme-time.css after core's.

Variants and options

A bar with a place, and the texts it needs:

HTML
<luz-theme-time variant="bar" show="time place saved" place="Madrid" latitude="40.42" longitude="-3.7"
                 data-luz-label-scrub="Hour of the day" data-luz-label-saved="Saved">
  <luz-theme-mode inline>
    …the fieldset and the four radios of core's page…
  </luz-theme-mode>
</luz-theme-time>
Class or attributeEffect
variant="bar"
Adds the slider of the 24 hours: by default the slider alone.
show
Words that add parts to the bar: time (the hour in large type), place (the place and the moment), saved (the note “Saved”; without it the note is still written, for a screen reader) and moments (the declared moments as options under the slider; choosing one pins its hour).
place
The name of the place for show="place"; a label, it does not set the sun.
latitude, longitude
Degrees north and east of the place whose sunrise and sunset set the turns of the day.
no-remember
A pinned hour is not stored under luz-light.
On a moment: value, data-from, data-theme, data-default
Its name (written to data-moment); where it starts, HH:MM by the visitor's clock; the base it is painted over, light or dark; and the one a chosen mode shows (light, the default light moment; dark and high contrast, the default dark one).
data-luz-label-scrub, data-luz-label-saved
On the element or an ancestor: the slider's name and the note “Saved”.

Properties, methods and event

NameWhat it does
el.hour, el.moments, el.moment, el.dayMoment, el.pinned, el.place
Read-only: the hour the slider shows (0 to 24: the pinned or previewed one, else the clock's); the declared moments, { name, from, theme, input }; the name of the moment the page shows; the name of the moment at that hour, shown or not; the pinned hour (null if none); the place attribute.
preview(hour), pin(hour), unpin()
The page shows the moment at that hour, and nothing is stored (preview(null) goes back); the page keeps the moment at that hour, stored under luz-light unless no-remember; lets go of the pinned hour, back to the system.
luz-theme-time:change
Bubbles after the upgrade and when the moment or the pinned hour changes. detail: moment and pinned (each null if none).

A method returns false when it does nothing: no moments, or an hour outside 0 to 24.

How it works

theme-time.js adds the moments of the day to <luz-theme-mode>, as a choice the reader makes. Core does the rest, and the button alone if the catalog does not load.

  • Auto is the system's, as in core: dark, high contrast, or light. The clock moves nothing. data-moment takes the default moment of that mode (data-default), and no moment radio is checked. The mode radio checked is the one in use, Light or Dark, and the button is named after it; nothing is stored, and the choice is still Auto. Auto itself is checked only while an hour or a moment is chosen. Auto does not follow the hour: a page that wants a moment sets it, with the slider, a moment option or pin(hour).
  • A moment the reader chose is the last one that has started at that hour (night runs through midnight): its name goes to data-moment, its radio is checked and the page takes its base. The visitor's location is never asked.
  • A mode the reader chose shows its default moment. High contrast always wins: data-theme is high-contrast, so a rule for "light" or "dark" does not match.
  • With a place, latitude and longitude give today's real sunrise and sunset, worked out in the page with no network, in place of the two turns of the day; if the sun does not rise or set there, the declared hours stand.
  • A pinned hour is the moment at that hour kept in Auto, stored under luz-light to five minutes; choosing a mode, or Auto, in <luz-theme-mode> lets it go, back to the system.
  • The slider (variant="bar"): dragging previews the hour; letting go pins and stores it and writes the note “Saved”. Its track is painted with --luz-theme-time-sky-<value>, one for each moment: Paint the track.
  • Two files: theme-time.min.js and theme-time-bar.min.js, which only variant="bar" loads. For a bar added after the scan, import @luz-ds/catalog/theme-time-bar and call initThemeTimeBar(root).

Without JavaScript

There is no bar and the moments are not shown. Core's radios still change the mode; Auto follows the system, and data-moment is not written.

Keyboard and ARIA

  • Slider: a native range in steps of 5 minutes: Tab reaches it, the arrow keys move one step, Home goes to 00:00 and End to 23:55. Each press previews the hour.
  • Escape on the slider lets go of a pinned hour, and then does not close a dialog around the bar.
  • Name and value: the name is data-luz-label-scrub; aria-valuetext is the moment and the hour, “Afternoon, 13:00”. When an hour is pinned a role="status" region says your data-luz-label-saved, shown or not.
  • Moments as options (show="moments") are radios in a fieldset: the arrow keys choose one. The mode button is core's: its keyboard and ARIA. With the system in use it steps on from the mode shown, so the first press never repeats it.

Script for the head

Optional, after core's. It writes the moment before the first paint. Copy node_modules/@luz-ds/catalog/dist/theme-time-boot.min.js to your own origin. The moments travel on its tag, as data-moments: the body is not read yet.

HTML
<head>
  <script src="/theme-boot.min.js"></script>
  <script src="/theme-time-boot.min.js"
          data-moments="dawn@05:00=dark morning@07:30=light afternoon@12:00=light* sunset@17:30=light dusk@20:30=dark night@22:30=dark*"></script>
  <link rel="stylesheet" href="…">
</head>
  • data-moments: value@HH:MM=base for each moment, in markup order; * marks data-default.
  • What it writes: data-moment on <html> and, for a pinned hour (luz-light), data-mode (and data-theme, with no brand) of that moment's base. A stored mode, or Auto with no pinned hour, shows the default moment of its mode; prefers-contrast: more wins. It never writes the storage. It blocks rendering as core's does, under the same exception (EX-012).

The six example moments

By the visitor's clock, as the markup above and the footer of this site declare them. Night runs through midnight, and data-moment on <html> takes the name.

MomentFromToBase
dawn
05:00
07:30
dark
morning
07:30
12:00
light
afternoon
12:00
17:30
light
sunset
17:30
20:30
light
dusk
20:30
22:30
dark
night
22:30
05:00
dark

Paint each moment

The colours are yours: set only the tokens that differ, over the base the moment names, as a theme of your own does, and measure each pair with the contrast check. Always write the base next to the moment: a rule with [data-moment] alone also matches high contrast and breaks its contrast.

CSS
/* Painted over the base the moment names: only the tokens that differ */
:root[data-theme="light"][data-moment="morning"] {
  --luz-surface-page: #fdf3dc;
  --luz-surface: #fffaf0;
}

:root[data-theme="dark"][data-moment="dawn"] {
  --luz-surface-page: #0f1520;
  --luz-surface: #172030;
}

/* With a brand of your own: */
:root[data-theme="my-brand"][data-mode="light"][data-moment="morning"] { … }

Paint the track

Set one property for each moment, once, on :root:

CSS
:root {
  --luz-theme-time-sky-night: #171b36;
  --luz-theme-time-sky-dawn: #6b3f5f;
  --luz-theme-time-sky-afternoon: #cfe1f6;
}
  • Without them, a bar gets a neutral ramp of tokens. It reads which properties are set when it is built; the colours stay variables, so a mode that changes them repaints the track.
  • High contrast: under :root[data-theme="high-contrast"] set every moment to var(--luz-text-primary): one solid fill that keeps its contrast. In forced colors the track is CanvasText. In right to left the day runs from the right.
PropertyEffect
--luz-theme-time-sky-<value>
Yours, on :root: the colour of a moment on the track.
--luz-theme-time-sky
Written by the bar: the gradient of the track. Set it for a track of your own.
--luz-theme-time-edge
The ring around the track, a box-shadow (default --luz-text-secondary); keep 3:1 against the page.

Contrast and focus are in the Design tab.

Design

When to use

Use it when a site has a look for each part of the day, or when readers should be able to try the light at another hour.

  • The moments alone, in a footer or a navbar, for a site that changes its look with the day.
  • The bar, where the light is part of the page, such as a home page.

When not to use

Do not use it when the site has one look in one mode: core's color mode is enough.

  • Do not promise the real sunrise without a place: the declared hours are fixed.
  • Do not use the bar as the only control: keep core's button.
  • Do not ask for the visitor's location: write the place yourself.

Do and don't

Do Name a moment for the light a reader sees: “Dawn”, “Sunset”, “Night”.
Don't Name a moment by its hours: they change with the place and the season, and the name is what a screen reader reads with the hour.

Writing the content

Call the moments by the light they show: “Dawn”, “Morning”, “Afternoon”, “Sunset”, “Dusk” and “Night”.

  • The labels are in the language of the page: the slider reads them with the hour.
  • Name the slider for what it does, “Hour of the day”, and the note “Saved”.
  • Say near the control what Auto follows: “Auto follows your system”, and that a moment is a choice.

Contrast and focus

The bar reads the semantic tokens of the page. The colours you give each moment are yours to measure, in the light and in the dark.

PartColour on backgroundLightDarkHigh contrastNeeds
Hour, in large type
--luz-text-primary on --luz-surface-page
15.8:1
15.1:1
21.0:1
4.5:1
Place and moment
--luz-text-primary on --luz-surface-page
15.8:1
15.1:1
21.0:1
4.5:1
Saved note
--luz-color-success on --luz-surface-page
5.3:1
12.5:1
9.3:1
4.5:1
Moment, hover
--luz-text-primary on --luz-surface-hover
14.4:1
12.6:1
20.1:1
4.5:1
Moment border
--luz-outline on --luz-surface-page
4.3:1
3.6:1
21.0:1
3:1
Checked moment
--luz-text-on-fill on --luz-brand-fill
6.0:1
4.9:1
14.1:1
4.5:1
Checked fill
--luz-brand-fill on --luz-surface-page
5.6:1
3.5:1
14.1:1
3:1
Slider track ring
--luz-text-secondary on --luz-surface-page
6.2:1
7.5:1
19.3:1
3:1
Slider thumb edge
--luz-brand-primary on --luz-surface
5.9:1
7.2:1
14.1:1
3:1
Focus ring
--luz-focus-ring on --luz-surface-page
5.6:1
7.7:1
21.0:1
3:1

Measured from the token values of each mode, rounded down. Needs is the WCAG 2.2 AA minimum: 4.5:1 for text, 3:1 for focus rings, borders and icons.

  • Focus: a 2 px outline, 2 px outside the slider or the moment, in --luz-focus-ring.
  • Track against the page: the ring uses --luz-text-secondary on --luz-surface-page, the pair in the table. Read on the pixels of the worst stretch of the track, which is a gradient and a soft edge the table cannot measure, the implementer found 3.48:1 in light, 4.63:1 in dark and 21:1 in high contrast.
  • Not colour alone: the track has a soft ring in --luz-text-secondary around its gradient, the hour is written as text, and by night the thumb is a crescent. In forced colors the track is CanvasText with a 1 px outline of the same colour, and the checked moment is a system highlight.
  • Target: the slider is 2.75rem high over a track 6 px high: the target is the slider, the track only draws.
  • Motion: the note fades in and out; there is no other motion.
  • Print: the element is not printed.

Tokens it uses

The 22 tokens that components/theme-time.css reads, by family. Each family links to its page, with the values in every mode. The button is core's: its tokens are on Color mode. In Figma they are variables with the same names: see Tokens in Figma.

Spacing and sizing
--luz-space-2xs, --luz-space-3xs, --luz-space-md, --luz-space-sm
Typography
--luz-text-3xl, --luz-text-sm
Radius
--luz-radius-full
Motion
--luz-duration-fast, --luz-duration-slow

Keyboard and ARIA are in the Code tab.