@oxyhq/bloom 1.0.0 → 1.0.2

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 (218) hide show
  1. package/README.md +16 -4
  2. package/docs/accordion.mdx +69 -0
  3. package/docs/activity-heatmap.mdx +71 -0
  4. package/docs/admonition.mdx +77 -0
  5. package/docs/alert-dialog.mdx +75 -0
  6. package/docs/alert.mdx +93 -0
  7. package/docs/animated-check.mdx +53 -0
  8. package/docs/aspect-ratio.mdx +45 -0
  9. package/docs/avatar-group.mdx +91 -0
  10. package/docs/avatar.mdx +126 -0
  11. package/docs/badge.mdx +74 -0
  12. package/docs/benefit-list.mdx +56 -0
  13. package/docs/bottom-sheet.mdx +103 -0
  14. package/docs/button.mdx +218 -0
  15. package/docs/card.mdx +106 -0
  16. package/docs/checkbox.mdx +59 -0
  17. package/docs/chip.mdx +53 -0
  18. package/docs/code.mdx +50 -0
  19. package/docs/combobox.mdx +64 -0
  20. package/docs/command.mdx +85 -0
  21. package/docs/composition-bar.mdx +63 -0
  22. package/docs/connection-dots.mdx +50 -0
  23. package/docs/connection-status.mdx +56 -0
  24. package/docs/content-panel.mdx +62 -0
  25. package/docs/context-menu.mdx +65 -0
  26. package/docs/design-tokens.mdx +254 -0
  27. package/docs/dialog.mdx +277 -0
  28. package/docs/divider.mdx +56 -0
  29. package/docs/dot-grid-meter.mdx +66 -0
  30. package/docs/dropdown-menu.mdx +161 -0
  31. package/docs/error-boundary.mdx +72 -0
  32. package/docs/fab.mdx +67 -0
  33. package/docs/field.mdx +70 -0
  34. package/docs/fill.mdx +58 -0
  35. package/docs/fonts.mdx +144 -0
  36. package/docs/frosted-icon-button.mdx +60 -0
  37. package/docs/getting-started.mdx +75 -0
  38. package/docs/grid.mdx +64 -0
  39. package/docs/hooks.mdx +81 -0
  40. package/docs/icon-circle.mdx +66 -0
  41. package/docs/icons.mdx +69 -0
  42. package/docs/image-aspect-ratio-cache.mdx +63 -0
  43. package/docs/image-resolver.mdx +66 -0
  44. package/docs/index.mdx +44 -0
  45. package/docs/input-group.mdx +58 -0
  46. package/docs/item.mdx +69 -0
  47. package/docs/kbd.mdx +57 -0
  48. package/docs/label.mdx +50 -0
  49. package/docs/link-preview.mdx +66 -0
  50. package/docs/list.mdx +72 -0
  51. package/docs/loading.mdx +129 -0
  52. package/docs/media-inset-border.mdx +52 -0
  53. package/docs/menubar.mdx +86 -0
  54. package/docs/migrating-to-1.0.mdx +434 -0
  55. package/docs/motion.mdx +83 -0
  56. package/docs/overlay.mdx +75 -0
  57. package/docs/popover.mdx +73 -0
  58. package/docs/portal.mdx +63 -0
  59. package/docs/pressable-scale.mdx +51 -0
  60. package/docs/profile-card.mdx +66 -0
  61. package/docs/progressive-blur.mdx +54 -0
  62. package/docs/prompt-input.mdx +183 -0
  63. package/docs/provider.mdx +64 -0
  64. package/docs/radio-indicator.mdx +60 -0
  65. package/docs/radio.mdx +65 -0
  66. package/docs/scroll.mdx +66 -0
  67. package/docs/search.mdx +67 -0
  68. package/docs/segmented-control.mdx +76 -0
  69. package/docs/select.mdx +93 -0
  70. package/docs/settings-list.mdx +186 -0
  71. package/docs/skeleton.mdx +66 -0
  72. package/docs/slider.mdx +73 -0
  73. package/docs/stat-bar.mdx +59 -0
  74. package/docs/stat-widgets.mdx +33 -0
  75. package/docs/styles.mdx +61 -0
  76. package/docs/subtle-hover.mdx +53 -0
  77. package/docs/surfaces.mdx +72 -0
  78. package/docs/switch.mdx +90 -0
  79. package/docs/tab-bar.mdx +386 -0
  80. package/docs/tabs.mdx +75 -0
  81. package/docs/text-field.mdx +187 -0
  82. package/docs/theme.mdx +67 -0
  83. package/docs/toast.mdx +285 -0
  84. package/docs/tooltip.mdx +67 -0
  85. package/docs/typography.mdx +88 -0
  86. package/docs/user-hover-card.mdx +140 -0
  87. package/docs/zoomable-image-gallery.mdx +66 -0
  88. package/lib/commonjs/design-tokens/color-roles.js +21 -1
  89. package/lib/commonjs/design-tokens/color-roles.js.map +1 -1
  90. package/lib/commonjs/design-tokens/index.js +6 -0
  91. package/lib/commonjs/design-tokens/index.js.map +1 -1
  92. package/lib/commonjs/design-tokens/tailwind-preset.js +3 -1
  93. package/lib/commonjs/design-tokens/tailwind-preset.js.map +1 -1
  94. package/lib/commonjs/design-tokens/theme-css.js +1 -0
  95. package/lib/commonjs/design-tokens/theme-css.js.map +1 -1
  96. package/lib/commonjs/design-tokens/theme.css +3 -0
  97. package/lib/commonjs/design-tokens/tokens-json.js +13 -4
  98. package/lib/commonjs/design-tokens/tokens-json.js.map +1 -1
  99. package/lib/commonjs/design-tokens/tokens.json +6227 -265
  100. package/lib/commonjs/index.js +7 -0
  101. package/lib/commonjs/index.js.map +1 -1
  102. package/lib/commonjs/index.web.js +7 -0
  103. package/lib/commonjs/index.web.js.map +1 -1
  104. package/lib/commonjs/theme/ambient-store.js +16 -1
  105. package/lib/commonjs/theme/ambient-store.js.map +1 -1
  106. package/lib/commonjs/theme/color-policy.js +30 -33
  107. package/lib/commonjs/theme/color-policy.js.map +1 -1
  108. package/lib/commonjs/theme/color-presets.js +413 -166
  109. package/lib/commonjs/theme/color-presets.js.map +1 -1
  110. package/lib/commonjs/theme/color-scope/seed-scope.js +1 -0
  111. package/lib/commonjs/theme/color-scope/seed-scope.js.map +1 -1
  112. package/lib/commonjs/theme/glass-colors.js +18 -21
  113. package/lib/commonjs/theme/glass-colors.js.map +1 -1
  114. package/lib/commonjs/theme/index.js +30 -0
  115. package/lib/commonjs/theme/index.js.map +1 -1
  116. package/lib/commonjs/theme/index.web.js +30 -0
  117. package/lib/commonjs/theme/index.web.js.map +1 -1
  118. package/lib/module/design-tokens/color-roles.js +20 -0
  119. package/lib/module/design-tokens/color-roles.js.map +1 -1
  120. package/lib/module/design-tokens/index.js +2 -2
  121. package/lib/module/design-tokens/index.js.map +1 -1
  122. package/lib/module/design-tokens/tailwind-preset.js +4 -2
  123. package/lib/module/design-tokens/tailwind-preset.js.map +1 -1
  124. package/lib/module/design-tokens/theme-css.js +2 -1
  125. package/lib/module/design-tokens/theme-css.js.map +1 -1
  126. package/lib/module/design-tokens/theme.css +3 -0
  127. package/lib/module/design-tokens/tokens-json.js +14 -5
  128. package/lib/module/design-tokens/tokens-json.js.map +1 -1
  129. package/lib/module/design-tokens/tokens.json +6227 -265
  130. package/lib/module/index.js +1 -1
  131. package/lib/module/index.js.map +1 -1
  132. package/lib/module/index.web.js +1 -1
  133. package/lib/module/index.web.js.map +1 -1
  134. package/lib/module/theme/ambient-store.js +16 -1
  135. package/lib/module/theme/ambient-store.js.map +1 -1
  136. package/lib/module/theme/color-policy.js +30 -33
  137. package/lib/module/theme/color-policy.js.map +1 -1
  138. package/lib/module/theme/color-presets.js +412 -165
  139. package/lib/module/theme/color-presets.js.map +1 -1
  140. package/lib/module/theme/color-scope/seed-scope.js +2 -1
  141. package/lib/module/theme/color-scope/seed-scope.js.map +1 -1
  142. package/lib/module/theme/glass-colors.js +18 -21
  143. package/lib/module/theme/glass-colors.js.map +1 -1
  144. package/lib/module/theme/index.js +1 -1
  145. package/lib/module/theme/index.js.map +1 -1
  146. package/lib/module/theme/index.web.js +1 -1
  147. package/lib/module/theme/index.web.js.map +1 -1
  148. package/lib/typescript/commonjs/design-tokens/color-roles.d.ts +20 -0
  149. package/lib/typescript/commonjs/design-tokens/color-roles.d.ts.map +1 -1
  150. package/lib/typescript/commonjs/design-tokens/index.d.ts +3 -3
  151. package/lib/typescript/commonjs/design-tokens/index.d.ts.map +1 -1
  152. package/lib/typescript/commonjs/design-tokens/tailwind-preset.d.ts.map +1 -1
  153. package/lib/typescript/commonjs/design-tokens/theme-css.d.ts.map +1 -1
  154. package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts +12 -3
  155. package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts.map +1 -1
  156. package/lib/typescript/commonjs/index.d.ts +2 -2
  157. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  158. package/lib/typescript/commonjs/index.web.d.ts +2 -2
  159. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  160. package/lib/typescript/commonjs/theme/ambient-store.d.ts.map +1 -1
  161. package/lib/typescript/commonjs/theme/color-policy.d.ts.map +1 -1
  162. package/lib/typescript/commonjs/theme/color-presets.d.ts +386 -62
  163. package/lib/typescript/commonjs/theme/color-presets.d.ts.map +1 -1
  164. package/lib/typescript/commonjs/theme/color-scope/seed-scope.d.ts.map +1 -1
  165. package/lib/typescript/commonjs/theme/glass-colors.d.ts +18 -21
  166. package/lib/typescript/commonjs/theme/glass-colors.d.ts.map +1 -1
  167. package/lib/typescript/commonjs/theme/index.d.ts +2 -2
  168. package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
  169. package/lib/typescript/commonjs/theme/index.web.d.ts +2 -2
  170. package/lib/typescript/commonjs/theme/index.web.d.ts.map +1 -1
  171. package/lib/typescript/module/design-tokens/color-roles.d.ts +20 -0
  172. package/lib/typescript/module/design-tokens/color-roles.d.ts.map +1 -1
  173. package/lib/typescript/module/design-tokens/index.d.ts +3 -3
  174. package/lib/typescript/module/design-tokens/index.d.ts.map +1 -1
  175. package/lib/typescript/module/design-tokens/tailwind-preset.d.ts.map +1 -1
  176. package/lib/typescript/module/design-tokens/theme-css.d.ts.map +1 -1
  177. package/lib/typescript/module/design-tokens/tokens-json.d.ts +12 -3
  178. package/lib/typescript/module/design-tokens/tokens-json.d.ts.map +1 -1
  179. package/lib/typescript/module/index.d.ts +2 -2
  180. package/lib/typescript/module/index.d.ts.map +1 -1
  181. package/lib/typescript/module/index.web.d.ts +2 -2
  182. package/lib/typescript/module/index.web.d.ts.map +1 -1
  183. package/lib/typescript/module/theme/ambient-store.d.ts.map +1 -1
  184. package/lib/typescript/module/theme/color-policy.d.ts.map +1 -1
  185. package/lib/typescript/module/theme/color-presets.d.ts +386 -62
  186. package/lib/typescript/module/theme/color-presets.d.ts.map +1 -1
  187. package/lib/typescript/module/theme/color-scope/seed-scope.d.ts.map +1 -1
  188. package/lib/typescript/module/theme/glass-colors.d.ts +18 -21
  189. package/lib/typescript/module/theme/glass-colors.d.ts.map +1 -1
  190. package/lib/typescript/module/theme/index.d.ts +2 -2
  191. package/lib/typescript/module/theme/index.d.ts.map +1 -1
  192. package/lib/typescript/module/theme/index.web.d.ts +2 -2
  193. package/lib/typescript/module/theme/index.web.d.ts.map +1 -1
  194. package/package.json +5 -2
  195. package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
  196. package/src/__tests__/support/constructed-style-sheets.ts +68 -0
  197. package/src/__tests__/support/press-host.ts +30 -0
  198. package/src/__tests__/support/rendered-style.ts +99 -0
  199. package/src/__tests__/support/unread-hook-fixture.ts +33 -0
  200. package/src/design-tokens/color-roles.ts +21 -0
  201. package/src/design-tokens/index.ts +3 -2
  202. package/src/design-tokens/tailwind-preset.ts +3 -2
  203. package/src/design-tokens/theme-css.ts +2 -1
  204. package/src/design-tokens/theme.css +3 -0
  205. package/src/design-tokens/tokens-json.ts +29 -9
  206. package/src/design-tokens/tokens.json +6227 -265
  207. package/src/glass/GlassSurface.tsx +2 -2
  208. package/src/index.ts +2 -0
  209. package/src/index.web.ts +2 -0
  210. package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +4082 -0
  211. package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
  212. package/src/theme/ambient-store.ts +21 -1
  213. package/src/theme/color-policy.ts +33 -38
  214. package/src/theme/color-presets.ts +473 -146
  215. package/src/theme/color-scope/seed-scope.ts +2 -1
  216. package/src/theme/glass-colors.ts +18 -21
  217. package/src/theme/index.ts +16 -1
  218. package/src/theme/index.web.ts +16 -1
@@ -0,0 +1,61 @@
1
+ ---
2
+ title: Styles
3
+ description: Atoms, tokens, breakpoints and the platform helpers — for styles a className cannot express.
4
+ order: 74
5
+ ---
6
+
7
+ # Styles
8
+
9
+ `@oxyhq/bloom/styles` holds the style primitives Bloom itself uses.
10
+
11
+ ```tsx
12
+ import { atoms as a, tokens, BREAKPOINTS, web, native, select } from '@oxyhq/bloom/styles';
13
+ ```
14
+
15
+ ## `atoms`
16
+
17
+ Utility style objects — `a.flex_1`, `a.flex_row`, `a.align_center`, `a.px_sm`.
18
+ They are the inline equivalent of the Tailwind utilities, and they exist
19
+ because **a className is inert on web without the consumer's Tailwind
20
+ pipeline**. Anything Bloom must draw correctly with or without that pipeline
21
+ uses atoms; layout that only apps compose can use classes.
22
+
23
+ ## `tokens`
24
+
25
+ The t-shirt view of the scales — `tokens.space.lg`, `tokens.borderRadius.xl`.
26
+
27
+ The numbers themselves live in `design-tokens/scales.ts` and are read from
28
+ there by reference, so a rung cannot exist in one spelling and not the other.
29
+ They did once diverge: `borderRadius.full` was 999 against `radius-max` 9999,
30
+ and each file's comments claimed to match the other.
31
+
32
+ Prefer the numeric names (`radius-20`) when you are naming a rung in an API —
33
+ [`Card`](/docs/bloom/main/card)'s `radius` prop takes those — and the t-shirt
34
+ names in ad-hoc inline style.
35
+
36
+ ## `BREAKPOINTS`
37
+
38
+ The shared widths, so a component and the screen around it agree about when the
39
+ layout changes.
40
+
41
+ ## `web` / `native` / `ios` / `android` / `platform` / `select`
42
+
43
+ Platform-conditional style helpers. Use them for a genuine platform difference
44
+ — a shadow, a font smoothing, a scrollbar — and not to work around something
45
+ that should be the same on both.
46
+
47
+ ## `Z_INDEX`
48
+
49
+ **Within-context values only.** Which overlay paints on top is decided by the
50
+ overlay stack, not by a constant: `overlay/stack.ts` ranks surfaces by when
51
+ they OPENED, and a per-component z-index answers that question by what a
52
+ surface *is*, which leaves some pairings permanently inverted.
53
+
54
+ Never give an overlay surface a `zIndex` of its own.
55
+
56
+ ## `flatten`
57
+
58
+ `StyleSheet.flatten` re-exported for code that already depends on the RN
59
+ runtime. A raw-DOM web fork should use `flattenWebStyle()` instead — jest's RN
60
+ mock stubs `StyleSheet.flatten` as identity, so the array form would survive
61
+ and crash the DOM with `Failed to set an indexed property [0]`.
@@ -0,0 +1,53 @@
1
+ ---
2
+ title: SubtleHover
3
+ description: The hover wash behind a row — one tint, so every hoverable row matches.
4
+ order: 64
5
+ ---
6
+
7
+ # SubtleHover
8
+
9
+ A faint fill that appears behind its parent's content on hover. It is what
10
+ makes a list row feel hoverable without a border or a shadow.
11
+
12
+ ```tsx
13
+ import { SubtleHover } from '@oxyhq/bloom/subtle-hover';
14
+
15
+ <Pressable onPress={open} onHoverIn={…} onHoverOut={…}>
16
+ <SubtleHover active={hovered} />
17
+ <Row />
18
+ </Pressable>
19
+ ```
20
+
21
+ ## Which prop decides what
22
+
23
+ | prop | decides |
24
+ | -------- | ------------------------------------------------------------ |
25
+ | `active` | whether the wash is shown — the caller owns the hover state |
26
+ | `native` | whether it also renders on native, where there is no hover |
27
+
28
+ ## It does not detect hover
29
+
30
+ The caller does. That is deliberate: what counts as "hovered" depends on the
31
+ row — sometimes it is the row itself, often it is a larger container, and
32
+ occasionally it should stay lit while a menu opened from the row is up. A
33
+ component that owned the detection could not express the third case at all.
34
+
35
+ ## `native` defaults off, because hover does not exist there
36
+
37
+ On touch there is no hover state to reflect, and a wash that appears on press
38
+ duplicates the press feedback the row already has. Turn it on only for a native
39
+ surface that genuinely has a pointer — an iPad with a trackpad, a keyboard
40
+ focus ring you are mirroring.
41
+
42
+ ## One tint, everywhere
43
+
44
+ The value of this component is not the effect, which is three lines of style —
45
+ it is that every hoverable row in the ecosystem uses the *same* wash. Rolling
46
+ your own is how a menu and a list on the same screen end up hovering in
47
+ different greys.
48
+
49
+ ## Not a substitute for focus
50
+
51
+ Hover is for pointers. A keyboard user needs a visible focus state, which is a
52
+ separate thing and must not be styled away — a row that only responds to hover
53
+ is invisible to anyone navigating with a keyboard.
@@ -0,0 +1,72 @@
1
+ ---
2
+ title: Surfaces
3
+ description: The one imperative overlay API — alert, confirm, prompt, and the stack they present onto.
4
+ order: 36
5
+ ---
6
+
7
+ # Surfaces
8
+
9
+ The surface stack is Bloom's **one** imperative overlay API. `alert()`,
10
+ `confirm()` and `prompt()` present from anywhere — no ref, no local `open`
11
+ state, no second provider — and a single `<SurfaceHost>` renders whatever is on
12
+ the stack.
13
+
14
+ ```tsx
15
+ import { alert, confirm, prompt } from '@oxyhq/bloom/surfaces';
16
+
17
+ alert('Saved', 'Your changes are live.');
18
+
19
+ if (await confirm({ title: 'Leave this page?', confirmLabel: 'Leave' })) {
20
+ router.back();
21
+ }
22
+
23
+ const name = await prompt({ title: 'Name this list', placeholder: 'Reading list' });
24
+ ```
25
+
26
+ ## Why these are not components
27
+
28
+ They are answers to a **question**, so the call site wants a value back, and a
29
+ component you have to mount cannot be awaited. `confirm()` resolves to a
30
+ boolean and `prompt()` to a string or `null`. That difference is the whole
31
+ reason this API exists beside [`Dialog`](/docs/bloom/main/dialog):
32
+
33
+ - **Surfaces** — a one-shot question from inside an event handler, in code
34
+ that has no render tree of its own (a mutation callback, a service).
35
+ - **Dialog** — a surface with real content that belongs to a screen.
36
+
37
+ ## Mounting
38
+
39
+ `<SurfaceProvider>` wraps the app; `<SurfaceHost>` renders the stack. **Both
40
+ stay out of `BloomProvider`** — where an outlet sits is an app decision, and a
41
+ second mount would duplicate every surface.
42
+
43
+ ## Two calls in a row STACK, they do not queue
44
+
45
+ ```tsx
46
+ alert('First');
47
+ alert('Second'); // paints over the first; dismissing reveals it
48
+ ```
49
+
50
+ If you want them one after the other, `await` the first. This is deliberate: a
51
+ queue would delay an urgent surface behind a trivial one, and there is no
52
+ general rule for which of two surfaces matters more.
53
+
54
+ ## `present()` is the general form
55
+
56
+ ```tsx
57
+ const size = await present<string>(
58
+ (surface) => <SizeList onPick={(s) => surface.dismiss(s)} />,
59
+ { placement: 'bottom', title: 'Pick a size', label: 'Pick a size' },
60
+ );
61
+ ```
62
+
63
+ The content resolves the promise by calling `surface.dismiss(value)`. Note what
64
+ it does **not** call: `Dialog`'s own `close()`. The value has to reach the
65
+ `present()` promise, and it resolves on the press — a surface closed any other
66
+ way resolves with nothing, which reads as the user having cancelled.
67
+
68
+ ## Buttons
69
+
70
+ A built-in surface's buttons carry `shouldCloseOnPress: false` and dismiss
71
+ through `surface.dismiss(result)` for that reason. A `'cancel'` colour on a
72
+ `DialogAction` is purely visual; it does not make the button dismiss.
@@ -0,0 +1,90 @@
1
+ ---
2
+ title: Switch
3
+ description: An immediate on/off toggle — why it must carry `aria-checked` rather than `accessibilityState`, and why it cannot name itself.
4
+ order: 24
5
+ ---
6
+
7
+ # Switch
8
+
9
+ A binary control whose change takes effect **immediately**. That is the whole
10
+ distinction from a checkbox: a switch turns something on, a checkbox records an
11
+ intent you confirm later with a Save button.
12
+
13
+ ```tsx
14
+ import { Switch } from '@oxyhq/bloom/switch';
15
+
16
+ <Switch
17
+ value={pushEnabled}
18
+ onValueChange={setPushEnabled}
19
+ accessibilityLabel="Push notifications"
20
+ />
21
+ ```
22
+
23
+ ## Which prop decides what
24
+
25
+ | prop | decides |
26
+ | -------------------- | --------------------------------------------- |
27
+ | `value` | on or off — the component is fully controlled |
28
+ | `onValueChange` | receives the **opposite** of the current value |
29
+ | `disabled` | dims it and takes it out of the tab order |
30
+ | `size` | `default` (44×26) or `sm` (36×22) |
31
+ | `accessibilityLabel` | what it announces as — see below |
32
+
33
+ There is no uncontrolled mode. A switch whose state the caller does not own is a
34
+ switch that can disagree with the thing it is supposed to be controlling — the
35
+ classic version being an optimistic toggle that stays on after the request
36
+ failed.
37
+
38
+ ## A switch needs a label it does not draw
39
+
40
+ `Switch` renders a track and a thumb and nothing else, so there is no text for
41
+ either platform to compute a name from. **`accessibilityLabel` is the only
42
+ route to one.** Without it the control announces "switch, on" and never says
43
+ which of the settings on the page it belongs to.
44
+
45
+ The caption next to it does not do this job. A `SettingsListItem` title, a
46
+ `Label`, a `Text` in the same row — all of them are SIBLING elements, and
47
+ neither React Native nor react-native-web associates a sibling with a control.
48
+ So the row below needs the label even though the words are already on screen:
49
+
50
+ ```tsx
51
+ <SettingsListItem
52
+ title="Push notifications"
53
+ rightElement={
54
+ <Switch
55
+ value={push}
56
+ onValueChange={setPush}
57
+ accessibilityLabel="Push notifications"
58
+ />
59
+ }
60
+ showChevron={false}
61
+ />
62
+ ```
63
+
64
+ One spelling covers both platforms, and it is not the same rule as the state
65
+ below: react-native-web emits `aria-label` from `accessibilityLabel` when no
66
+ `aria-label` is given, and React Native reads it directly.
67
+
68
+ Omitting it warns once per component in development. The warning is dev-only
69
+ (`process.env.NODE_ENV`, folded out of production bundles) and never throws — an
70
+ unnamed switch still renders and still toggles. An empty or whitespace-only
71
+ label counts as missing, because `aria-label=""` names nothing.
72
+
73
+ ## The accessibility rule this component is the reference for
74
+
75
+ `Switch` sets `role="switch"` and **`aria-checked`**, not
76
+ `accessibilityState={{ checked }}`.
77
+
78
+ react-native-web drops `accessibilityState` entirely and reads only `aria-*`,
79
+ while React Native folds `aria-checked` back into `accessibilityState`. So
80
+ `aria-checked` is the one spelling that works on both platforms, and a control
81
+ that set only `accessibilityState` would render a switch role carrying no
82
+ state — drawn correctly, announced as nothing.
83
+
84
+ The disabled state is the mirror image and goes the other way: it travels on
85
+ the `disabled` **prop**, because react-native-web's `Pressable` appends its own
86
+ `aria-disabled` *after* spreading the caller's props, so a hand-written one is
87
+ silently overwritten.
88
+
89
+ Neither of these is visible to a prop-level test. Assert the rendered
90
+ attribute.
@@ -0,0 +1,386 @@
1
+ ---
2
+ title: TabBar
3
+ description: A floating, minimize-on-scroll tab bar with a sliding highlight, liquid glass on iOS 26 and a backdrop-filter on web.
4
+ order: 19
5
+ ---
6
+
7
+ # TabBar
8
+
9
+ A floating tab bar that sits over your content instead of boxing it in. It minimizes as you scroll down and re-expands as you scroll up, its highlight slides between tabs and can be scrubbed with a finger, and its capsule renders as real liquid glass on iOS 26, a CSS `backdrop-filter` on web, and a solid themed fill everywhere else.
10
+
11
+ It works with or without a router. `@oxyhq/bloom/tab-bar` has no router dependency at all; the expo-router bindings live in a separate `@oxyhq/bloom/tab-bar/expo-router` subpath.
12
+
13
+ ## Install
14
+
15
+ The tab bar adds three peers on top of Bloom's usual ones:
16
+
17
+ ```bash
18
+ bunx expo install expo-glass-effect expo-symbols react-native-screens
19
+ ```
20
+
21
+ | Peer | Used by | Needed when |
22
+ |------|---------|-------------|
23
+ | `expo-glass-effect` | `tab-bar/surface.native` | Any native build that renders the bar. Its NATIVE module is Apple-only (`"platforms": ["apple"]`) and provides the iOS 26 liquid-glass capsule; on Android the package is pure JS, its own `isLiquidGlassAvailable()` answers `false`, and the bar paints the solid themed fill instead. A build whose native module is missing altogether — an OTA JS update landing on a binary built before the package was linked — degrades to that same solid fill and logs one `[Bloom]` warning, rather than throwing out of render. |
24
+ | `expo-symbols` | `tab-bar/glyph.native` | Any native build that renders the bar. Apple-only as well: it draws `item.sfSymbol` as a real SF Symbol on iOS, and the bar falls back to `item.icon` everywhere else. |
25
+ | `react-native-screens` | `tab-bar/expo-router` | Only if you use `renderFadingTabScreen`. |
26
+
27
+ The bar also uses Bloom's existing `expo-blur`, `react-native-reanimated`, `react-native-gesture-handler` and `react-native-safe-area-context` peers. Native apps must wrap their root in `GestureHandlerRootView` — the bar is scrubbable, so it needs gesture-handler mounted.
28
+
29
+ <Callout variant="info" title="Web builds pull none of the native packages">
30
+ `expo-glass-effect` and `expo-symbols` are imported only from `.native` files, and the router packages only from the `expo-router` subpath. A web bundle of `@oxyhq/bloom/tab-bar` reaches none of them, so nothing about them ends up in a web app's output.
31
+
32
+ Both are nonetheless declared as **required** peers, deliberately. They are static imports, so omitting them from a native build that imports the tab bar does not degrade to anything — Metro fails to resolve the specifier and the build dies. The only thing marking them optional would buy is silencing the one warning that says so in advance. That applies on Android too: the native module there is Apple-only (`"platforms": ["apple"]`), but the JS package still has to be installed for the bundle to resolve.
33
+
34
+ That degrade-to-nothing only follows once you import the tab bar. An app that never imports `@oxyhq/bloom/tab-bar` at all — on native or web — never reaches these files, so leaving the peer unmet costs nothing; don't install two Apple-only packages just to quiet a warning about a surface you don't use. In practice bun 1.3.14 doesn't even print that warning, so the gap is silent regardless of whether you use the tab bar.
35
+ </Callout>
36
+
37
+ ## Usage without a router
38
+
39
+ Drive it like any controlled component: pass `activeIndex`, handle `onIndexChange`.
40
+
41
+ ```tsx
42
+ import { useState } from 'react';
43
+ import * as Icons from '@oxyhq/bloom/icons';
44
+ import { TabBar, TabBarButton, TabBarMinimizeProvider } from '@oxyhq/bloom/tab-bar';
45
+
46
+ const TABS = [
47
+ { name: 'home', label: 'Home', icon: <Icons.Home_Stroke2_Corner0_Rounded /> },
48
+ { name: 'search', label: 'Search', icon: <Icons.MagnifyingGlass_Stroke2_Corner0_Rounded /> },
49
+ { name: 'you', label: 'You', icon: <Icons.UserCircle_Stroke2_Corner0_Rounded /> },
50
+ ];
51
+
52
+ export function Shell() {
53
+ const [index, setIndex] = useState(0);
54
+
55
+ return (
56
+ <TabBarMinimizeProvider>
57
+ <Screen index={index} />
58
+ <TabBar activeIndex={index} onIndexChange={setIndex}>
59
+ {TABS.map((item, i) => (
60
+ <TabBarButton key={item.name} item={item} index={i} />
61
+ ))}
62
+ </TabBar>
63
+ </TabBarMinimizeProvider>
64
+ );
65
+ }
66
+ ```
67
+
68
+ `TabBarMinimizeProvider` wraps the whole tab tree once, so any screen that calls `useMinimizeOnScroll()` drives the same bar. It is optional — without it each consumer falls back to its own local progress, which keeps a screen working when it is rendered outside a tab layout.
69
+
70
+ ### No selection
71
+
72
+ An `activeIndex` that names no tab — negative, past the last tab, or fractional — means **no selection**: the highlight fades out where it stands and no tab is left tinted. Returning to a real index fades it back in at that tab rather than sliding to it, since there was no visible position to travel from. Scrubbing and tapping still work from there: the finger arms the highlight and a release selects, exactly as when a tab was already picked.
73
+
74
+ This is the state to use whenever the current screen is not a tab. An index derived from the route — `TABS.findIndex((tab) => tab.href === pathname)` — is `-1` on every screen outside the tab set, and passing it straight through is correct; there is nothing to special-case.
75
+
76
+ <Callout variant="warning" title="Omitting `activeIndex` is NOT the same as no selection">
77
+ Leaving the prop off entirely selects the focus-driven path, where each `TabBarButton` supplies `isFocused` and drives the highlight itself (this is what `RouterTabBar` does). That path always shows the highlight. Only a NUMBER that names no tab hides it.
78
+ </Callout>
79
+
80
+ ## Usage with expo-router
81
+
82
+ ```tsx
83
+ import * as Icons from '@oxyhq/bloom/icons';
84
+ import { TabBarMinimizeProvider } from '@oxyhq/bloom/tab-bar';
85
+ import {
86
+ RouterTabBar,
87
+ RouterTabBarButton,
88
+ renderFadingTabScreen,
89
+ } from '@oxyhq/bloom/tab-bar/expo-router';
90
+ import { Tabs, TabList, TabSlot, TabTrigger } from 'expo-router/ui';
91
+ import { router } from 'expo-router';
92
+
93
+ const TABS = [
94
+ { name: 'home', label: 'Home', href: '/', icon: <Icons.Home_Stroke2_Corner0_Rounded /> },
95
+ { name: 'search', label: 'Search', href: '/search', icon: <Icons.MagnifyingGlass_Stroke2_Corner0_Rounded /> },
96
+ { name: 'you', label: 'You', href: '/you', icon: <Icons.UserCircle_Stroke2_Corner0_Rounded /> },
97
+ ];
98
+
99
+ export default function TabLayout() {
100
+ return (
101
+ <TabBarMinimizeProvider>
102
+ <Tabs>
103
+ <TabSlot style={{ height: '100%' }} renderFn={renderFadingTabScreen} />
104
+ <TabList asChild>
105
+ <RouterTabBar onIndexChange={(i) => router.navigate(TABS[i].href)}>
106
+ {TABS.map(({ href, ...item }, index) => (
107
+ <TabTrigger key={item.name} name={item.name} href={href} asChild>
108
+ <RouterTabBarButton item={item} index={index} />
109
+ </TabTrigger>
110
+ ))}
111
+ </RouterTabBar>
112
+ </TabList>
113
+ </Tabs>
114
+ </TabBarMinimizeProvider>
115
+ );
116
+ }
117
+ ```
118
+
119
+ Two things about this shape are load-bearing:
120
+
121
+ - **Navigation runs through `onIndexChange`, not through the triggers.** The bar's gesture detector consumes the touches — that is what makes scrubbing possible — so a tap never reaches a trigger's own `Pressable`.
122
+ - **`RouterTabBar` takes no `activeIndex`.** With a router the highlight is driven by each trigger's own focus, which is what keeps it correct through deep links, back gestures and any other programmatic navigation. Setting both would put two writers on one shared value.
123
+
124
+ `renderFadingTabScreen` is a drop-in for `TabSlot`'s `renderFn`: identical to expo-router's default render, plus a fade and a micro-scale on the screen becoming focused. It is the only place `react-native-screens` is used.
125
+
126
+ ## Minimizing on scroll
127
+
128
+ Screens opt in with `useMinimizeOnScroll()`, which returns a scroll handler for `Animated.ScrollView`. Scrolling down minimizes the bar to icons only; scrolling up, or being near the top, expands it.
129
+
130
+ ```tsx
131
+ import Animated from 'react-native-reanimated';
132
+ import { useMinimizeOnScroll } from '@oxyhq/bloom/tab-bar';
133
+
134
+ export default function Feed() {
135
+ const onScroll = useMinimizeOnScroll();
136
+
137
+ return (
138
+ <Animated.ScrollView onScroll={onScroll} scrollEventThrottle={16}>
139
+ {/* … */}
140
+ </Animated.ScrollView>
141
+ );
142
+ }
143
+ ```
144
+
145
+ For anything else that should react to the same progress — a header that fades with the bar, say — read the raw shared value:
146
+
147
+ ```tsx
148
+ import { useTabBarMinimized } from '@oxyhq/bloom/tab-bar';
149
+
150
+ const minimized = useTabBarMinimized(); // SharedValue<number>, 0 = expanded, 1 = minimized
151
+ ```
152
+
153
+ <Callout variant="warning" title="List every shared value in your mapper's deps array">
154
+ On web without the react-native-worklets babel plugin — the production reality for every Oxy RN-Web app — reanimated drives a mapper off its dependency array, not off auto-tracked reads. `useAnimatedStyle(() => ({ opacity: minimized.value }), [minimized])` animates; the same mapper with `[]` runs once and freezes at the first frame, with no error anywhere. Native ignores the extra deps, so listing them is correct on both platforms.
155
+ </Callout>
156
+
157
+ ## Icons
158
+
159
+ `item.icon` is the primary API and takes any node — a Bloom icon, an SVG, your own component — exactly like `TabsTrigger` and `FrostedIconButton`. The bar renders it twice per tab (an inactive layer with the active layer crossfading on top) and injects the layer's tint as the icon's `fill`. An icon that sets its own `fill` (a brand mark, say) keeps it, so both layers render it identically and it never flickers.
160
+
161
+ `item.sfSymbol` is an **iOS-only enhancement**: when present, iOS renders the real system glyph instead, weight-matched to the platform and tinted natively. Android, web and every non-Metro resolver ignore it and render `item.icon`, so every item still needs one.
162
+
163
+ ```tsx
164
+ const TABS = [
165
+ {
166
+ name: 'home',
167
+ label: 'Home',
168
+ icon: <Icons.Home_Stroke2_Corner0_Rounded />,
169
+ sfSymbol: 'house.fill',
170
+ },
171
+ {
172
+ name: 'you',
173
+ label: 'You',
174
+ icon: <Icons.UserCircle_Stroke2_Corner0_Rounded />,
175
+ sfSymbol: 'person.crop.circle',
176
+ },
177
+ ];
178
+ ```
179
+
180
+ ### Icon sets that change SHAPE when selected
181
+
182
+ Tinting says everything an icon set needs to say only when its selected state *is* a different color. Plenty of sets — including Mention's — say it with a different **shape** instead: an outline glyph becomes a filled one, on a different path. A tint crossfade cannot express that, because both layers would draw the same path in two colors.
183
+
184
+ `item.activeIcon` gives the crossfade a second node, so the outline dissolves into the fill:
185
+
186
+ ```tsx
187
+ const TABS = [
188
+ {
189
+ name: 'home',
190
+ label: 'Home',
191
+ icon: <Home />, // outline: fill="none" + stroke
192
+ activeIcon: <HomeActive />, // filled, different path
193
+ },
194
+ ];
195
+ ```
196
+
197
+ It is optional and additive: an item with only `icon` renders it on both layers exactly as before. On iOS an item carrying `sfSymbol` renders the symbol on both layers and ignores `activeIcon` — a symbol is tinted natively, so the tint crossfade is still the right expression there.
198
+
199
+ <Callout variant="warning" title="An icon that paints from a `color` prop never lights up">
200
+ The bar tints a glyph by cloning the element with a `fill` prop. That is how Bloom's own icons (and raw SVG) take paint — they resolve `fill ?? style.color`. An icon set whose components read a **`color` prop** instead simply ignores the injected `fill`, and the result is a glyph that renders fine and never changes between the two layers. Nothing throws and nothing warns.
201
+
202
+ Two ways out, both explicit: pass `activeIcon` so selection is carried by the shape rather than by the tint, or pre-color both nodes yourself (`icon={<Home color={inactive} />}`, `activeIcon={<HomeActive color={active} />}`) — an element that already has a `fill` is never overridden, and one that paints from `color` was never going to be.
203
+ </Callout>
204
+
205
+ ## Long press
206
+
207
+ The bar's gesture detector consumes its touches — that is what makes scrubbing possible — so a `TabBarButton`'s own `onLongPress` never fires for a real finger. `onIndexLongPress` on the **bar** is the way to get one:
208
+
209
+ ```tsx
210
+ <TabBar
211
+ activeIndex={index}
212
+ onIndexChange={setIndex}
213
+ onIndexLongPress={(i) => TABS[i].name === 'you' && openAccountSheet()}
214
+ >
215
+ ```
216
+
217
+ It reports the index under the finger after a 500ms hold, resolved through the same geometry as a tap, and it does **not** select the tab — it is a secondary action (an account switcher on the profile tab, say), so the highlight stays where it was.
218
+
219
+ The gesture joins the bar's gesture race only when you pass the prop. Without it, nothing about a held finger changes: you can still rest a finger and then scrub. With it, a hold that reaches 500ms wins the race and cancels the scrub — the tap is unaffected either way, since it gives up at 400ms, before the long press can activate.
220
+
221
+ ## Laying out around the bar
222
+
223
+ The bar floats over your content (`position: absolute`), so nothing accounts for it. `useTabBarFootprint()` returns the space it physically occupies above the bottom of the window — its own height plus its own bottom offset, and nothing else:
224
+
225
+ ```tsx
226
+ import { useMinimizeOnScroll, useTabBarFootprint } from '@oxyhq/bloom/tab-bar';
227
+
228
+ export default function Feed() {
229
+ const onScroll = useMinimizeOnScroll();
230
+ const footprint = useTabBarFootprint();
231
+
232
+ return (
233
+ <>
234
+ <Animated.ScrollView
235
+ onScroll={onScroll}
236
+ scrollEventThrottle={16}
237
+ // The last row clears the pill by 12pt — that gap is yours, not Bloom's.
238
+ contentContainerStyle={{ paddingBottom: footprint + 12 }}
239
+ />
240
+ {/* A FAB anchored by `bottom` already carries its own gap: sit it right
241
+ on top of the footprint. */}
242
+ <Fab style={{ position: 'absolute', right: 16, bottom: footprint }} />
243
+ </>
244
+ );
245
+ }
246
+ ```
247
+
248
+ **No clearance is baked in.** Clearance is a per-call-site decision — a list wants breathing room above the pill, a FAB anchored by `bottom` supplies its own — and only you know which. Adding a margin inside the hook would place that FAB too high and make one word mean two things in a single file. Bloom returns the one number it actually knows.
249
+
250
+ <Callout variant="warning" title="Never add `insets.bottom` to it">
251
+ The value is measured from the true bottom of the window, because the bar folds the bottom safe-area inset into its own gap. `footprint + insets.bottom` — the reflex for anything anchored to the bottom edge — counts the home indicator twice and accounts 110pt for a 76pt bar, leaving a visible band of dead space under every list. Whatever you add on top is your own clearance, never the inset a second time.
252
+ </Callout>
253
+
254
+ It is the bar's **expanded** height, deliberately: the bar minimizes on the way down and re-expands on the way up, so measuring the minimized height would hide the end of a list the moment the user scrolled back to it.
255
+
256
+ ## Constraining the width on a tablet
257
+
258
+ By default the bar spans the window. That is right on a phone and wrong on a tablet: an iPad 11" gives you an 810pt pill in portrait and 1342pt in landscape, with 21pt glyphs adrift in cells hundreds of points wide. `maxWidth` caps it and centres it:
259
+
260
+ ```tsx
261
+ <TabBar activeIndex={index} onIndexChange={setIndex} maxWidth={480}>
262
+ ```
263
+
264
+ There is **no default** — omit the prop and the bar is exactly the bar it has always been. The value is a **ceiling, never a floor**: a window narrower than it keeps the full-bleed bar, so you can pass one number unconditionally from a shared tab layout and phones are untouched.
265
+
266
+ The important part is what comes with it: the item width, the sliding highlight's position and the tap / scrub / long-press hit-testing all derive from the **same** constrained width, so a tap still lands on the tab it is visibly over.
267
+
268
+ <Callout variant="warning" title="Do not narrow the bar with `style` instead">
269
+ `style={{ maxWidth: 480, alignSelf: 'center' }}` on the bar moves the pixels and nothing else. The highlight and the gesture worklet keep dividing the **window** width by the tab count, so the pill is sized and positioned for a bar that is no longer there — and on a wide window a tap near the right edge selects a tab two places to the left. `maxWidth` is a prop rather than something you can express in a style precisely because the geometry has to follow it.
270
+ </Callout>
271
+
272
+ Centring and the minimize animation compose without either knowing about the other: the constrained wrap is centred once by layout, and the pill's minimized inset stays a pair of equal animated margins **inside** it, so the bar shrinks about its own centre on the way down.
273
+
274
+ ## The blur behind the bar
275
+
276
+ The bar draws a progressive blur rising from the bottom edge of the screen — the band content dissolves into instead of meeting the pill at a hard line. It is full-bleed and 114pt tall at a zero bottom inset (the bar's bottom gap + its expanded height + 44pt of bleed above it), which is taller than the bar itself.
277
+
278
+ That matters if your screen floats its own chrome down there. A video scrubber, a compose FAB, a QR-scan button — anything inside that band is blurred, and **no `zIndex` gets it out**: z-order only sorts siblings within one stacking context, and your FAB lives inside the screen, which is an earlier sibling of the bar's host. Turn the band off instead:
279
+
280
+ ```tsx
281
+ <TabBar activeIndex={index} onIndexChange={setIndex} blur={false}>
282
+ ```
283
+
284
+ `false` renders no blur at all — not an invisible one, and not an empty absolutely-positioned view left behind. `true` (or omitting it) is the default. The object form tunes the strength on the same 0–100 `expo-blur` scale the standalone [`ProgressiveBlur`](#progressiveblur) uses, defaulting to 5:
285
+
286
+ ```tsx
287
+ <TabBar activeIndex={index} onIndexChange={setIndex} blur={{ intensity: 2 }}>
288
+ ```
289
+
290
+ The band stays full-bleed under `maxWidth`: it is the screen-edge scrim, not part of the pill, and constraining it would leave a floating blurred rectangle.
291
+
292
+ ## Theming
293
+
294
+ Every color comes from the active `BloomThemeProvider` preset and mode — there is no hardcoded palette, so the bar follows a preset or a light/dark switch for free.
295
+
296
+ | Role | Resolves from | Meaning |
297
+ |------|---------------|---------|
298
+ | `activeTint` | `colors.text` | Label and glyph of the tab the highlight is over. |
299
+ | `inactiveTint` | `colors.textSecondary` | Label and glyph of every other tab. |
300
+ | `highlight` | `colors.contrast50` | The sliding highlight pill. |
301
+ | `glassTint` | `colors.background` at 55% | Tint layered over the liquid glass (iOS 26) or `backdrop-filter` (web) material. |
302
+ | `solidFallback` | `colors.backgroundSecondary` at 94% | Near-opaque background used when there is no glass to lens. |
303
+
304
+ Override any subset with the `theme` prop; the rest keep their token-derived values.
305
+
306
+ ```tsx
307
+ <TabBar activeIndex={index} onIndexChange={setIndex} theme={{ highlight: 'rgb(255 0 128)' }}>
308
+ ```
309
+
310
+ Overrides are coalesced key by key, so an explicit `undefined` in a partial can never punch a hole in the resolved theme.
311
+
312
+ ## Per-platform behaviour
313
+
314
+ | Platform | Capsule surface | Blur behind the bar |
315
+ |----------|-----------------|---------------------|
316
+ | iOS 26 | Real `UIGlassEffect` liquid glass under `glassTint`, with the native corner configuration (true squircle + rim lighting) tracking the animated height. | Ten thin stacked `BlurView`s — iOS has no public variable-blur API, so the falloff is built from layer coverage. |
317
+ | iOS &lt; 26, Android | Solid `solidFallback` fill. `isLiquidGlassAvailable()` reports `false`, so `GlassView` is never mounted. | Same ten-layer stack. |
318
+ | Web | `backdrop-filter: blur(24px) saturate(180%)` under `glassTint`. Browsers without `backdrop-filter` simply skip it and keep the translucent tint. | One `backdrop-filter` faded with a `mask-image` — an order of magnitude cheaper than ten composited backdrop passes, and it traces the same curve as the native stack. |
319
+
320
+ The capsule radius rides on the surface itself rather than on a clipping parent: iOS 26 glass renders its own native corner configuration, which an RN mask would flatten.
321
+
322
+ ## ProgressiveBlur
323
+
324
+ The gradient blur behind the bar is also a standalone component. Use it behind any floating chrome — a transparent header, a docked composer — so content dissolves rather than meeting a hard blur line.
325
+
326
+ ```tsx
327
+ import { ProgressiveBlur } from '@oxyhq/bloom/progressive-blur';
328
+
329
+ <ProgressiveBlur
330
+ direction="top"
331
+ intensity={5}
332
+ style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 120 }}
333
+ />;
334
+ ```
335
+
336
+ | Prop | Type | Default | Description |
337
+ |------|------|---------|-------------|
338
+ | `direction?` | `'top' \| 'bottom'` | `'top'` | Which edge the blur is anchored to — strongest there, fading away. |
339
+ | `intensity?` | `number` | `5` | Blur strength at the anchored edge, on `expo-blur`'s 0–100 scale. Applied per layer on native; the web fork maps it onto the single CSS blur radius that approximates the same stack. |
340
+ | `style?` | `StyleProp<ViewStyle>` | — | Position and size it yourself; it has no intrinsic layout. |
341
+
342
+ The tail gradient is derived from the theme's page background, so it works on light and dark alike.
343
+
344
+ ## TabBar props
345
+
346
+ | Prop | Type | Default | Description |
347
+ |------|------|---------|-------------|
348
+ | `children` | `ReactNode` | — | The `TabBarButton`s, in bar order. |
349
+ | `activeIndex?` | `number` | — | **Controlled path.** The focused tab; an index that names no tab (`-1`, past the last, fractional) hides the highlight — see [No selection](#no-selection). Omit it with a router — use per-button `isFocused` instead. |
350
+ | `onIndexChange?` | `(index: number) => void` | — | Called with the chosen index on tap, on scrub release, or on keyboard / assistive-technology activation. |
351
+ | `onIndexLongPress?` | `(index: number) => void` | — | Called with the index under the finger after a 500ms hold. Does not select. The gesture is armed only when this is supplied. |
352
+ | `theme?` | `Partial<TabBarTheme>` | — | Partial override of the token-resolved theme. |
353
+ | `haptics?` | `boolean` | `true` | Picker-style tick as the scrub crosses tab boundaries. Already a no-op on web and when `expo-haptics` is absent. |
354
+ | `blur?` | `boolean \| { intensity?: number }` | `true` | The progressive blur band behind the bar. `false` removes it entirely — the escape hatch for a FAB or scrubber that sits inside its 114pt reach. |
355
+ | `maxWidth?` | `number` | — | Caps the pill's width and centres it, with the item geometry and the hit-testing following. A ceiling, never a floor: narrower windows keep the full-bleed bar. |
356
+
357
+ Everything `View` accepts is forwarded.
358
+
359
+ ## TabBarButton props
360
+
361
+ | Prop | Type | Default | Description |
362
+ |------|------|---------|-------------|
363
+ | `item` | `TabBarItem` | — | `{ name, label, icon, activeIcon?, sfSymbol? }`. |
364
+ | `index` | `number` | — | Position in the bar. Must match the child order. |
365
+ | `isFocused?` | `boolean` | — | **Focus-driven path**, supplied by the router adapter. When present it wins over the bar's `activeIndex` and this button drives the highlight. |
366
+
367
+ Everything `Pressable` accepts is forwarded, including the function form of `style`.
368
+
369
+ ## Exports
370
+
371
+ | Export | From | Description |
372
+ |--------|------|-------------|
373
+ | `TabBar`, `TabBarButton` | `@oxyhq/bloom/tab-bar` | The bar and its trigger. |
374
+ | `TabBarMinimizeProvider` | `@oxyhq/bloom/tab-bar` | Shares one minimize progress across the tab tree. |
375
+ | `useMinimizeOnScroll` | `@oxyhq/bloom/tab-bar` | Scroll handler for `Animated.ScrollView`. |
376
+ | `useTabBarMinimized` | `@oxyhq/bloom/tab-bar` | The raw `SharedValue<number>`, 0…1. |
377
+ | `useTabBarFootprint` | `@oxyhq/bloom/tab-bar` | Space the bar occupies at the bottom, safe-area included, no clearance added. |
378
+ | `useMinimizeState`, `setMinimized`, `MINIMIZE_SPRING` | `@oxyhq/bloom/tab-bar` | Lower-level control of the same progress. |
379
+ | `RouterTabBar`, `RouterTabBarButton`, `renderFadingTabScreen` | `@oxyhq/bloom/tab-bar/expo-router` | expo-router bindings. |
380
+ | `ProgressiveBlur` | `@oxyhq/bloom/progressive-blur` | The gradient blur, standalone. |
381
+
382
+ Types: `TabBarItem`, `TabBarProps`, `TabBarButtonProps`, `TabBarTheme`, `MinimizeState`, `ProgressiveBlurProps`.
383
+
384
+ <Callout variant="info" title="TabBar vs Tabs">
385
+ `TabBar` is app-level navigation chrome: floating, minimize-on-scroll, one per screen tree. For switching between panels *inside* a screen, reach for `Tabs` from `@oxyhq/bloom/tabs` instead — an inline, underlined control with no gestures and no surface of its own.
386
+ </Callout>