# Disclosure

> Show and hide content behind a summary. It's a native `<details>` with a few extras.

`<slab-disclosure>` · Disclosure · built on `<details>`

## Install

```sh
pnpm add slab-elements lit
```

## Usage

```html
<script type="module">
  import 'slab-elements/disclosure.js';
</script>

<slab-disclosure label="Shipping">
  Orders ship within two business days.
</slab-disclosure>
```

## 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.

```html
<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>
```

Swap `slab-styled-` for `slab-` in that markup and you've got the same example as it ships: the headless elements with no CSS at all. That's your starting point.

## 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>`

#### Attributes

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `string` |  |  |
| `name` | `string` | `''` |  |
| `open` | `boolean` | `false` |  |
| `version` | `string` |  |  |

#### Methods

| Signature | Description |
| --- | --- |
| `click(): void` |  |
| `focus(options?: FocusOptions): void` |  |

#### Events

| Name | Type | Description |
| --- | --- | --- |
| `toggle` | `ToggleEvent` |  |

#### Slots

| Name | Type | Description |
| --- | --- | --- |
| `(default)` | `Element \| string` |  |

#### CSS parts

| 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.

### TypeScript

These files are TypeScript, and `@customElement()` is one of Lit's decorators (TypeScript's experimental flavor). You'll need two options in your tsconfig, per [Lit's docs](https://lit.dev/docs/components/decorators/#decorators-typescript): `experimentalDecorators` to compile them, and `useDefineForClassFields` turned off so class fields don't stomp on the accessors Lit sets up.

```json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "useDefineForClassFields": false
  }
}
```

Using plain JavaScript? Drop the types and register the class with `customElements.define()` instead of the decorator.

### disclosure.ts

```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

```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

```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);
  }
`;
```
