# Disclosure Show and hide non-critical, contextual content in place Section: components Docs: https://nebula.kaluza.com/components/disclosure --- ## When to use - To make a page easier to scan when it contains information that only some users will need - For supporting explanations that users are unlikely to need on repeat visits ## When not to use - **When the information is critical, or a majority of users will need it** - forcing most users to make an extra click slows down task completion, and some users might miss it altogether. Show the information instead. - **In a group** - for instance, to show a list of frequently asked questions. Use [Accordion](/components/accordion) instead, unless the content is inside a constrained layout such as a Card or narrow sidebar, where Accordion would be disproportionate. - **To switch between distinct sections of content** - Disclosure is for revealing detail in place, not for navigation between views. Use [Tabs](/components/tabs) instead. - **For brief supplementary information about a specific element** - Disclosure is for longer content that benefits from being read in context. Use [Tooltip](/components/tooltip) instead. - **For substantial content that warrants its own focus** - it may be more appropriate to use [Modal](/components/modal) instead. ## Anatomy 1. **Caret** 2. **Title** 3. **Content indent** 4. **Disclosure content** ## Keyboard support ### Focus order 1. Caret and title ### Keyboard functions - **Tab** - Moves keyboard focus to Disclosure - **Space or Enter** - Toggles the expanded/collapsed state of the disclosure ## Basic usage Disclosure uses the native HTML `details` and `summary` elements. This means open and close behaviour is handled by the browser without JavaScript. ```tsx

A person's carbon footprint is made up of all the carbon that’s released through actions they take and choices they make, in their day-to-day lives.

``` ## Variants and features ### Expanded by default Disclosure is an uncontrolled component and manages its own state. The default state can be set with the `open` prop. ```tsx

A person's carbon footprint is made up of all the carbon that's released through actions they take and choices they make, in their day-to-day lives.

``` ## Rules and guidance ### Usage - **Disclosure** MUST NOT hide critical information. [id: disclosure.no-critical-content; testability: manual] - **Disclosure** SHOULD NOT be used when the majority of users will need the content. Rationale: Forcing most users to make an extra click slows down task completion and some users may miss the content altogether. [id: disclosure.no-majority-content; testability: manual] - **Disclosure** SHOULD NOT be used to switch between distinct sections of content. Rationale: Disclosure is for revealing detail in place, not for navigation between views. Use Tabs instead. [id: disclosure.no-navigation; testability: manual] - **Disclosure** SHOULD NOT be used in a group. Rationale: Use Accordion instead, which is designed for this purpose. Exception: content must fit within a constrained layout where a full Accordion would be visually disproportionate, such as inside a Card or narrow sidebar column. [id: disclosure.no-grouping; testability: manual] ### Content - **Disclosure title** MUST clearly describe the content it will reveal. Rationale: Users decide whether to expand a Disclosure based on its title. A vague title means users may miss information they need or expand content that is irrelevant to them. [id: disclosure.title.clearly-descriptive; testability: manual] - **Disclosure title** SHOULD avoid sounding like it will take the user to another page. Rationale: Language that implies navigation may dissuade users from clicking even when they need the information. [id: disclosure.title.no-navigation-language; testability: manual] - **Disclosure title** SHOULD NOT be longer than a single short sentence. [id: disclosure.title.single-sentence; testability: auto] - **Disclosure content** SHOULD NOT be complex or contain a large amount of content. Rationale: Revealing a large amount of content in place can disrupt the user's position on the page and makes it harder to reorient. [id: disclosure.content.no-large-content; testability: manual] ### Interaction - **Disclosure** WILL render as a native HTML details/summary element. The browser provides the expanded and collapsed state, focus management, and aria-expanded on the summary without any additional props. [id: disclosure.will-native-details; testability: auto] - **Disclosure** WILL always be toggleable between expanded and collapsed states. The component manages its own toggle state. [id: disclosure.always-toggleable; testability: auto] ### Composition - **Disclosure content** MUST NOT contain another Disclosure. Rationale: Nesting creates disorientation and makes it hard for users to track where they are. [id: disclosure.content.no-nested-disclosure; testability: auto] - **Disclosure content** MUST NOT contain a primary button. Rationale: By its nature, Disclosure content is supplementary and should not contain primary actions. [id: disclosure.content.no-primary-button; testability: auto] - **Disclosure content** SHOULD NOT contain a TextLink. Rationale: The Disclosure title is already styled as a link - additional links inside the content may create ambiguity about what is navigation and what reveals detail. [id: disclosure.content.no-text-link; testability: manual] - **Disclosure content** MAY contain a secondary button, using CTALink or CTAButton depending on whether the action navigates or triggers behaviour. [id: disclosure.content.secondary-button; testability: manual] ## Accessibility ### In brief Disclosure is a native `details` element with a `summary` for its title. The browser owns the keyboard model, the open state and keeping closed content out of reach. ### What Nebula provides - The title is a native `summary`, so it is a tab stop that opens and closes with the keyboard, with a visible focus ring. - Nebula sets no ARIA attributes of its own: the browser exposes the open state. - The caret is an Icon, so it is hidden from screen readers. ### Keyboard behaviour The title is a single tab stop, reached with `Tab`. #### Interactions - **Tab** - Moves focus to the title - **Enter or Space** - Opens or closes the content ### Accessible design #### Write a title that says what opens The title is the only name a keyboard or screen reader user has for the hidden content. "How is my carbon footprint calculated?" says more than "Find out more". WCAG: [2.4.6 Headings and Labels](https://www.w3.org/WAI/WCAG22/Understanding/headings-and-labels.html) ### Accessible development #### Avoid interactive elements in the `title` The whole title is the control that opens the Disclosure, so a link or button inside it competes with it for the same click and key press. WCAG: [4.1.2 Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) ### Accessibility testing status Reviewed on 9 October 2026 against v17.0.1. | Test | Status | Tested with | | --- | --- | --- | | Default state | Tested | axe-core via the Storybook test runner (CI) | | Advanced states | Partially tested | axe-core via the Storybook test runner (CI) | | Keyboard navigation | Manually tested | keyboard (reported by the owner) | | Screen reader | Partially tested | VoiceOver with Chrome (reported by the owner) | What each test covers: - Default state: The component as it first appears, before anything is clicked or changed. - Advanced states: Every other state and variant: error, open, selected, each size and style, and hovering. - Keyboard navigation: Whether every function can be reached and used with the keyboard alone, with a visible focus ring. - Screen reader: Whether a screen reader announces what is on screen correctly: the name, the role and any state. What these statuses mean: - Tested: Every check in this row passed in an automated run that repeats on every pull request. - Manually tested: Every check in this row was run once, by a person or with a tool they started, and all of them passed. It is not re-run on every change. - Partially tested: Some checks in this row were run once and passed; the rest are still to do. - Issue found: A check in this row failed. The ticket is shown beside the status. - Not tested: No check in this row has been run yet. - Not applicable: This row does not apply to the component, for the reason given beside it. The table records which checks were run on this component on its own. It is not a statement of WCAG conformance, and it does not cover how the component is used. ## Properties ### Disclosure Also accepts all props from `DetailsHTMLAttributes`, `HTMLAttributes`. - `children` (type `ReactNode`, optional). Content revealed when the disclosure is open. - `title` (type `ReactNode`, required). Content of the clickable summary that opens and closes the disclosure. - `expanded` (type `boolean`, optional). Opens the disclosure when `true`. Works like the native `open` attribute, and `false` does not close a disclosure that has `open` set. - `onToggle` (type `(open: boolean) => void`, optional). Called with the new open state whenever the disclosure is opened or closed. - `open` (type `boolean`, optional). Opens the disclosure when `true`; the native `details` attribute. `expanded` does the same. ## Styles Reference only; you do not need this to use the component. This is the CSS its styled elements use, with theme tokens shown by name. `${...}` is an interpolation (a theme token, a mixin or another value from the code). A `&[prop]` or `@when ` block holds CSS that applies only under that condition; these are notation, not valid CSS. ### disclosure-summary ```css background: none; border: none; border-radius: 0; padding: 0; margin: 0; font-family: ${fonts.body}; font-size: ${responsiveFontSizes.body[0]}; line-height: ${lineHeights.body}; font-weight: inherit; text-decoration: underline; text-align: left; color: ${colors.primary}; position: relative; cursor: pointer; &::-webkit-details-marker, &::marker { display: none; } list-style: none; display: flex; align-items: center; ${mediaQueries.smallAndUp} { font-size: ${responsiveFontSizes.body[1]}; } &:focus-visible { ${focusRing} } ``` ### icon-wrapper ```css display: inline-flex; details[open] & { transform: rotate(90deg); } ``` ### line-indent ```css position: absolute; top: 0; bottom: 0; width: ${borderWidths.heavier}; border-radius: ${radii.standard}; left: 0.2rem; background-color: ${colors.borderMuted}; ${mediaQueries.smallAndUp} { left: 0.3rem; } ``` ### disclosure-panel ```css position: relative; box-sizing: border-box; margin-top: ${space[2]}; padding-left: ${responsiveFontSizes.body[0] + space[1]}; ${mediaQueries.smallAndUp} { margin-top: ${space[3]}; padding-left: ${responsiveFontSizes.body[1] + space[2]}; } ```