Form
Date Range Field
A start and an end date, each on the native date input, linked so the end is never before the start.
- Element
<slab-date-range-field>- Built on
<input type=date>- As Markdown
- date-range-field.md
Example
Here's a styled version, <slab-styled-date-range-field>. It's not
in the package — it's an example of what you can build with the
headless element. Copy it and make it yours at the
bottom of this page. Switch to "As it ships" to see the same markup
with the headless element and no CSS at all. That's your starting
point.
<slab-styled-date-range-field label="Pour window" start-label="Earliest" end-label="Latest" min="2026-10-12" start-value="2026-10-16" end-value="2026-10-20" required>
<span slot="description">The crew needs at least one dry day in the window.</span>
</slab-styled-date-range-field>
<slab-date-range-field label="Pour window" start-label="Earliest" end-label="Latest" min="2026-10-12" start-value="2026-10-16" end-value="2026-10-20" required>
<span slot="description">The crew needs at least one dry day in the window.</span>
</slab-date-range-field>
Usage
pnpm add slab-elements lit
import 'slab-elements/date-range-field.js';
export function MyDateRangeField() {
return (
<slab-date-range-field label="Stay" name="stay" start-label="Check-in" end-label="Check-out" required></slab-date-range-field>
);
}
MyDateRangeField.jsx React 19 or later, which sets props on custom elements as properties.
import 'slab-elements/date-range-field.js';
import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('my-date-range-field')
export class MyDateRangeField extends LitElement {
render() {
return html`
<slab-date-range-field label="Stay" name="stay" start-label="Check-in" end-label="Check-out" required></slab-date-range-field>
`;
}
}
my-date-range-field.ts Slab is built on Lit, so its elements are at home in Lit's templates.
<script setup>
import 'slab-elements/date-range-field.js';
</script>
<template>
<slab-date-range-field label="Stay" name="stay" start-label="Check-in" end-label="Check-out" required></slab-date-range-field>
</template>
MyDateRangeField.vue Let Vue's compiler know slab- tags are custom elements with isCustomElement.
<script>
import 'slab-elements/date-range-field.js';
</script>
<slab-date-range-field label="Stay" name="stay" start-label="Check-in" end-label="Check-out" required></slab-date-range-field>
MyDateRangeField.svelte Svelte binds properties and events to custom elements like any other element.
<script type="module">
import 'slab-elements/date-range-field.js';
</script>
<slab-date-range-field label="Stay" name="stay" start-label="Check-in" end-label="Check-out" required></slab-date-range-field>
index.html Any bundler (or an import map) takes care of the imports.
Each input’s bound is the other’s value, as well as min and max, so a browser’s picker won’t offer an end before the start. An end typed before the start isn’t changed to fit: it stays, and the field is rangeUnderflow and rangeOverflow, as a date outside min and max is on a single input. The form gets two entries under name, the start and then the end, even when one is empty.
The group is named by label, and each input by it and then its own label, “Start” and “End” unless you set start-label and end-label, so a screenreader reads “Stay Check-in”. Lay the inputs out with ::part(fields). For a calendar instead of the browser’s picker, Calendar with range picks the same thing, as “yyyy-mm-dd/yyyy-mm-dd”.
Keyboard
| Key | What it does |
|---|---|
| Tab, Shift+Tab | Moves from the start to the end, and out of the field. Firefox stops at each part of a date first. |
| Enter | Submits the form. |
| Anything else | What it does on a native date input, where the browser edits the date. |
Behavior
A date range field with no visual styles: two native <input type="date">s, a start and an end, for picking the first and last days of a range, such as a stay or a reporting period. Each is typed into, picked from, and read the way one is, with the browser's own picker. A calendar of Slab's making is slab-calendar's job, and its range picks the same thing.
The values are the inputs': ISO "yyyy-mm-dd" strings, or empty ones, as on a Date Field. What a person sees is formatted for their locale by the browser, but what's submitted and read is always that one format.
The two are linked rather than kept apart: the start's latest date is the end, and the end's earliest is the start, as well as max and min. So a browser's picker won't offer an end before the start, and an end typed before it is rangeUnderflow on the end and rangeOverflow on the start, just as a single input reports a date outside min and max. Neither value is changed to fit the other: what's typed stays, and it's invalid.
The group is named by label, and each input by it and its own label, so a screenreader reads "Stay Start" and "Stay End". The host delegates focus, so clicking anywhere on it, or a <label for> pointing at it, focuses the start.
Validity is the native inputs'. required, min and max are passed through to both, and whatever either decides is mirrored onto the host, so valueMissing, rangeUnderflow, rangeOverflow and badInput all mean what they mean on an <input>. Nothing is computed twice.
Style it through its parts and through :state(disabled), :state(error), :state(readonly) and :state(required). Each input is ::part(input) as well as ::part(start) or ::part(end), and each of their labels ::part(input-label) as well as ::part(start-label) or ::part(end-label), so both can be styled at once. Whether the inputs sit side by side or one above the other is layout, so it's left to the styles around it.
API
<slab-date-range-field>
| Name | Type | Default | Description |
|---|---|---|---|
label
Required
|
string |
Names the group. Each input is named by it and its own label. | |
disabled
|
boolean |
false |
|
end-label
|
string |
The end input's label, read after label. "End" when unset. |
|
end-value
|
string |
'' |
The last day of the range, as "yyyy-mm-dd". Like a native input's value, the attribute is the default and the property is the current value. |
hide-label
|
boolean |
false |
|
max
|
string |
The latest date allowed for either, as "yyyy-mm-dd". | |
min
|
string |
The earliest date allowed for either, as "yyyy-mm-dd". | |
name
|
string |
'' |
Submitted twice, with the start value and then the end value. |
readonly
|
boolean |
false |
|
required
|
boolean |
false |
Requires both dates. |
start-label
|
string |
The start input's label, read after label. "Start" when unset. |
|
start-value
|
string |
'' |
The first day of the range, as "yyyy-mm-dd". Like a native input's value, the attribute is the default and the property is the current value. |
version
|
string |
| Name | Type | Description |
|---|---|---|
form
Read-only
|
HTMLFormElement | null |
|
validity
Read-only
|
ValidityState |
| Signature | Description |
|---|---|
checkValidity(): boolean
|
|
focus(options?: FocusOptions): void
|
|
reportValidity(): boolean
|
|
resetValidityFeedback(): void
|
Clears the validity feedback without changing validity. |
setCustomValidity(message: string): void
|
|
setValidity(flags?: ValidityStateFlags, message?: string): void
|
| Name | Type | Description |
|---|---|---|
input
|
Event |
|
change
|
Event |
| Name | Type | Description |
|---|---|---|
description
|
Element | string |
| Name | Description |
|---|---|
container
|
The label, the inputs and the description |
description
|
Text about the field, and its validity message when it has one |
end
|
|
end-field
|
The end input and its label |
end-label
|
The end input's label |
field
|
The start input and its label |
fields
|
Both inputs and their labels |
input
|
|
input-label
|
The start input's label |
label
|
The label |
start
|
|
start-field
|
The start input and its label |
start-label
|
The start input's label |
validity-message
|
Copy and own it
<slab-styled-date-range-field> isn't in the package. It's an example —
copy it into your project and change whatever you want. Keyboard,
focus, ARIA, forms, and events all come from the headless element,
and the styles only reach in through ::part() and
:state(), same as yours would.
The same styles, written from outside the element, so
there's nothing to subclass. The CSS styles every
<slab-date-range-field> on the page. Import
tokens.css, the colors and fonts every styled
example shares, once, wherever your global styles go.
Each component registers its headless element and imports its CSS. Use it like the element: its props are the element's attributes and properties.
DateRangeField.jsx
import 'slab-elements/date-range-field.js';
import './date-range-field.css';
export function DateRangeField(props) {
return <slab-date-range-field {...props} />;
}
date-range-field.css
slab-date-range-field {
color: var(--ink);
display: grid;
font-family: var(--font-body);
}
slab-date-range-field::part(label) {
font-family: var(--font-display);
font-size: 1.0625rem;
font-weight: 600;
margin-block-end: 0.375rem;
}
slab-date-range-field:state(required)::part(label)::after {
color: var(--ember-deep);
content: ' *' / '';
}
slab-date-range-field::part(input),
slab-date-range-field::part(textarea) {
background-color: var(--paper);
block-size: 3rem;
border: var(--border);
border-radius: 0;
box-sizing: border-box;
color: inherit;
font: inherit;
/* A fixed height, shared with Select, so fields line up in a row. */
padding: 0 0.75rem;
transition:
background-color 100ms,
border-color 100ms,
box-shadow 100ms,
translate 100ms;
}
slab-date-range-field::part(input)::placeholder,
slab-date-range-field::part(textarea)::placeholder {
color: var(--ash);
}
/*
The outline is transparent rather than gone, so it still shows in forced
colors, where box shadows don't.
*/
slab-date-range-field::part(input):focus-visible,
slab-date-range-field::part(textarea):focus-visible {
box-shadow: 4px 4px 0 var(--ember);
outline: 2px solid transparent;
translate: -2px -2px;
}
slab-date-range-field::part(input):disabled,
slab-date-range-field::part(textarea):disabled {
background-color: var(--chalk);
border-style: dashed;
color: var(--ash);
cursor: not-allowed;
}
slab-date-range-field::part(textarea) {
block-size: auto;
min-block-size: 7rem;
padding: 0.625rem 0.75rem;
resize: vertical;
}
slab-date-range-field:state(readonly)::part(input),
slab-date-range-field:state(readonly)::part(textarea) {
background-color: var(--chalk);
}
slab-date-range-field:state(error)::part(input),
slab-date-range-field:state(error)::part(textarea) {
border-color: var(--ember-deep);
}
/*
Spaced by what's in it rather than by a gap, so a description with
nothing in it takes no room, and a control beside the field lines up
with its bottom edge.
*/
slab-date-range-field::part(description) {
color: var(--ash);
display: grid;
font-size: 0.875rem;
}
slab-date-range-field > [slot='description'] {
margin-block-start: 0.375rem;
}
slab-date-range-field::part(validity-message) {
color: var(--ember-deep);
font-weight: 600;
margin-block-start: 0.375rem;
}
slab-date-range-field::part(validity-message)::before {
content: '■ ' / '';
}
@media (prefers-reduced-motion: reduce) {
slab-date-range-field::part(input),
slab-date-range-field::part(textarea) {
transition: none;
}
}
slab-date-range-field::part(fields) {
display: grid;
gap: 0.75rem;
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
}
slab-date-range-field::part(field) {
display: grid;
gap: 0.25rem;
}
slab-date-range-field::part(input-label) {
color: var(--ash);
font-size: 0.8125rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
tokens.css
:root {
/* Secondary text. 6.8:1 and 6.9:1 on paper. */
--ash: var(--slab-styled-ash, light-dark(#5e5a55, #a8a19a));
--border: 2px solid var(--ink);
/* Behind disabled and read-only controls, and hovered rows. */
--chalk: var(--slab-styled-chalk, light-dark(#f4f1ec, #2b2926));
/*
The logo's orange, brighter in the dark. 4.25:1 and 5.2:1 on paper is
enough for borders, fills and large text, but not always for small text,
which takes "--ember-deep".
*/
--ember: var(--slab-styled-ember, light-dark(#d9492a, #ec5f3b));
/*
Orange for small text on paper, and for paper text on it. 5.7:1 and
7.4:1.
*/
--ember-deep: var(--slab-styled-ember-deep, light-dark(#b3381a, #ff8566));
--font-body: var(
--slab-styled-font-body,
'IBM Plex Sans',
system-ui,
sans-serif
);
--font-display: var(
--slab-styled-font-display,
'Zilla Slab',
rockwell,
'Roboto Slab',
serif
);
--font-mono: var(
--slab-styled-font-mono,
'IBM Plex Mono',
ui-monospace,
monospace
);
--ink: var(--slab-styled-ink, light-dark(#1c1c1c, #f4f1ec));
--paper: var(--slab-styled-paper, light-dark(#fff, #1a1918));
/* The hard, offset shadow a raised surface casts. */
--shadow: 4px 4px 0 var(--ink);
}
Under the hood, it's just the headless element, subclassed,
with styles set. That's it. To use it, copy
these files, swap the tag in @customElement()
for one of your own, and import it.
tokens.styles.ts holds the colors and fonts
every styled example shares, so you only need it once.
date-range-field.ts
import { customElement } from 'lit/decorators.js';
import DateRangeField from 'slab-elements/date-range-field.js';
import styles from './date-range-field.styles.js';
import textInput from './text-input.styles.js';
import tokens from './tokens.styles.js';
declare global {
interface HTMLElementTagNameMap {
'slab-styled-date-range-field': StyledDateRangeField;
}
}
/**
* Date Range Field, styled: two of Date Field's inputs side by side, each
* with a small label of its own, wrapping one above the other when there's
* no room.
*/
@customElement('slab-styled-date-range-field')
export default class StyledDateRangeField extends DateRangeField {
static override styles = [tokens, textInput, styles];
}
date-range-field.styles.ts
import { css } from 'lit';
export default css`
[part~='fields'] {
display: grid;
gap: 0.75rem;
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
}
[part~='field'] {
display: grid;
gap: 0.25rem;
}
[part~='input-label'] {
color: var(--ash);
font-size: 0.8125rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
`;
text-input.styles.ts
import { css } from 'lit';
/*
The look shared by everything typed into: Text Field, Textarea, Number, Date
Field, Time Field, Combobox and Tags Input, whose fields are
"::part(input)", or "::part(textarea)" for Textarea. File Drop starts from it
too, for its label and description.
*/
export default css`
:host {
color: var(--ink);
display: grid;
font-family: var(--font-body);
}
[part~='label'] {
font-family: var(--font-display);
font-size: 1.0625rem;
font-weight: 600;
margin-block-end: 0.375rem;
}
:host(:state(required)) [part~='label']::after {
color: var(--ember-deep);
content: ' *' / '';
}
[part~='input'],
[part~='textarea'] {
background-color: var(--paper);
block-size: 3rem;
border: var(--border);
border-radius: 0;
box-sizing: border-box;
color: inherit;
font: inherit;
/* A fixed height, shared with Select, so fields line up in a row. */
padding: 0 0.75rem;
transition:
background-color 100ms,
border-color 100ms,
box-shadow 100ms,
translate 100ms;
&::placeholder {
color: var(--ash);
}
/*
The outline is transparent rather than gone, so it still shows in forced
colors, where box shadows don't.
*/
&:focus-visible {
box-shadow: 4px 4px 0 var(--ember);
outline: 2px solid transparent;
translate: -2px -2px;
}
&:disabled {
background-color: var(--chalk);
border-style: dashed;
color: var(--ash);
cursor: not-allowed;
}
}
[part~='textarea'] {
block-size: auto;
min-block-size: 7rem;
padding: 0.625rem 0.75rem;
resize: vertical;
}
:host(:state(readonly)) [part~='input'],
:host(:state(readonly)) [part~='textarea'] {
background-color: var(--chalk);
}
:host(:state(error)) [part~='input'],
:host(:state(error)) [part~='textarea'] {
border-color: var(--ember-deep);
}
/*
Spaced by what's in it rather than by a gap, so a description with
nothing in it takes no room, and a control beside the field lines up
with its bottom edge.
*/
[part~='description'] {
color: var(--ash);
display: grid;
font-size: 0.875rem;
}
::slotted([slot='description']) {
margin-block-start: 0.375rem;
}
[part~='validity-message'] {
color: var(--ember-deep);
font-weight: 600;
margin-block-start: 0.375rem;
&::before {
content: '■ ' / '';
}
}
@media (prefers-reduced-motion: reduce) {
[part~='input'],
[part~='textarea'] {
transition: none;
}
}
`;
tokens.styles.ts
import { css } from 'lit';
/*
The styled components' palette and type, taken from the logo: ink for the
courses, ember for the base, paper behind them.
Each is a short alias for a "--slab-styled-*" property, with the default as
its fallback. Retheming is "--slab-styled-ember" and the rest on ":root",
which every shadow root inherits. Were the defaults declared under those
names here instead, each host's own declaration would shadow the page's.
Each default is "light-dark()", so the components follow the page's
"color-scheme": "dark" for dark, "light" for light, and "light dark" to
follow the operating system. Ink and paper trade places in the dark, so
ink is always the text and paper always what's behind it. Contrast is given
as light, then dark.
*/
export default css`
:host {
/* Secondary text. 6.8:1 and 6.9:1 on paper. */
--ash: var(--slab-styled-ash, light-dark(#5e5a55, #a8a19a));
--border: 2px solid var(--ink);
/* Behind disabled and read-only controls, and hovered rows. */
--chalk: var(--slab-styled-chalk, light-dark(#f4f1ec, #2b2926));
/*
The logo's orange, brighter in the dark. 4.25:1 and 5.2:1 on paper is
enough for borders, fills and large text, but not always for small text,
which takes "--ember-deep".
*/
--ember: var(--slab-styled-ember, light-dark(#d9492a, #ec5f3b));
/*
Orange for small text on paper, and for paper text on it. 5.7:1 and
7.4:1.
*/
--ember-deep: var(--slab-styled-ember-deep, light-dark(#b3381a, #ff8566));
--font-body: var(
--slab-styled-font-body,
'IBM Plex Sans',
system-ui,
sans-serif
);
--font-display: var(
--slab-styled-font-display,
'Zilla Slab',
rockwell,
'Roboto Slab',
serif
);
--font-mono: var(
--slab-styled-font-mono,
'IBM Plex Mono',
ui-monospace,
monospace
);
--ink: var(--slab-styled-ink, light-dark(#1c1c1c, #f4f1ec));
--paper: var(--slab-styled-paper, light-dark(#fff, #1a1918));
/* The hard, offset shadow a raised surface casts. */
--shadow: 4px 4px 0 var(--ink);
}
`;
The same styles, written from outside the element, so
there's nothing to subclass. The CSS styles every
<slab-date-range-field> on the page. Import
tokens.css, the colors and fonts every styled
example shares, once, wherever your global styles go.
Each component registers its headless element and imports its CSS. Use it like the element: its props are the element's attributes and properties.
DateRangeField.vue
<script setup>
import 'slab-elements/date-range-field.js';
import './date-range-field.css';
</script>
<template>
<slab-date-range-field><slot /></slab-date-range-field>
</template>
date-range-field.css
slab-date-range-field {
color: var(--ink);
display: grid;
font-family: var(--font-body);
}
slab-date-range-field::part(label) {
font-family: var(--font-display);
font-size: 1.0625rem;
font-weight: 600;
margin-block-end: 0.375rem;
}
slab-date-range-field:state(required)::part(label)::after {
color: var(--ember-deep);
content: ' *' / '';
}
slab-date-range-field::part(input),
slab-date-range-field::part(textarea) {
background-color: var(--paper);
block-size: 3rem;
border: var(--border);
border-radius: 0;
box-sizing: border-box;
color: inherit;
font: inherit;
/* A fixed height, shared with Select, so fields line up in a row. */
padding: 0 0.75rem;
transition:
background-color 100ms,
border-color 100ms,
box-shadow 100ms,
translate 100ms;
}
slab-date-range-field::part(input)::placeholder,
slab-date-range-field::part(textarea)::placeholder {
color: var(--ash);
}
/*
The outline is transparent rather than gone, so it still shows in forced
colors, where box shadows don't.
*/
slab-date-range-field::part(input):focus-visible,
slab-date-range-field::part(textarea):focus-visible {
box-shadow: 4px 4px 0 var(--ember);
outline: 2px solid transparent;
translate: -2px -2px;
}
slab-date-range-field::part(input):disabled,
slab-date-range-field::part(textarea):disabled {
background-color: var(--chalk);
border-style: dashed;
color: var(--ash);
cursor: not-allowed;
}
slab-date-range-field::part(textarea) {
block-size: auto;
min-block-size: 7rem;
padding: 0.625rem 0.75rem;
resize: vertical;
}
slab-date-range-field:state(readonly)::part(input),
slab-date-range-field:state(readonly)::part(textarea) {
background-color: var(--chalk);
}
slab-date-range-field:state(error)::part(input),
slab-date-range-field:state(error)::part(textarea) {
border-color: var(--ember-deep);
}
/*
Spaced by what's in it rather than by a gap, so a description with
nothing in it takes no room, and a control beside the field lines up
with its bottom edge.
*/
slab-date-range-field::part(description) {
color: var(--ash);
display: grid;
font-size: 0.875rem;
}
slab-date-range-field > [slot='description'] {
margin-block-start: 0.375rem;
}
slab-date-range-field::part(validity-message) {
color: var(--ember-deep);
font-weight: 600;
margin-block-start: 0.375rem;
}
slab-date-range-field::part(validity-message)::before {
content: '■ ' / '';
}
@media (prefers-reduced-motion: reduce) {
slab-date-range-field::part(input),
slab-date-range-field::part(textarea) {
transition: none;
}
}
slab-date-range-field::part(fields) {
display: grid;
gap: 0.75rem;
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
}
slab-date-range-field::part(field) {
display: grid;
gap: 0.25rem;
}
slab-date-range-field::part(input-label) {
color: var(--ash);
font-size: 0.8125rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
tokens.css
:root {
/* Secondary text. 6.8:1 and 6.9:1 on paper. */
--ash: var(--slab-styled-ash, light-dark(#5e5a55, #a8a19a));
--border: 2px solid var(--ink);
/* Behind disabled and read-only controls, and hovered rows. */
--chalk: var(--slab-styled-chalk, light-dark(#f4f1ec, #2b2926));
/*
The logo's orange, brighter in the dark. 4.25:1 and 5.2:1 on paper is
enough for borders, fills and large text, but not always for small text,
which takes "--ember-deep".
*/
--ember: var(--slab-styled-ember, light-dark(#d9492a, #ec5f3b));
/*
Orange for small text on paper, and for paper text on it. 5.7:1 and
7.4:1.
*/
--ember-deep: var(--slab-styled-ember-deep, light-dark(#b3381a, #ff8566));
--font-body: var(
--slab-styled-font-body,
'IBM Plex Sans',
system-ui,
sans-serif
);
--font-display: var(
--slab-styled-font-display,
'Zilla Slab',
rockwell,
'Roboto Slab',
serif
);
--font-mono: var(
--slab-styled-font-mono,
'IBM Plex Mono',
ui-monospace,
monospace
);
--ink: var(--slab-styled-ink, light-dark(#1c1c1c, #f4f1ec));
--paper: var(--slab-styled-paper, light-dark(#fff, #1a1918));
/* The hard, offset shadow a raised surface casts. */
--shadow: 4px 4px 0 var(--ink);
}
The same styles, written from outside the element, so
there's nothing to subclass. The CSS styles every
<slab-date-range-field> on the page. Import
tokens.css, the colors and fonts every styled
example shares, once, wherever your global styles go.
Import date-range-field.js once, such
as in your root layout, and use
<slab-date-range-field> as is. A Svelte component
around it would take slot="…" content as its
own slot and drop it.
date-range-field.js
import 'slab-elements/date-range-field.js';
import './date-range-field.css';
date-range-field.css
slab-date-range-field {
color: var(--ink);
display: grid;
font-family: var(--font-body);
}
slab-date-range-field::part(label) {
font-family: var(--font-display);
font-size: 1.0625rem;
font-weight: 600;
margin-block-end: 0.375rem;
}
slab-date-range-field:state(required)::part(label)::after {
color: var(--ember-deep);
content: ' *' / '';
}
slab-date-range-field::part(input),
slab-date-range-field::part(textarea) {
background-color: var(--paper);
block-size: 3rem;
border: var(--border);
border-radius: 0;
box-sizing: border-box;
color: inherit;
font: inherit;
/* A fixed height, shared with Select, so fields line up in a row. */
padding: 0 0.75rem;
transition:
background-color 100ms,
border-color 100ms,
box-shadow 100ms,
translate 100ms;
}
slab-date-range-field::part(input)::placeholder,
slab-date-range-field::part(textarea)::placeholder {
color: var(--ash);
}
/*
The outline is transparent rather than gone, so it still shows in forced
colors, where box shadows don't.
*/
slab-date-range-field::part(input):focus-visible,
slab-date-range-field::part(textarea):focus-visible {
box-shadow: 4px 4px 0 var(--ember);
outline: 2px solid transparent;
translate: -2px -2px;
}
slab-date-range-field::part(input):disabled,
slab-date-range-field::part(textarea):disabled {
background-color: var(--chalk);
border-style: dashed;
color: var(--ash);
cursor: not-allowed;
}
slab-date-range-field::part(textarea) {
block-size: auto;
min-block-size: 7rem;
padding: 0.625rem 0.75rem;
resize: vertical;
}
slab-date-range-field:state(readonly)::part(input),
slab-date-range-field:state(readonly)::part(textarea) {
background-color: var(--chalk);
}
slab-date-range-field:state(error)::part(input),
slab-date-range-field:state(error)::part(textarea) {
border-color: var(--ember-deep);
}
/*
Spaced by what's in it rather than by a gap, so a description with
nothing in it takes no room, and a control beside the field lines up
with its bottom edge.
*/
slab-date-range-field::part(description) {
color: var(--ash);
display: grid;
font-size: 0.875rem;
}
slab-date-range-field > [slot='description'] {
margin-block-start: 0.375rem;
}
slab-date-range-field::part(validity-message) {
color: var(--ember-deep);
font-weight: 600;
margin-block-start: 0.375rem;
}
slab-date-range-field::part(validity-message)::before {
content: '■ ' / '';
}
@media (prefers-reduced-motion: reduce) {
slab-date-range-field::part(input),
slab-date-range-field::part(textarea) {
transition: none;
}
}
slab-date-range-field::part(fields) {
display: grid;
gap: 0.75rem;
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
}
slab-date-range-field::part(field) {
display: grid;
gap: 0.25rem;
}
slab-date-range-field::part(input-label) {
color: var(--ash);
font-size: 0.8125rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
tokens.css
:root {
/* Secondary text. 6.8:1 and 6.9:1 on paper. */
--ash: var(--slab-styled-ash, light-dark(#5e5a55, #a8a19a));
--border: 2px solid var(--ink);
/* Behind disabled and read-only controls, and hovered rows. */
--chalk: var(--slab-styled-chalk, light-dark(#f4f1ec, #2b2926));
/*
The logo's orange, brighter in the dark. 4.25:1 and 5.2:1 on paper is
enough for borders, fills and large text, but not always for small text,
which takes "--ember-deep".
*/
--ember: var(--slab-styled-ember, light-dark(#d9492a, #ec5f3b));
/*
Orange for small text on paper, and for paper text on it. 5.7:1 and
7.4:1.
*/
--ember-deep: var(--slab-styled-ember-deep, light-dark(#b3381a, #ff8566));
--font-body: var(
--slab-styled-font-body,
'IBM Plex Sans',
system-ui,
sans-serif
);
--font-display: var(
--slab-styled-font-display,
'Zilla Slab',
rockwell,
'Roboto Slab',
serif
);
--font-mono: var(
--slab-styled-font-mono,
'IBM Plex Mono',
ui-monospace,
monospace
);
--ink: var(--slab-styled-ink, light-dark(#1c1c1c, #f4f1ec));
--paper: var(--slab-styled-paper, light-dark(#fff, #1a1918));
/* The hard, offset shadow a raised surface casts. */
--shadow: 4px 4px 0 var(--ink);
}