@lumiose/ui 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -2
  3. package/dist/accordion/accordion.css +20 -0
  4. package/dist/accordion/accordion.script.ts +66 -0
  5. package/dist/accordion/accordion.tsx +288 -0
  6. package/dist/animation/animation.ts +34 -0
  7. package/dist/autocomplete/autocomplete.tsx +239 -0
  8. package/dist/avatar/avatar.css +54 -0
  9. package/dist/avatar/avatar.script.ts +96 -0
  10. package/dist/avatar/avatar.tsx +110 -0
  11. package/dist/button/button.script.ts +35 -0
  12. package/dist/button/button.tsx +40 -0
  13. package/dist/checkbox/checkbox.script.ts +225 -0
  14. package/dist/checkbox/checkbox.tsx +156 -0
  15. package/dist/checkbox/context.ts +17 -0
  16. package/dist/checkbox-group/checkbox-group.tsx +111 -0
  17. package/dist/collapsible/collapsible.css +12 -0
  18. package/dist/collapsible/collapsible.script.ts +7 -0
  19. package/dist/collapsible/collapsible.tsx +153 -0
  20. package/dist/collapsible/panel.ts +115 -0
  21. package/dist/combobox/combobox.css +37 -0
  22. package/dist/combobox/combobox.script.ts +974 -0
  23. package/dist/combobox/combobox.tsx +556 -0
  24. package/dist/combobox/context.ts +46 -0
  25. package/dist/context-menu/context-menu.css +16 -0
  26. package/dist/context-menu/context-menu.script.ts +172 -0
  27. package/dist/context-menu/context-menu.tsx +178 -0
  28. package/dist/dialog/dialog.css +35 -0
  29. package/dist/dialog/dialog.script.ts +269 -0
  30. package/dist/dialog/dialog.tsx +214 -0
  31. package/dist/drawer/drawer.css +38 -0
  32. package/dist/drawer/drawer.script.ts +207 -0
  33. package/dist/drawer/drawer.tsx +113 -0
  34. package/dist/field/context.ts +156 -0
  35. package/dist/field/field.css +16 -0
  36. package/dist/field/field.script.ts +422 -0
  37. package/dist/field/field.tsx +465 -0
  38. package/dist/fieldset/fieldset.tsx +41 -0
  39. package/dist/form/form.tsx +50 -0
  40. package/dist/form/native.ts +21 -0
  41. package/dist/form/reset.ts +53 -0
  42. package/dist/index.ts +261 -0
  43. package/dist/input/input.tsx +67 -0
  44. package/dist/layer/layer.ts +191 -0
  45. package/dist/locale/digits.ts +21 -0
  46. package/dist/menu/menu.css +22 -0
  47. package/dist/menu/menu.script.ts +591 -0
  48. package/dist/menu/menu.tsx +520 -0
  49. package/dist/menubar/menubar.css +16 -0
  50. package/dist/menubar/menubar.script.ts +195 -0
  51. package/dist/menubar/menubar.tsx +67 -0
  52. package/dist/meter/meter.tsx +134 -0
  53. package/dist/navigation-menu/navigation-menu.css +14 -0
  54. package/dist/navigation-menu/navigation-menu.script.ts +466 -0
  55. package/dist/navigation-menu/navigation-menu.tsx +266 -0
  56. package/dist/number-field/format.ts +70 -0
  57. package/dist/number-field/number-field.css +38 -0
  58. package/dist/number-field/number-field.script.ts +490 -0
  59. package/dist/number-field/number-field.tsx +403 -0
  60. package/dist/otp-field/otp-field.css +45 -0
  61. package/dist/otp-field/otp-field.script.ts +330 -0
  62. package/dist/otp-field/otp-field.tsx +320 -0
  63. package/dist/placement/place.ts +148 -0
  64. package/dist/placement/placement.css +155 -0
  65. package/dist/placement/placement.ts +60 -0
  66. package/dist/popover/hover.ts +178 -0
  67. package/dist/popover/popover.css +20 -0
  68. package/dist/popover/popover.script.ts +256 -0
  69. package/dist/popover/popover.tsx +317 -0
  70. package/dist/preview-card/preview-card.css +7 -0
  71. package/dist/preview-card/preview-card.script.ts +136 -0
  72. package/dist/preview-card/preview-card.tsx +128 -0
  73. package/dist/progress/progress.tsx +155 -0
  74. package/dist/progress/value.ts +23 -0
  75. package/dist/radio-group/radio-group.tsx +156 -0
  76. package/dist/scroll-area/scroll-area.css +72 -0
  77. package/dist/scroll-area/scroll-area.script.ts +281 -0
  78. package/dist/scroll-area/scroll-area.tsx +118 -0
  79. package/dist/select/select.css +25 -0
  80. package/dist/select/select.tsx +171 -0
  81. package/dist/separator/separator.css +12 -0
  82. package/dist/separator/separator.tsx +38 -0
  83. package/dist/slider/format.ts +33 -0
  84. package/dist/slider/slider.css +93 -0
  85. package/dist/slider/slider.script.ts +355 -0
  86. package/dist/slider/slider.tsx +374 -0
  87. package/dist/styles.css +30 -0
  88. package/dist/switch/switch.tsx +92 -0
  89. package/dist/tabs/tabs.css +54 -0
  90. package/dist/tabs/tabs.script.ts +225 -0
  91. package/dist/tabs/tabs.tsx +272 -0
  92. package/dist/themes/default.css +2111 -0
  93. package/dist/toast/command.ts +3 -0
  94. package/dist/toast/toast.css +68 -0
  95. package/dist/toast/toast.script.ts +516 -0
  96. package/dist/toast/toast.tsx +432 -0
  97. package/dist/toggle/context.ts +16 -0
  98. package/dist/toggle/toggle.tsx +99 -0
  99. package/dist/toggle-group/toggle-group.script.ts +166 -0
  100. package/dist/toggle-group/toggle-group.tsx +117 -0
  101. package/dist/toolbar/context.ts +18 -0
  102. package/dist/toolbar/toolbar.css +12 -0
  103. package/dist/toolbar/toolbar.script.ts +123 -0
  104. package/dist/toolbar/toolbar.tsx +180 -0
  105. package/dist/tooltip/tooltip.css +8 -0
  106. package/dist/tooltip/tooltip.script.ts +222 -0
  107. package/dist/tooltip/tooltip.tsx +149 -0
  108. package/docs/README.md +174 -0
  109. package/docs/accordion.md +208 -0
  110. package/docs/autocomplete.md +161 -0
  111. package/docs/avatar.md +198 -0
  112. package/docs/button.md +98 -0
  113. package/docs/checkbox-group.md +112 -0
  114. package/docs/checkbox.md +133 -0
  115. package/docs/collapsible.md +165 -0
  116. package/docs/combobox.md +369 -0
  117. package/docs/context-menu.md +197 -0
  118. package/docs/dialog.md +256 -0
  119. package/docs/drawer.md +204 -0
  120. package/docs/field.md +295 -0
  121. package/docs/fieldset.md +92 -0
  122. package/docs/form.md +131 -0
  123. package/docs/input.md +87 -0
  124. package/docs/installation.md +65 -0
  125. package/docs/llms-full.txt +6923 -0
  126. package/docs/llms.txt +53 -0
  127. package/docs/menu.md +365 -0
  128. package/docs/menubar.md +186 -0
  129. package/docs/meter.md +103 -0
  130. package/docs/navigation-menu.md +257 -0
  131. package/docs/number-field.md +181 -0
  132. package/docs/otp-field.md +160 -0
  133. package/docs/popover.md +271 -0
  134. package/docs/preview-card.md +130 -0
  135. package/docs/progress.md +115 -0
  136. package/docs/radio-group.md +127 -0
  137. package/docs/scroll-area.md +147 -0
  138. package/docs/select.md +236 -0
  139. package/docs/separator.md +116 -0
  140. package/docs/slider.md +184 -0
  141. package/docs/switch.md +98 -0
  142. package/docs/tabs.md +254 -0
  143. package/docs/theming.md +132 -0
  144. package/docs/toast.md +311 -0
  145. package/docs/toggle-group.md +119 -0
  146. package/docs/toggle.md +94 -0
  147. package/docs/toolbar.md +138 -0
  148. package/docs/tooltip.md +128 -0
  149. package/package.json +102 -3
@@ -0,0 +1,112 @@
1
+ # Checkbox Group
2
+
3
+ > A group of checkboxes with one value, the list of the checked ones, after Base UI's CheckboxGroup: a native `<fieldset>` whose checkboxes share a name, so the form submits each checked value and a disabled group disables them all, without JavaScript. A parent checkbox checks them all and shows checked, mixed or unchecked as they are; `bind:value` keeps a signal in sync.
4
+
5
+ ```ts
6
+ import { CheckboxGroup, Checkbox, Fieldset } from "@lumiose/ui"; // or "@lumiose/ui/checkbox-group"
7
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
8
+ ```
9
+
10
+ ## Anatomy
11
+
12
+ ```tsx
13
+ <CheckboxGroup>
14
+ <Fieldset.Legend />
15
+ <Checkbox.Root parent />
16
+ <Checkbox.Root />
17
+ </CheckboxGroup>
18
+ ```
19
+
20
+ | Part | Renders |
21
+ | --------------- | ----------------------------------------------------------------------------------- |
22
+ | `CheckboxGroup` | `<fieldset>` around the checkboxes; a `Fieldset.Legend` first inside names it. |
23
+ | `Checkbox.Root` | A [Checkbox](checkbox.md) of the group, checked when its `value` is in the group's. |
24
+
25
+ ## API
26
+
27
+ ### CheckboxGroup
28
+
29
+ | Prop | Type | Default | |
30
+ | --------------- | --------------------------- | ------- | -------------------------------------------------------------------------------------- |
31
+ | `value` | `string[]` | | Checks the checkboxes with these values when it changes. Pair it with `onValueChange`. |
32
+ | `defaultValue` | `string[]` | `[]` | The values checked at first. |
33
+ | `onValueChange` | `(value: string[]) => void` | | Called with the checked values after the user checks or unchecks one. |
34
+ | `bind:value` | `Signal<string[]>` | | Stands in for `value` and `onValueChange`. |
35
+ | `allValues` | `string[]` | | Every checkbox's value: the parent checkbox's state in server HTML. |
36
+ | `name` | `string` | | The name the checkboxes submit under, unless they have their own. |
37
+ | `disabled` | `boolean` | `false` | Disables every checkbox (a disabled fieldset). |
38
+
39
+ Every other `<fieldset>` prop passes through.
40
+
41
+ **Differences from Base UI's CheckboxGroup.** It renders a `<fieldset>`, so a legend names it and
42
+ `disabled` is native. `name` is added: Base UI gives each checkbox its own. A parent checkbox
43
+ needs only `parent` here, not a controlled group, since the script runs it from the DOM. There is
44
+ no `required` (one box checked at least), as in Base UI: HTML has no such constraint for a group,
45
+ so it would work only with a script and an unlocalized message of ours. Check it in the form
46
+ action instead and return the message with `fail()`: it renders with the page, without JavaScript
47
+ too.
48
+
49
+ ## Example
50
+
51
+ Toppings in a server-rendered form, with a "select all" box:
52
+
53
+ ```tsx check
54
+ import { Checkbox, CheckboxGroup, Fieldset } from "@lumiose/ui";
55
+
56
+ const TOPPINGS = ["Cheese", "Olives", "Peppers"];
57
+
58
+ export function ToppingsGroup() {
59
+ return (
60
+ <CheckboxGroup
61
+ name="toppings"
62
+ defaultValue={["Cheese"]}
63
+ allValues={TOPPINGS}
64
+ class="lu-fieldset"
65
+ >
66
+ <Fieldset.Legend>Toppings</Fieldset.Legend>
67
+ <label>
68
+ <Checkbox.Root parent class="lu-checkbox" /> All toppings
69
+ </label>
70
+ {TOPPINGS.map((topping) => (
71
+ <label>
72
+ <Checkbox.Root value={topping} class="lu-checkbox" /> {topping}
73
+ </label>
74
+ ))}
75
+ </CheckboxGroup>
76
+ );
77
+ }
78
+ ```
79
+
80
+ ## Styling
81
+
82
+ **With the default theme**, put `lu-fieldset` on the group and `lu-checkbox` on each checkbox
83
+ ([Checkbox](checkbox.md#styling)). Style a parent checkbox with `[data-parent]` if it should look
84
+ different.
85
+
86
+ CheckboxGroup ships no stylesheet: it needs none to work.
87
+
88
+ ## Keyboard and focus
89
+
90
+ Each checkbox is its own tab stop; Space toggles the focused one. The parent is checked, mixed or
91
+ unchecked as all, some or none of the group's checkboxes are, disabled ones counted (as the server
92
+ counts them, from `allValues`). Toggling it goes, as Base UI's, from a partial selection to all,
93
+ then to none, then back to the selection the user last made with the checkboxes; disabled
94
+ checkboxes keep their state through it. Its `aria-controls` names the checkboxes, whose ids are
95
+ the group's and their values (`<id>-<value>`) unless you give them your own.
96
+
97
+ ## Without JavaScript, and the client script
98
+
99
+ The checkboxes are native: they toggle, submit under the group's name and reset with no script.
100
+ The parent checkbox shows its server state and is never submitted, but checks nothing until the
101
+ shared checkbox script (1.0 kB brotli, [Checkbox](checkbox.md#without-javascript-and-the-client-script))
102
+ runs.
103
+
104
+ ## Browser support
105
+
106
+ Every supported browser.
107
+
108
+ ## Related
109
+
110
+ - [Checkbox](checkbox.md): the checkboxes, `indeterminate` and `readOnly`.
111
+ - [Fieldset](fieldset.md): the legend.
112
+ - [Radio Group](radio-group.md): one value out of several.
@@ -0,0 +1,133 @@
1
+ # Checkbox
2
+
3
+ > A native `<input type="checkbox">`, after Base UI's Checkbox: Space and a label click toggle it, the form submits its value when checked, reset and `required` work, in server HTML and without JavaScript. `indeterminate`, `readOnly`, the parent checkbox of a [Checkbox Group](checkbox-group.md) and `bind:checked` come on top, with a 1.0 kB script shared with Radio Group and Switch.
4
+
5
+ ```ts
6
+ import { Checkbox } from "@lumiose/ui"; // or "@lumiose/ui/checkbox"
7
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
8
+ ```
9
+
10
+ ## Anatomy
11
+
12
+ ```tsx
13
+ <label>
14
+ <Checkbox.Root /> Label
15
+ </label>
16
+ ```
17
+
18
+ | Part | Renders |
19
+ | ------ | ----------------------------------------------------------------------------------------------------------------- |
20
+ | `Root` | `<input type="checkbox">`; `data-indeterminate` in server HTML while indeterminate, `aria-readonly` if read-only. |
21
+
22
+ Name it with a wrapping `<label>`, a `<label for>`, a `Field.Label`, or `aria-label`.
23
+
24
+ ## API
25
+
26
+ ### Checkbox.Root
27
+
28
+ | Prop | Type | Default | |
29
+ | ----------------- | ---------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------- |
30
+ | `checked` | `boolean` | | Checks or unchecks it when it changes. Pair it with `onCheckedChange`. |
31
+ | `defaultChecked` | `boolean` | `false` | Whether it starts checked; a form reset goes back to it. |
32
+ | `onCheckedChange` | `(checked: boolean) => void` | | Called after the user checks or unchecks it. |
33
+ | `bind:checked` | `Signal<boolean>` | | Stands in for `checked` and `onCheckedChange`. |
34
+ | `indeterminate` | `boolean` | `false` | Neither checked nor unchecked (mixed for assistive technology) until the user toggles it, and again after a form reset. |
35
+ | `value` | `string` | `"on"` | What it submits when checked, and its identity in a Checkbox Group. |
36
+ | `parent` | `boolean` | `false` | The "select all" checkbox of a Checkbox Group: it checks them all and follows them. Never submitted. |
37
+ | `readOnly` | `boolean` | `false` | It cannot be toggled, but stays focusable and is submitted. |
38
+ | `uncheckedValue` | `string` | | What it submits when unchecked (by the script); by default an unchecked box submits nothing. |
39
+
40
+ Every other native prop passes through: `name`, `required`, `disabled`, `form`, `id`, `class`,
41
+ `aria-*`. Inside a Field it takes the field's `id`, `name`, `aria-describedby` and
42
+ `aria-invalid`; inside a Checkbox Group, its `checked` state and `name`.
43
+
44
+ **Differences from Base UI's Checkbox.** The box is the native input, so there is no
45
+ `Checkbox.Indicator`: draw the box and its mark with `appearance: none` and `::before` (as the
46
+ default theme does). There is no `nativeButton` or `inputRef`. `uncheckedValue` needs the client
47
+ script: without it an unchecked box submits nothing, as a native one does.
48
+
49
+ ## Example
50
+
51
+ In an island, bound to a signal:
52
+
53
+ ```tsx check
54
+ import { Checkbox } from "@lumiose/ui";
55
+ import { signal } from "lumiose";
56
+
57
+ export function NewsletterOptIn() {
58
+ const subscribed = signal(false);
59
+ return (
60
+ <p>
61
+ <label>
62
+ <Checkbox.Root name="newsletter" bind:checked={subscribed} class="lu-checkbox" /> Send me
63
+ the newsletter
64
+ </label>{" "}
65
+ {subscribed() ? "Thanks!" : ""}
66
+ </p>
67
+ );
68
+ }
69
+ ```
70
+
71
+ ## Styling
72
+
73
+ **With the default theme**, put `lu-checkbox` on `Root`: an 18px box drawn by `::before` inside a
74
+ 24px target, a ✓ when checked and a dash when indeterminate (a symbol, not only a colour), the
75
+ primary colour for both, a focus ring, a red border with `aria-invalid="true"`.
76
+
77
+ **With your own CSS**, style the input and its state: `:checked`, `:indeterminate` (and
78
+ `[data-indeterminate]` before the script runs), `:disabled`, `:focus-visible`,
79
+ `[aria-readonly="true"]`, `[aria-invalid="true"]`.
80
+
81
+ ```css
82
+ .checkbox {
83
+ appearance: none;
84
+ inline-size: 1.25rem;
85
+ block-size: 1.25rem;
86
+ border: 1px solid #85858e;
87
+ border-radius: 0.25rem;
88
+ }
89
+ .checkbox:checked {
90
+ background: #2563eb;
91
+ }
92
+ .checkbox:checked::before {
93
+ content: "✓" / "";
94
+ color: #fff;
95
+ }
96
+ ```
97
+
98
+ Checkbox ships no stylesheet: it needs none to work.
99
+
100
+ ## Keyboard and focus
101
+
102
+ | Key | Does |
103
+ | ----- | ------------------------------------------------------ |
104
+ | Space | Toggles it (not when read-only). |
105
+ | Tab | Moves to the next control: a checkbox is one tab stop. |
106
+
107
+ Enter does not toggle it. A click on its label toggles it too.
108
+
109
+ ## Without JavaScript, and the client script
110
+
111
+ A native checkbox: toggling, submission, `required`, reset and `disabled` (its own or a fieldset's)
112
+ work with no script.
113
+
114
+ A page that renders a Checkbox, Checkbox Group, Radio Group or Switch loads one 1.0 kB (brotli)
115
+ script once, without the Lumiose runtime; islands bundle it. It turns server HTML's
116
+ `data-indeterminate` into the `indeterminate` property (which has no attribute), makes read-only
117
+ inputs ignore clicks, Space and the label, runs the parent checkbox of a group and a [Radio
118
+ Group](radio-group.md)'s arrow keys, and adds an
119
+ unchecked box's `uncheckedValue` (`data-unchecked-value`) to its form's data (the `formdata`
120
+ event, so it comes after the form's other values; a disabled box submits nothing). Without it, an
121
+ indeterminate box shows unchecked (the theme still draws it mixed), a read-only one toggles, and
122
+ an unchecked one submits nothing: treat a missing value as unchecked on the server too.
123
+
124
+ ## Browser support
125
+
126
+ Native checkboxes, `:indeterminate` and `appearance: none` with `::before`: every supported
127
+ browser.
128
+
129
+ ## Related
130
+
131
+ - [Checkbox Group](checkbox-group.md): several checkboxes with one value and a parent checkbox.
132
+ - [Switch](switch.md): on and off for a setting that applies at once.
133
+ - [Field](field.md): a label, a description and errors for a checkbox.
@@ -0,0 +1,165 @@
1
+ # Collapsible
2
+
3
+ > A panel that its trigger shows and hides: a native `<details>` whose `<summary>` is the trigger, so it works in server HTML with no JavaScript, keeps the browser's keyboard handling, and opens when find-in-page or a link lands inside it. A 0.7 kB script adds Base UI's animation hooks. `bind:open` (or `open` and `onOpenChange`) keeps a signal in sync in islands.
4
+
5
+ ```ts
6
+ import { Collapsible } from "@lumiose/ui"; // or "@lumiose/ui/collapsible"
7
+ import "@lumiose/ui/collapsible.css"; // a disabled trigger ignores the pointer
8
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
9
+ ```
10
+
11
+ ## Anatomy
12
+
13
+ ```tsx
14
+ <Collapsible.Root>
15
+ <Collapsible.Trigger />
16
+ <Collapsible.Panel />
17
+ </Collapsible.Root>
18
+ ```
19
+
20
+ | Part | Renders |
21
+ | --------- | ------------------------------------------------------------------------------------------- |
22
+ | `Root` | `<details>`, `open` while the panel shows. |
23
+ | `Trigger` | `<summary aria-controls="…">`, the first child of `Root`: it opens and closes the panel. |
24
+ | `Panel` | `<div>`, the rest of the `<details>`: the browser hides it while the `<details>` is closed. |
25
+
26
+ The browser exposes the `<summary>` as a button with its expanded state, so the trigger has no
27
+ `aria-expanded` of its own. HTML allows one heading inside a `<summary>`, for a trigger that
28
+ titles a section (Accordion renders its headings that way).
29
+
30
+ ## API
31
+
32
+ ### Collapsible.Root
33
+
34
+ | Prop | Type | Default | |
35
+ | -------------- | ------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
36
+ | `open` | `boolean` | | Opens or closes the panel when it changes. Server HTML starts as it says. |
37
+ | `defaultOpen` | `boolean` | `false` | Whether the panel starts open, when there is no `open`. |
38
+ | `onOpenChange` | `(open: boolean) => void` | | Called after the panel opens or closes, whatever did it: the trigger, find-in-page, a link, another item of its `name`; not when it renders open. |
39
+ | `bind:open` | `Signal<boolean>` | | Stands in for `open` and `onOpenChange`. |
40
+ | `disabled` | `boolean` | `false` | The trigger takes no focus and does not toggle the panel. |
41
+
42
+ Without `open`, `onOpenChange` or `bind:open` the element keeps its own state. `Root` takes the
43
+ props of a `<details>` too: `name` makes collapsibles with the same name an exclusive group, where
44
+ opening one closes the others.
45
+
46
+ ### The other parts
47
+
48
+ Each part takes the props of the element it renders (`class`, `style`, `aria-*`, `data-*`, event
49
+ handlers, `ref`) and passes them through. The `Trigger`'s `id` names it, and the `Panel` points at
50
+ it; the `Panel` keeps its own `id`, which the trigger points at (see [Ids](README.md#ids)), and
51
+ `aria-controls` stays the trigger's own.
52
+
53
+ - **`Trigger`**: a disabled trigger gets `aria-disabled="true"` and `tabindex="-1"`.
54
+ - **`Panel`**: the client script sets `--lu-collapsible-panel-height` and `--lu-collapsible-panel-width`
55
+ on it while it opens and closes, and `data-starting-style` and `data-ending-style`
56
+ ([Styling](#styling)).
57
+
58
+ ## Example
59
+
60
+ A section in a server-rendered page, with no island, styled by the default theme:
61
+
62
+ ```tsx check
63
+ import { Collapsible } from "@lumiose/ui";
64
+
65
+ export function RecoveryKeys() {
66
+ return (
67
+ <Collapsible.Root class="lu-collapsible">
68
+ <Collapsible.Trigger>Recovery keys</Collapsible.Trigger>
69
+ <Collapsible.Panel>
70
+ <p>Store these keys somewhere safe: each one lets you back in once.</p>
71
+ </Collapsible.Panel>
72
+ </Collapsible.Root>
73
+ );
74
+ }
75
+ ```
76
+
77
+ In an island, bound to a signal: `bind:open={open}` is short for `open={open()}` plus
78
+ `onOpenChange={(next) => open.set(next)}`.
79
+
80
+ ```tsx check
81
+ import { Collapsible } from "@lumiose/ui";
82
+ import { signal } from "lumiose";
83
+
84
+ export function AdvancedOptions() {
85
+ const open = signal(false);
86
+ return (
87
+ <>
88
+ <Collapsible.Root class="lu-collapsible" bind:open={open}>
89
+ <Collapsible.Trigger>Advanced options</Collapsible.Trigger>
90
+ <Collapsible.Panel>Proxy, timeouts and retries.</Collapsible.Panel>
91
+ </Collapsible.Root>
92
+ <p>{open() ? "Showing the advanced options" : "Advanced options hidden"}</p>
93
+ </>
94
+ );
95
+ }
96
+ ```
97
+
98
+ ## Styling
99
+
100
+ **With the default theme**, put `lu-collapsible` on `Root`: the trigger becomes a row with a
101
+ chevron that turns while the panel is open, and the panel's height animates.
102
+
103
+ **With your own CSS**, style the parts through `class` and `[open]` on the root. The client script
104
+ gives the panel what Base UI's does, for an open and close animation: `--lu-collapsible-panel-height`
105
+ (the content's height in pixels while it moves, `auto` once open), `data-starting-style` for the
106
+ frame an opening starts from and `data-ending-style` while it closes. The browser hides the panel
107
+ as soon as the `<details>` closes, so keep it visible while it closes with a transition on
108
+ `::details-content`:
109
+
110
+ ```css
111
+ .collapsible > .panel {
112
+ overflow: hidden;
113
+ block-size: var(--lu-collapsible-panel-height, auto);
114
+ transition: block-size 200ms ease-out;
115
+ }
116
+ .collapsible > .panel:is([data-starting-style], [data-ending-style]) {
117
+ block-size: 0;
118
+ }
119
+ .collapsible::details-content {
120
+ transition: content-visibility 200ms allow-discrete;
121
+ }
122
+ ```
123
+
124
+ A panel rendered open does not animate. `@lumiose/ui/collapsible.css` holds one rule, in the
125
+ `lu.core` layer: a disabled trigger (`summary[aria-disabled="true"]`) ignores the pointer.
126
+
127
+ ## Keyboard and focus
128
+
129
+ | Key | Does |
130
+ | ------------ | ------------------------------------------------------------------- |
131
+ | Enter, Space | On the trigger: opens or closes the panel. |
132
+ | Tab | Moves to the trigger, then into the open panel's focusable content. |
133
+
134
+ The trigger is a `<summary>`, so the browser handles its keys and its focus. A disabled trigger is
135
+ out of the tab order.
136
+
137
+ ## Without JavaScript, and the client script
138
+
139
+ Opening and closing, the exclusive group of a `name`, and find-in-page are HTML features: a
140
+ Collapsible in a server-rendered page works before any script loads, and with none. A disabled
141
+ trigger ignores the pointer through CSS. `open`, `onOpenChange` and `bind:open` need the component
142
+ to run in an island.
143
+
144
+ A page that renders a `Collapsible` loads a 0.7 kB (brotli) module once, without the Lumiose
145
+ runtime; islands bundle it. It covers every Collapsible and [Accordion](accordion.md) panel on the
146
+ page:
147
+
148
+ - the animation hooks above, set on every opening and closing, whatever caused it (the trigger,
149
+ find-in-page, a link, the `open` prop, another item of the group);
150
+ - a disabled trigger does not toggle, also from the keyboard.
151
+
152
+ ## Browser support
153
+
154
+ - **`<details>` and `<summary>`**: every browser. **`name`** (exclusive groups): Chrome 120,
155
+ Firefox 130, Safari 17.2.
156
+ - **Find-in-page and links into a closed panel** open it: Chrome 97, Firefox 139. Safari has not
157
+ shipped it yet (WebKit bug 304174): there, find-in-page skips a closed panel.
158
+ - **`::details-content`**, which the closing transition needs: Chrome 131, Firefox 143, Safari
159
+ 18.4. Elsewhere the panel disappears at once when it closes.
160
+
161
+ ## Related
162
+
163
+ - [Accordion](accordion.md): a stack of collapsible items, with arrow keys between them.
164
+ - [UI components](README.md): the approach, styling and the other components.
165
+ - [Theming](theming.md): the default theme's classes and custom properties.