Disclosure
Disclosure
Show and hide content behind a summary. It's a native <details> with a few extras.
- Element
<slab-disclosure>- Built on
<details>- As Markdown
- disclosure.md
Example
Here's a styled version, <slab-styled-disclosure>. 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-disclosure label="Why keep it wet?" open>
Concrete cures by reacting with water, not by drying. A slab that dries
too soon is weaker and cracks.
</slab-styled-disclosure>
<slab-disclosure label="Why keep it wet?" open>
Concrete cures by reacting with water, not by drying. A slab that dries
too soon is weaker and cracks.
</slab-disclosure>
Usage
pnpm add slab-elements lit
<script type="module">
import 'slab-elements/disclosure.js';
</script>
<slab-disclosure label="Shipping">
Orders ship within two business days.
</slab-disclosure>
Keyboard
| Key | What it does |
|---|---|
| Enter, Space | Opens or closes it, as on a <summary>. |
Behavior
A disclosure with no visual styles, around a native <details>, so it opens, closes, and is read the way one is.
Nearly everything is the platform's. Enter and Space toggle it, find-in-page opens it to reveal a match, and its marker is the browser's until a design replaces it through ::part(summary). What Disclosure adds is a label, an open kept in step with the <details> however it was toggled, and a "toggle" event from the host.
name is passed to the <details>, but a name only groups <details> in the same tree, and each Disclosure's is in a shadow root of its own. So two Disclosures with the same name don't close each other. Grouping them is Accordion's job.
The label is text rather than a slot. A <summary> is a button to assistive technology, and a button's name is flattened to text regardless. Anything a design wants beside it, such as an icon, goes on through ::part(summary).
API
<slab-disclosure>
| Name | Type | Default | Description |
|---|---|---|---|
label
Required
|
string |
||
name
|
string |
'' |
|
open
|
boolean |
false |
|
version
|
string |
| Signature | Description |
|---|---|
click(): void
|
|
focus(options?: FocusOptions): void
|
| Name | Type | Description |
|---|---|---|
toggle
|
ToggleEvent |
| Name | Type | Description |
|---|---|---|
(default)
|
Element | string |
| Name | Description |
|---|---|
content
|
The content revealed when open |
details
|
|
summary
|
The label, and the control that toggles the content |
Copy and own it
<slab-styled-disclosure> isn't in the package. It's an example —
copy it into your project and change whatever you want. Under the
hood, it's just the headless element, subclassed, with
styles set. That's it. 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.
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.
disclosure.ts
import { customElement } from 'lit/decorators.js';
import Disclosure from 'slab-elements/disclosure.js';
import styles from './disclosure.styles.js';
import tokens from './tokens.styles.js';
declare global {
interface HTMLElementTagNameMap {
'slab-styled-disclosure': StyledDisclosure;
}
}
/**
* Disclosure, styled: a slab-serif summary with a square marker at its end,
* a plus on ink that turns to a minus on ember when it's open.
*/
@customElement('slab-styled-disclosure')
export default class StyledDisclosure extends Disclosure {
static override styles = [tokens, styles];
}
disclosure.styles.ts
import { css } from 'lit';
export default css`
:host {
border-block-end: var(--border);
color: var(--ink);
font-family: var(--font-body);
}
[part~='summary'] {
align-items: center;
cursor: pointer;
display: flex;
font-family: var(--font-display);
font-size: 1.25rem;
font-weight: 600;
gap: 1rem;
justify-content: space-between;
list-style: none;
padding: 1rem 0.25rem;
&::-webkit-details-marker {
display: none;
}
/*
The marker. Its text is decorative, so it's given empty alternative
text: the summary's expanded state already says it's open.
*/
&::after {
align-items: center;
background-color: var(--ink);
block-size: 1.75rem;
color: var(--paper);
content: '+' / '';
display: flex;
flex-shrink: 0;
font-family: var(--font-mono);
font-size: 1.25rem;
font-weight: 400;
inline-size: 1.75rem;
justify-content: center;
transition: background-color 150ms;
}
&:hover::after {
background-color: var(--ember);
}
&:focus-visible {
outline: 3px solid var(--ember);
outline-offset: 2px;
}
}
:host([open]) [part~='summary']::after {
background-color: var(--ember);
content: '−' / '';
}
/*
A closed "<details>" hides its content with "content-visibility". The
transition on it is discrete, so the content stays visible until the
height has finished closing over it.
*/
[part~='details']::details-content {
block-size: 0;
overflow: clip;
transition:
block-size 250ms ease-out,
content-visibility 250ms allow-discrete;
}
/*
"calc-size()" is "auto" that a transition can reach. Where it isn't
supported, the declaration is dropped for plain "auto", and Disclosure
opens and closes at once, as it would with no styles.
*/
:host([open]) [part~='details']::details-content {
block-size: auto;
block-size: calc-size(auto, size);
}
[part~='content'] {
line-height: 1.6;
padding: 0 0.25rem 1.25rem;
}
@media (prefers-reduced-motion: reduce) {
[part~='summary']::after,
[part~='details']::details-content {
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);
}
`;