On this pageOverview
Disclosure
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.
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 }),
})
}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.
| Attribute | Condition |
|---|---|
data-open | Present on both button and panel when the disclosure is open. |
data-disabled | Present on the button when isDisabled is true. |
| Key | Description |
|---|---|
| Enter | Toggles the disclosure. |
| Space | Toggles the disclosure. |
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.
Configuration object passed to Disclosure.init().
| Name | Type | Default | Description |
|---|---|---|---|
id | string | - | Unique ID for the disclosure instance. |
isOpen | boolean | false | Initial open/closed state. |
Configuration object passed to Disclosure.view().
| Name | Type | Default | Description |
|---|---|---|---|
model | Disclosure.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. |
isDisabled | boolean | false | When true, the button is not clickable, gets `aria-disabled` and a `data-disabled` attribute. |
Attribute bundles delivered to the toView callback each render.
| Name | Type | Default | Description |
|---|---|---|---|
button | ReadonlyArray<ChildAttribute> | - | Spread onto the toggle button element. Includes `aria-expanded`, `aria-controls`, `tabindex`, and the click + Enter/Space keyboard handlers. |
panel | ReadonlyArray<ChildAttribute> | - | Spread onto the panel element. Includes the panel id (`${id}-panel`) and a `data-open` attribute when open. |
Messages emitted to the parent through the third element of [Model, Commands, Option<OutMessage>]. Pattern-match on the OutMessage in your update handler.
| Name | Type | Default | Description |
|---|---|---|---|
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). |
Helpers a parent calls in its update without constructing a Disclosure Message.
| Name | Type | Default | Description |
|---|---|---|---|
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. |