Skip to main content
On this pageOverview

Disclosure

Overview

A toggle for showing and hiding content inline. Disclosure manages its own open/closed state and renders a button + panel pair. Use it for FAQs, accordions, and collapsible sections. For overlaying content in a floating panel, use Dialog or Popover instead.

For programmatic control in update functions, use Disclosure.toggle(model) and Disclosure.close(model) which return [Model, Commands, Option<OutMessage>] directly.

See it in an app

Check out how Disclosure is wired up in a real Foldkit app.

Examples

Provide a toView callback that receives the button and panel attribute bundles. Spread them onto your own elements; Disclosure manages the ARIA linking and toggle behavior.

// Pseudocode walkthrough of the Foldkit integration points. Each labeled
// block below is an excerpt. Fit them into your own Model, init, Message,
// update, and view definitions.
import { Match as M, Option } from 'effect'
import { Command, Ui } from 'foldkit'
import { html } from 'foldkit/html'
import { m } from 'foldkit/message'
import { evo } from 'foldkit/struct'

// Add a field to your Model for the Disclosure Submodel:
const Model = S.Struct({
  disclosure: Ui.Disclosure.Model,
  // ...your other fields
})

// In your init function, initialize the Disclosure Submodel with a unique id:
const init = () => [
  {
    disclosure: Ui.Disclosure.init({ id: 'faq-1' }),
    // ...your other fields
  },
  [],
]

// Embed the Disclosure Message in your parent Message:
const GotDisclosureMessage = m('GotDisclosureMessage', {
  message: Ui.Disclosure.Message,
})

// Inside your update function's M.tagsExhaustive({...}), delegate to
// Ui.Disclosure.update. The OutMessage's `ToggledOpenState` fires on each
// open / close transition with the new `isOpen`. Useful for analytics
// or coordinated UI changes.
GotDisclosureMessage: ({ message }) => {
  const [nextDisclosure, commands, maybeOutMessage] = Ui.Disclosure.update(
    model.disclosure,
    message,
  )
  const mappedCommands = Command.mapMessages(commands, message =>
    GotDisclosureMessage({ message }),
  )

  return Option.match(maybeOutMessage, {
    onNone: () => [
      evo(model, { disclosure: () => nextDisclosure }),
      mappedCommands,
    ],
    onSome: M.type<Ui.Disclosure.OutMessage>().pipe(
      M.tagsExhaustive({
        ToggledOpenState: ({ isOpen }) => [
          // The child has emitted `ToggledOpenState`. The body commits
          // the child's next state as usual. In this arm the parent
          // can also update its own state or dispatch its own
          // Commands, for example persist the open state, lazy-load
          // panel content, or log analytics.
          evo(model, { disclosure: () => nextDisclosure }),
          mappedCommands,
        ],
      }),
    ),
  })
}

// Inside your view function, embed the disclosure via h.submodel:
const view = (model: Model) => {
  const h = html<Message>()

  return h.submodel({
    slotId: 'faq-1',
    model: model.disclosure,
    view: Ui.Disclosure.view,
    viewInputs: {
      toView: attributes =>
        h.div(
          [],
          [
            h.button(
              [
                ...attributes.button,
                h.Class(
                  'flex items-center justify-between w-full p-4 border rounded-lg data-[open]:rounded-b-none',
                ),
              ],
              [h.span([], ['What is Foldkit?'])],
            ),
            model.disclosure.isOpen
              ? h.div(
                  [
                    ...attributes.panel,
                    h.Class('p-4 border-x border-b rounded-b-lg'),
                  ],
                  [h.p([], ['A functional UI framework built on Effect-TS.'])],
                )
              : h.empty,
          ],
        ),
    },
    toParentMessage: message => GotDisclosureMessage({ message }),
  })
}

Styling

Use the data-open attribute to style the button and panel differently when open. A common pattern is rotating a chevron icon and changing border radius: data-[open]:rounded-b-none on the button, rounded-b-lg on the panel.

AttributeCondition
data-openPresent on both button and panel when the disclosure is open.
data-disabledPresent on the button when isDisabled is true.

Keyboard Interaction

KeyDescription
EnterToggles the disclosure.
SpaceToggles the disclosure.

Accessibility

The toggle button receives aria-expanded and aria-controls linking to the panel. When the disclosure closes, focus is returned to the toggle button automatically.

API Reference

InitConfig

Configuration object passed to Disclosure.init().

NameTypeDefaultDescription
idstring-Unique ID for the disclosure instance.
isOpenbooleanfalseInitial open/closed state.

ViewConfig

Configuration object passed to Disclosure.view().

NameTypeDefaultDescription
modelDisclosure.Model-The disclosure state from your parent Model.
toParentMessage(childMessage: Disclosure.Message) => ParentMessage-Wraps Disclosure Messages in your parent Message type for Submodel delegation.
toView(attributes: DisclosureAttributes) => Html-Callback that receives the `button` and `panel` attribute bundles and returns the composed layout. The consumer reads `isOpen` from their parent model when they need to render conditionally on it.
isDisabledbooleanfalseWhen true, the button is not clickable, gets `aria-disabled` and a `data-disabled` attribute.

DisclosureAttributes

Attribute bundles delivered to the toView callback each render.

NameTypeDefaultDescription
buttonReadonlyArray<ChildAttribute>-Spread onto the toggle button element. Includes `aria-expanded`, `aria-controls`, `tabindex`, and the click + Enter/Space keyboard handlers.
panelReadonlyArray<ChildAttribute>-Spread onto the panel element. Includes the panel id (`${id}-panel`) and a `data-open` attribute when open.

OutMessage

Messages emitted to the parent through the third element of [Model, Commands, Option<OutMessage>]. Pattern-match on the OutMessage in your update handler.

NameTypeDefaultDescription
ToggledOpenState{ isOpen: boolean }-Emitted on each toggle, carrying the new open state. Pattern-match the third tuple element of Disclosure.update in your GotDisclosureMessage handler to react (e.g. analytics, lazy content loading, persisting open state).

Programmatic Helpers

Helpers a parent calls in its update without constructing a Disclosure Message.

NameTypeDefaultDescription
toggle(model: Model) => [Model, Commands, Option<OutMessage>]-Flips the open state as a user-style choice, emitting ToggledOpenState. Use for a programmatic toggle that should behave like a click. To mirror an external open state without emitting, use reflectOpenState.
reflectOpenState(model: Model, isOpen: boolean) => Model-Reflects an externally-sourced open state onto the model without emitting an OutMessage. Use to mirror external truth (a URL, restored layout state, a sibling field) onto the disclosure. Dual: pass just the boolean for a point-free setter in an evo callback.

Stay in the update loop.

New releases, patterns, and the occasional deep dive.


Built with Foldkit.

© 2026 Devin Jameson