Koyomi has two style sheets:

  • koyomi.css: the structure of the pickers (the field, the popover, the calendar, the time list, the recurrence editor). Every colour, font, space and radius comes from a --koyomi-* custom property. The defaults are Kotoba's, so that a page with both looks like one thing.
  • koyomi-sumi.css: the Sumi theme. Sumi is the design system of Hattori AI, the makers of Koyomi. The theme sets the --koyomi-* properties from the Sumi tokens, and it also works on a page that does not use Sumi.
/* assets/css/app.css */
@import "../../deps/koyomi/priv/static/koyomi.css";
@import "../../deps/koyomi/priv/static/koyomi-sumi.css"; /* optional */

The properties

The defaults of koyomi.css have no specificity (:where(:root)), so a value that you set on :root, on an ancestor or on .koyomi wins:

.koyomi {
  --koyomi-accent: #0f766e;
  --koyomi-radius: 0;
}

The popover is a child of the picker's element, also when the browser shows it in the top layer, so it takes the properties set on .koyomi or on an ancestor.

Type

PropertyDefaultUse
--koyomi-fontsystem-ui, …the text of the field and the popover
--koyomi-font-size1remthe text size
--koyomi-small0.875emsmall text (the zone, chips, the message, presets, notices)
--koyomi-line-height1.4the line height

Colours

PropertyDefaultUse
--koyomi-text#1f1f23the text colour
--koyomi-muted#6b6b76placeholders, weekday names, days outside the month
--koyomi-background#ffffffthe field
--koyomi-surface#f6f6f7hovered days and buttons, a read-only field
--koyomi-border#d9d9deborders
--koyomi-accent#2f5bd3the selected day, the ends of a range, focus
--koyomi-accent-text#fffffftext on the accent

Range and states

PropertyDefaultUse
--koyomi-rangethe accent at 12%the band behind the days of a range, and a selected week
--koyomi-range-previewthe accent at 8%the range drawn while the pointer moves, a hovered week
--koyomi-todaythe accenttoday's number and the line under it
--koyomi-holidaya red mixed with the text coloura holiday (its number is also underlined with dots)
--koyomi-weekendthe muted colourthe days of the weekend
--koyomi-disabledthe muted colour at 55%days, months, years and times that may not be chosen
--koyomi-invalida red mixed with the text colourthe border of an invalid field, the message under it

Tones

The tones of day content (:tone and :marks in Koyomi.Live):

PropertyDefault
--koyomi-tone-accentthe accent
--koyomi-tone-successa green mixed with the text colour
--koyomi-tone-warningan orange mixed with the text colour
--koyomi-tone-dangera red mixed with the text colour
--koyomi-tone-mutedthe muted colour

A day or a mark with a tone has data-tone, and sets --koyomi-tone to the tone's property; the label and the marks read --koyomi-tone.

Shape and space

PropertyDefaultUse
--koyomi-radius6pxcorners
--koyomi-space0.5remgaps
--koyomi-field-height2.5rem, 2.75rem on a touch screenthe height of the field
--koyomi-field-padding0 0.5rem 0 0.75remthe padding of the field
--koyomi-day-size2.25rem, 2.75rem on a touch screenthe width and height of a day
--koyomi-icon-size18pxicons; 14px in the clear button, 12px in a chip's remove button
--koyomi-focus-ringa 2px gap and a 2px accent ringthe box-shadow of focus
--koyomi-transition120ms ease-outhover and focus transitions, the popover's entrance

"A touch screen" is (pointer: coarse). The clear and chip buttons set their own --koyomi-icon-size, so a value set on .koyomi does not reach them: set it on .koyomi-clear or .koyomi-chip-remove.

Popover

PropertyDefaultUse
--koyomi-floating-backgroundthe backgroundthe popover
--koyomi-floating-textthe text colourthe text of the popover
--koyomi-floating-borderthe border colourits border
--koyomi-floating-shadow--koyomi-shadowits shadow
--koyomi-floating-radiusthe radius plus 2pxits corners (the top corners of a sheet)
--koyomi-shadowa soft shadowthe default of the floating shadow
--koyomi-backdropblack at 32%the backdrop behind a sheet

On a screen narrower than 40rem, the popover is a sheet at the bottom of the screen, with a backdrop.

Dark mode without Sumi

koyomi.css has one light palette. For a dark theme, set the colour properties in your own dark rule:

[data-theme="dark"] .koyomi,
.dark .koyomi {
  --koyomi-text: #e7e5e4;
  --koyomi-muted: #a8a29e;
  --koyomi-background: #1c1917;
  --koyomi-surface: #292524;
  --koyomi-border: #44403c;
  --koyomi-range: rgba(129, 161, 255, 0.18);
  --koyomi-range-preview: rgba(129, 161, 255, 0.1);
}

The defaults of --koyomi-range and --koyomi-range-preview are fixed colours, not mixed from the accent: set them again when you change the accent.

The Sumi theme

koyomi-sumi.css sets the properties on :root and on every element with a data-theme attribute. For each colour it reads, in this order:

  1. the Sumi tokens of the page: --font-body, --color-sumi-blade, --color-sumi-ember, --color-sumi-ink, --sumi-text-muted, and the colours of the Sumi daisyUI themes (--color-base-100, --color-base-200, --color-base-300, --color-base-content, --color-primary-content, --color-success, --color-warning, --color-error, --radius-box);
  2. the short names --sumi-font, --sumi-ink, --sumi-paper, --sumi-surface, --sumi-line, --sumi-muted, --sumi-accent, --sumi-accent-ink and --sumi-radius, for a page that sets them itself;
  3. a built-in Sumi palette (--koyomi-sumi-*), so that the pickers look like Sumi on a page with no Sumi tokens.

The main mappings:

PropertyReads
--koyomi-font--font-body, --sumi-font, then Outfit
--koyomi-text, --koyomi-floating-text, --koyomi-today--color-base-content, --sumi-ink
--koyomi-background, --koyomi-floating-background--color-base-100, --sumi-paper
--koyomi-surface--color-base-200, --sumi-surface
--koyomi-border, --koyomi-floating-border--color-base-300, --sumi-line
--koyomi-muted, --koyomi-weekend, --koyomi-tone-muted--sumi-text-muted, --sumi-muted
--koyomi-accent, --koyomi-tone-accent, the focus ring--color-sumi-blade, --sumi-accent, then #e84a1c
--koyomi-accent-text--color-primary-content, --sumi-accent-ink
--koyomi-range, --koyomi-range-previewthe blade at 14% and 8%
--koyomi-holiday--color-sumi-ember mixed with the text colour
--koyomi-invalid, --koyomi-tone-danger--color-error
--koyomi-tone-success--color-success
--koyomi-tone-warning--color-warning
--koyomi-shadow, --koyomi-floating-shadow, --koyomi-backdrop--color-sumi-ink
--koyomi-radius, --koyomi-floating-radius--radius-box, --sumi-radius, then 0.25rem

Today is marked in ink, not in the blade: a Sumi page keeps the blade for what needs attention, here the selection.

Sumi's themes

Each of Sumi's seven themes (Ink, Smoke, Stone and Lacquer; Paper, Snow and Silk) has its own accent, and the selection, the range, the focus ring and the accent tone follow it. The holiday is Sumi's ember, a red, and the tones are the status colours; where a theme's accent is of their hue, they take another in that theme, so that a holiday is not mistaken for the selection or the range, nor a tone for the accent:

ThemeAccentChanged
Smokejadethe holiday is Ink's ember; success is a leaf green; danger and an invalid value are lighter, for contrast on its page
Stonegoldwarning is orange, and the holiday a vermilion
Lacquervermiliondanger and an invalid value are a crimson
Snowindigothe holiday is Paper's ember
Silklotus pinkthe holiday is Paper's ember

The development app (mix dev) shows every page in each: ?sumi=smoke, or the links under its menu on ?theme=sumi.

Sizes and spaces have no Sumi token, so the theme gives them the Sumi values: --koyomi-small is 0.8125em and --koyomi-icon-size is 16px. The field height, the day size, the field padding and the transition keep the values of koyomi.css.

The theme sets the properties on :root with the weight of a class. To change one, set it on .koyomi or on an element inside the root, or on :root in a rule that comes after the theme.

Dark and light

On a page with the Sumi daisyUI themes, the pickers follow them, because their colours change with data-theme. Without them, the built-in palette is chosen:

  1. by the data-theme attribute: Sumi's dark themes ("dark", "smoke", "stone", "lacquer") give the dark palette, and any other value the light one;
  2. else, on the root element, by prefers-color-scheme: a dark preference gives the dark palette, unless the root has one of Sumi's light themes (data-theme="light", "snow" or "silk").

The palette and the mapping are resolved again on every element with a data-theme attribute, so a part of the page with its own theme gets it:

<section data-theme="dark">
  <.date_picker field={@form[:due]} />
</section>

In an app with daisyUI (a new Phoenix 1.8 app has it), the theme reads the app's own daisyUI colours (--color-base-*), with the Sumi accent. Koyomi does not load the Outfit font; load it yourself if you want it.

Import koyomi-sumi.css from app.css, after koyomi.css.

Icons

The pickers draw five icons from Lucide: the calendar button (calendar), the button of a time picker (clock) and of a recurrence picker (repeat), the clear button and a chip's remove button (close), and the previous and next buttons of the calendar (chevron). Each is an element with the class koyomi-icon and a data-icon attribute with its name, hidden from assistive technology; the button around it has the label. The icons are drawn in currentColor with a stroke of 2, at --koyomi-icon-size.

To use the icons of your app, call setIcons before the pickers mount, that is before liveSocket.connect() or mountKoyomi():

import { Koyomi, setIcons } from "koyomi"

setIcons({
  // The markup of one element, an <svg> as a rule.
  calendar: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">…</svg>`,
  // Or a function that makes a new element: here a Heroicon of Phoenix.
  close: () => Object.assign(document.createElement("span"), { className: "hero-x-mark" })
})

The names are chevron, calendar, clock, repeat and close; a name left out keeps Koyomi's icon, and null brings it back. The chevron points to the right, to the next month: Koyomi mirrors it for the previous month, and swaps the two in a right-to-left language.

Koyomi adds koyomi-icon, data-icon, aria-hidden="true" and a data-custom attribute to the element, and keeps its own classes. The size of --koyomi-icon-size applies to it, but not the fill and stroke of Koyomi's icons, so a filled icon stays filled.

Without JavaScript, an icon can be replaced in CSS by its data-icon, for instance with a mask:

.koyomi-icon[data-icon="calendar"] {
  stroke: none;
  background-color: currentColor;
  mask: url("/images/calendar.svg") center / contain no-repeat;
}
.koyomi-icon[data-icon="calendar"] > * { display: none; }

Classes

Every part of a picker has a koyomi-* class, for styles of your own. Most rules of koyomi.css have the weight of one or two classes, so a rule of your app with the same selector, loaded after it, replaces it.

The picker's element:

ClassOn
koyomievery picker
koyomi-date, koyomi-range, koyomi-dates, koyomi-month, koyomi-year, koyomi-week, koyomi-time, koyomi-datetime, koyomi-recurrence, koyomi-calendarthe kind of picker
koyomi-inlinea picker shown on the page, with no popover (calendar/1, or inline)
koyomi-readyonce the hook has built the picker; the browser's own inputs are hidden
koyomi-openwhile the popover is open
koyomi-has-valuewhile the picker has a value
koyomi-is-disabled, koyomi-is-readonlya disabled or read-only picker

The class attribute of a component adds classes to this element.

The parts:

ClassPart
koyomi-nativethe browser's own inputs, before the hook mounts
koyomi-controlthe field; with koyomi-invalid when it is invalid
koyomi-segments, koyomi-segment, koyomi-literalthe segments, and the separators between them; an empty segment has koyomi-placeholder
koyomi-range-dash, koyomi-zonethe dash of a range, the time zone of a date and time
koyomi-trigger, koyomi-clearthe calendar button, the clear button
koyomi-chips, koyomi-chip, koyomi-chip-removethe days of the several days picker
koyomi-summarythe rule of a recurrence picker, in words
koyomi-messagethe message under the field (a value that the rules refuse)
koyomi-popover, koyomi-sheet, koyomi-backdropthe popover, as a sheet, and the backdrop behind a sheet
koyomi-panel, koyomi-panel-body, koyomi-footerthe content of the popover, and its buttons (koyomi-action, koyomi-done)
koyomi-typing, koyomi-type, koyomi-type-previewthe typing box and its preview; koyomi-no-match when the text reads as no date
koyomi-presets, koyomi-presetthe presets
koyomi-noticea notice in the popover, such as a time that happens twice
koyomi-calendar, koyomi-header, koyomi-nav, koyomi-captionthe calendar, its header, the previous and next buttons, a month's name
koyomi-months, koyomi-month, koyomi-gridthe months shown, one month, its table
koyomi-day, koyomi-day-number, koyomi-day-label, koyomi-day-marks, koyomi-marka day's button and its content
koyomi-week-numbera week number
koyomi-month-grid, koyomi-year-grid, koyomi-cells-row, koyomi-cellthe months of a year and the years of a page
koyomi-times, koyomi-time, koyomi-times-emptythe time list, a time, the text of a day with no times
koyomi-recurrence, koyomi-weekdays, koyomi-weekday, koyomi-occurrencesthe recurrence editor, its weekday buttons, the next dates

The calendar has data-view (days, months or years), and the popover data-side (top or bottom).

The states of a day

The states are classes of the day's cell (the td), and the day's button is .koyomi-day inside it:

ClassThe day
koyomi-todayis today
koyomi-selectedis selected
koyomi-range-start, koyomi-range-endis the first or the last day of a range (a range of one day has both)
koyomi-range-middleis between the ends of a range
koyomi-range-previewis in the range drawn while the pointer moves
koyomi-unavailablemay not be chosen
koyomi-holidayis a holiday
koyomi-weekendis a day of the weekend of the locale
koyomi-outsidebelongs to the month before or after
koyomi-blankis an empty cell (a day of another month, not shown)

The button also has aria-disabled="true" when the day may not be chosen, aria-current="date" on today, and data-tone with its content's tone. In a week picker, a row has koyomi-week-row, and koyomi-week-selected or koyomi-week-hover. A month or a year cell is selected with koyomi-selected on its cell, and its button has aria-current="date" and aria-disabled="true". A time has aria-selected="true" when it is chosen, aria-disabled="true" when it may not be, and data-active when the arrow keys are on it.

For example, a day with a booked tone in green, and holidays in bold:

.koyomi-holiday .koyomi-day-number {
  font-weight: 600;
  text-decoration: none;
}

.koyomi-day[data-tone="success"] {
  background: color-mix(in oklab, var(--koyomi-tone-success) 10%, transparent);
}

Forced colours

In forced colours mode (Windows High Contrast), koyomi.css draws the states with the system colours:

  • a selected day, the ends of a range, a selected month or year, a selected time and a pressed weekday are drawn in Highlight and HighlightText;
  • the days between the ends of a range, and a range drawn while the pointer moves, have a Highlight line above and below, with no fill;
  • a day that may not be chosen is in GrayText, and keeps its line through;
  • a focused day or segment has a 2px Highlight outline.

Other colours, the tones and the marks among them, are the system's in that mode.

Motion

When the person asks for reduced motion (prefers-reduced-motion: reduce), the popover and the sheet appear with no animation, and the transitions of the pickers and the popover are off.