Disclosure

Progressively enhances a button/content pair into an accessible disclosure.

Default closed

With open omitted, Disclosure initially shows only its trigger.

Disclosure content.

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-disclosure/ui-disclosure.html"
>

<ui-disclosure
  summary="Details"
>Disclosure content.</ui-disclosure>
<script setup>
  import { UiDisclosure } from "@threadlabs/looma";
</script>

<template>
  <UiDisclosure
    summary="Details"
  >Disclosure content.</UiDisclosure>
</template>
import { UiDisclosure } from "@threadlabs/looma";

export default function Example() {
  return <UiDisclosure
    summary="Details"
  >Disclosure content.</UiDisclosure>;
}
/** @jsxImportSource preact */
import { UiDisclosure } from "@threadlabs/looma";

export default function Example() {
  return <UiDisclosure
    summary="Details"
  >Disclosure content.</UiDisclosure>;
}
<script>
  import { UiDisclosure } from "@threadlabs/looma";
</script>

<UiDisclosure
  summary="Details"
>Disclosure content.</UiDisclosure>
import { Component } from "@angular/core";
import { UiDisclosure } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiDisclosure],
  template: `
    <div
      ui-disclosure
      [summary]="'Details'"
    >Disclosure content.</div>
  `,
})
export default class Example {}

open and disabled

Open starts the panel expanded; disabled prevents the generated trigger from toggling its content.

Visible content.
Unavailable content.

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-disclosure/ui-disclosure.html"
>

<div>
  <ui-disclosure
    summary="Open disclosure"
    open
  >Visible content.</ui-disclosure>
  <ui-disclosure
    summary="Disabled disclosure"
    disabled
  >Unavailable content.</ui-disclosure>
</div>
<script setup>
  import { UiDisclosure } from "@threadlabs/looma";
</script>

<template>
  <div>
    <UiDisclosure
      summary="Open disclosure"
      :open="true"
    >Visible content.</UiDisclosure>
    <UiDisclosure
      summary="Disabled disclosure"
      :disabled="true"
    >Unavailable content.</UiDisclosure>
  </div>
</template>
import { UiDisclosure } from "@threadlabs/looma";

export default function Example() {
  return <div>
    <UiDisclosure
      summary="Open disclosure"
      open
    >Visible content.</UiDisclosure>
    <UiDisclosure
      summary="Disabled disclosure"
      disabled
    >Unavailable content.</UiDisclosure>
  </div>;
}
/** @jsxImportSource preact */
import { UiDisclosure } from "@threadlabs/looma";

export default function Example() {
  return <div>
    <UiDisclosure
      summary="Open disclosure"
      open
    >Visible content.</UiDisclosure>
    <UiDisclosure
      summary="Disabled disclosure"
      disabled
    >Unavailable content.</UiDisclosure>
  </div>;
}
<script>
  import { UiDisclosure } from "@threadlabs/looma";
</script>

<div>
  <UiDisclosure
    summary="Open disclosure"
    open
  >Visible content.</UiDisclosure>
  <UiDisclosure
    summary="Disabled disclosure"
    disabled
  >Unavailable content.</UiDisclosure>
</div>
import { Component } from "@angular/core";
import { UiDisclosure } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiDisclosure],
  template: `
    <div>
      <div
        ui-disclosure
        [summary]="'Open disclosure'"
        [open]="true"
      >Visible content.</div>
      <div
        ui-disclosure
        [summary]="'Disabled disclosure'"
        [disabled]="true"
      >Unavailable content.</div>
    </div>
  `,
})
export default class Example {}

Named group and rich summary

Disclosures with the same name allow one open item; a summary slot can include emphasis or a count.

Account settings and access.

Billing methods and invoices.

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-stack/ui-stack.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-disclosure/ui-disclosure.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-text/ui-text.html"
>

<ui-stack
  gap="none"
>
  <ui-disclosure
    name="questions"
    summary="Account"
    heading-level="3"
  >
    <strong
      slot="summary"
    >Account <small>2 topics</small></strong>
    <ui-text>Account settings and access.</ui-text>
  </ui-disclosure>
  <ui-disclosure
    name="questions"
    summary="Billing"
    heading-level="3"
  >
    <ui-text>Billing methods and invoices.</ui-text>
  </ui-disclosure>
</ui-stack>
<script setup>
  import { UiDisclosure, UiStack, UiText } from "@threadlabs/looma";
</script>

<template>
  <UiStack
    gap="none"
  >
    <UiDisclosure
      name="questions"
      summary="Account"
      :heading-level="3"
    >
      <template #summary>
        <strong>Account <small>2 topics</small></strong>
      </template>
      <UiText>Account settings and access.</UiText>
    </UiDisclosure>
    <UiDisclosure
      name="questions"
      summary="Billing"
      :heading-level="3"
    >
      <UiText>Billing methods and invoices.</UiText>
    </UiDisclosure>
  </UiStack>
</template>
import { UiDisclosure, UiStack, UiText } from "@threadlabs/looma";

export default function Example() {
  return <UiStack
    gap="none"
  >
    <UiDisclosure
      name="questions"
      summary="Account"
      headingLevel={3}
      slots={{
        summary: <strong>Account <small>2 topics</small></strong>,
      }}
    >
      <UiText>Account settings and access.</UiText>
    </UiDisclosure>
    <UiDisclosure
      name="questions"
      summary="Billing"
      headingLevel={3}
    >
      <UiText>Billing methods and invoices.</UiText>
    </UiDisclosure>
  </UiStack>;
}
/** @jsxImportSource preact */
import { UiDisclosure, UiStack, UiText } from "@threadlabs/looma";

export default function Example() {
  return <UiStack
    gap="none"
  >
    <UiDisclosure
      name="questions"
      summary="Account"
      headingLevel={3}
      slots={{
        summary: <strong>Account <small>2 topics</small></strong>,
      }}
    >
      <UiText>Account settings and access.</UiText>
    </UiDisclosure>
    <UiDisclosure
      name="questions"
      summary="Billing"
      headingLevel={3}
    >
      <UiText>Billing methods and invoices.</UiText>
    </UiDisclosure>
  </UiStack>;
}
<script>
  import { UiDisclosure, UiStack, UiText } from "@threadlabs/looma";
</script>

{#snippet summary()}
  <strong>Account <small>2 topics</small></strong>
{/snippet}

<UiStack
  gap="none"
>
  <UiDisclosure
    name="questions"
    summary="Account"
    headingLevel={3}
    slots={{ summary }}
  >
    <UiText>Account settings and access.</UiText>
  </UiDisclosure>
  <UiDisclosure
    name="questions"
    summary="Billing"
    headingLevel={3}
  >
    <UiText>Billing methods and invoices.</UiText>
  </UiDisclosure>
</UiStack>
import { Component } from "@angular/core";
import { UiDisclosure, UiStack, UiText } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiStack, UiDisclosure, UiText],
  template: `
    <div
      ui-stack
      [gap]="'none'"
    >
      <div
        ui-disclosure
        [name]="'questions'"
        [summary]="'Account'"
        [headingLevel]="3"
      >
        <ng-container ngProjectAs="[slot='summary']">
          <strong>Account <small>2 topics</small></strong>
        </ng-container>
        <ng-template
          ui-text
        >Account settings and access.</ng-template>
      </div>
      <div
        ui-disclosure
        [name]="'questions'"
        [summary]="'Billing'"
        [headingLevel]="3"
      >
        <ng-template
          ui-text
        >Billing methods and invoices.</ng-template>
      </div>
    </div>
  `,
})
export default class Example {}

API

Package: @threadlabs/looma · Native root: div

Attributes

NameDescriptionPropertyTypeDefaultAllowed values
densityCompact trades padding and type size for rows that fit more on screen.densitykeywordcomfortablecomfortable, compact
disabledDisables the trigger and mutes its text, so the disclosure keeps its current state and the user cannot toggle it.disabledbooleanfalse—
fillIn a column with a bounded height, an open disclosure fills the remaining space and keeps its trigger visible. The content slot can contain a Scroll Area. Closed items keep only their trigger height.fillbooleanfalse—
heading-levelAdds heading semantics around the trigger at level 2 through 6. Zero leaves it a plain disclosure; choose a level that matches the page outline.headingLevelinteger0—
nameDisclosures with the same nonempty name form an exclusive group in one document: opening one closes the others. An empty name allows multiple open items.namestring—
openWhether the disclosure is open initially or when set externally. The trigger toggles it from there, reporting each change as an open or close event.openbooleanfalse—
required-openIn a named exclusive group, keeps at least one member open. Clicking the last open trigger does not close it; opening another member transfers the open state.requiredOpenbooleanfalse—
summaryThe text of the trigger row that opens and closes the disclosure. It is the trigger button's accessible name, so name what the content is.summarystringDetails—
variantNavigation shares icon columns, spacing, type, hover, and corners with Nav Item, while keeping expanded sections neutral: opening content is not route selection. Plain retains the standard disclosure treatment.variantkeywordplainplain, navigation

Framework props

NameTypeDefaultAllowed values
density"comfortable" | "compact"comfortablecomfortable, compact
disabledbooleanfalse—
fillbooleanfalse—
headingLevelnumber0—
namestring—
openbooleanfalse—
requiredOpenbooleanfalse—
summarystringDetails—
variant"plain" | "navigation"plainplain, navigation

Events

NameDetailDescription
closeobject
  • open: boolean (required)
  • reason: keyword (required)
  • trigger: keyword (required)
Emitted when the component transitions to a closed state.
openobject
  • open: boolean (required)
  • reason: keyword (required)
  • trigger: keyword (required)
Emitted when the component transitions to an open state.

Slots

NamePurpose
leadingNamed content for the leading region.
summaryNamed content for the summary region.
indicatorNamed content for the indicator region.
defaultDefault child content.

Design tokens

Set shared theme properties on a theme boundary. Component hooks apply only to their owning component.

Component tokens

TokenDeclared valuesFallback
--_ui-disclosure-heading-sizevar(--ui-control-size) | var(--ui-control-size-sm)—
--ui-disclosure-trigger-min-block-sizevar(--ui-control-size) | var(--ui-control-size-sm)—
--ui-disclosure-trigger-padding-blockvar(--ui-space-2) | var(--ui-space-1)—
--ui-disclosure-trigger-padding-inlinevar(--ui-space-3) | var(--ui-space-2)—

Shared tokens

TokenFallback
--ui-border-divider—
--ui-border-width—
--ui-control-min-block-size—
--ui-control-size—
--ui-control-size-sm—
--ui-disabled-text—
--ui-focus-ring—
--ui-focus-width—
--ui-font-normal—
--ui-font-semibold—
--ui-font-size-md—
--ui-font-size-sm—
--ui-motion-ease—
--ui-motion-fast—
--ui-motion-feedback—
--ui-motion-feedback-ease—
--ui-motion-layout—
--ui-motion-layout-ease—
--ui-motion-reveal—
--ui-motion-reveal-ease—
--ui-nav-compact-min-block-size—
--ui-nav-compact-padding-block—
--ui-nav-compact-padding-inline—
--ui-nav-gap—
--ui-nav-hover-surface—
--ui-nav-icon-size—
--ui-nav-min-block-size—
--ui-nav-padding-block—
--ui-nav-padding-inline—
--ui-radius-sm—
--ui-selection-radius—
--ui-space-1—
--ui-space-2—
--ui-space-3—
--ui-space-4—
--ui-text-primary—
--ui-text-secondary—