@cdx-ui/components 0.2.3 → 0.2.4

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 (248) hide show
  1. package/AGENTS.md +24 -0
  2. package/README.md +2 -0
  3. package/coverage.svg +1 -1
  4. package/docs/components/accordion/content.md +538 -0
  5. package/docs/components/alert/content.md +203 -0
  6. package/docs/components/alert-dialog/content.md +319 -0
  7. package/docs/components/avatar/content.md +368 -0
  8. package/docs/components/avatar/design.md +202 -0
  9. package/docs/components/badge/content.md +235 -0
  10. package/docs/components/bottom-sheet/content.md +351 -0
  11. package/docs/components/bottom-sheet/design.md +249 -0
  12. package/docs/components/breadcrumbs/content.md +169 -0
  13. package/docs/components/button/content.md +398 -0
  14. package/docs/components/button/design.md +258 -0
  15. package/docs/components/calendar/content.md +137 -0
  16. package/docs/components/card/content.md +283 -0
  17. package/docs/components/carousel/content.md +416 -0
  18. package/docs/components/checkbox/content.md +236 -0
  19. package/docs/components/checkbox/design.md +214 -0
  20. package/docs/components/chip/content.md +439 -0
  21. package/docs/components/chip/design.md +418 -0
  22. package/docs/components/color-area/content.md +86 -0
  23. package/docs/components/color-field/content.md +134 -0
  24. package/docs/components/color-picker/content.md +208 -0
  25. package/docs/components/color-slider/content.md +120 -0
  26. package/docs/components/color-swatch/content.md +64 -0
  27. package/docs/components/color-swatch-picker/content.md +126 -0
  28. package/docs/components/content.md +97 -0
  29. package/docs/components/currency-input/content.md +205 -0
  30. package/docs/components/data-grid/content.md +522 -0
  31. package/docs/components/date-field/content.md +147 -0
  32. package/docs/components/date-picker/content.md +360 -0
  33. package/docs/components/date-range-picker/content.md +301 -0
  34. package/docs/components/date-time-field/content.md +157 -0
  35. package/docs/components/date-time-list/content.md +105 -0
  36. package/docs/components/date-time-picker/content.md +272 -0
  37. package/docs/components/dialog/content.md +436 -0
  38. package/docs/components/dialog/design.md +192 -0
  39. package/docs/components/divider/content.md +112 -0
  40. package/docs/components/drawer/content.md +436 -0
  41. package/docs/components/field/content.md +277 -0
  42. package/docs/components/form/content.md +62 -0
  43. package/docs/components/full-screen-dialog/content.md +881 -0
  44. package/docs/components/heading/content.md +55 -0
  45. package/docs/components/icon/content.md +190 -0
  46. package/docs/components/icon-button/content.md +200 -0
  47. package/docs/components/icon-button/design.md +165 -0
  48. package/docs/components/image/content.md +104 -0
  49. package/docs/components/input/content.md +281 -0
  50. package/docs/components/input/design.md +389 -0
  51. package/docs/components/insurance-disclosure/content.md +103 -0
  52. package/docs/components/link/content.md +180 -0
  53. package/docs/components/link/design.md +160 -0
  54. package/docs/components/list-item/content.md +695 -0
  55. package/docs/components/menu/content.md +1247 -0
  56. package/docs/components/month-calendar/content.md +84 -0
  57. package/docs/components/month-picker/content.md +273 -0
  58. package/docs/components/otp-input/content.md +327 -0
  59. package/docs/components/progress-bar/content.md +87 -0
  60. package/docs/components/progress-circle/content.md +176 -0
  61. package/docs/components/progress-segmented/content.md +119 -0
  62. package/docs/components/quick-action-button/content.md +154 -0
  63. package/docs/components/quick-action-button/design.md +170 -0
  64. package/docs/components/radio/content.md +224 -0
  65. package/docs/components/range-calendar/content.md +123 -0
  66. package/docs/components/reorderable-list/content.md +309 -0
  67. package/docs/components/scoped-theme/content.md +47 -0
  68. package/docs/components/scroll-shadow/content.md +686 -0
  69. package/docs/components/select/content.md +400 -0
  70. package/docs/components/select/design.md +301 -0
  71. package/docs/components/skeleton/content.md +103 -0
  72. package/docs/components/slider/content.md +423 -0
  73. package/docs/components/stack/content.md +161 -0
  74. package/docs/components/switch/content.md +150 -0
  75. package/docs/components/switch/design.md +208 -0
  76. package/docs/components/table/content.md +548 -0
  77. package/docs/components/table-pagination/content.md +177 -0
  78. package/docs/components/tabs/content.md +292 -0
  79. package/docs/components/text/content.md +142 -0
  80. package/docs/components/textarea/content.md +222 -0
  81. package/docs/components/tile/content.md +703 -0
  82. package/docs/components/toast/content.md +456 -0
  83. package/docs/components/toggle-button/content.md +505 -0
  84. package/docs/components/tooltip/content.md +350 -0
  85. package/docs/components/virtualized-list/content.md +250 -0
  86. package/docs/foundations/content.md +9 -0
  87. package/docs/foundations/expressions/icons/content.md +19 -0
  88. package/docs/foundations/styles/tokens/all-tokens/content.md +192 -0
  89. package/docs/foundations/styles/tokens/what-are-tokens/content.md +33 -0
  90. package/docs/foundations/styles/tokens/what-are-tokens/design.md +33 -0
  91. package/docs/getting-started/content.md +4 -0
  92. package/docs/getting-started/designers/content.md +13 -0
  93. package/docs/getting-started/designers/design.md +13 -0
  94. package/docs/getting-started/developers/accessibility/content.md +31 -0
  95. package/docs/getting-started/developers/content.md +612 -0
  96. package/docs/getting-started/developers/styling/content.md +193 -0
  97. package/docs/llms.txt +193 -0
  98. package/docs/patterns/account-management/content.md +75 -0
  99. package/docs/patterns/authentication/content.md +52 -0
  100. package/docs/patterns/content.md +10 -0
  101. package/docs/patterns/dashboard/content.md +39 -0
  102. package/docs/patterns/empty-states/content.md +31 -0
  103. package/docs/patterns/forms/content.md +109 -0
  104. package/docs/patterns/onboarding/content.md +69 -0
  105. package/docs/patterns/payments/content.md +35 -0
  106. package/docs/patterns/settings/content.md +72 -0
  107. package/docs/resources/content.md +9 -0
  108. package/docs/resources/figma-libraries/content.md +42 -0
  109. package/docs/resources/release-notes/0.0.1-beta.118/content.md +27 -0
  110. package/docs/resources/release-notes/0.0.1-beta.122/content.md +21 -0
  111. package/docs/resources/release-notes/0.0.1-beta.124/content.md +16 -0
  112. package/docs/resources/release-notes/0.0.1-beta.126/content.md +10 -0
  113. package/docs/resources/release-notes/0.0.1-beta.127/content.md +14 -0
  114. package/docs/resources/release-notes/0.0.1-beta.128/content.md +9 -0
  115. package/docs/resources/release-notes/0.0.1-beta.129/content.md +18 -0
  116. package/docs/resources/release-notes/0.0.1-beta.130/content.md +18 -0
  117. package/docs/resources/release-notes/0.0.1-beta.133/content.md +7 -0
  118. package/docs/resources/release-notes/0.0.1-beta.134/content.md +17 -0
  119. package/docs/resources/release-notes/0.0.1-beta.136/content.md +7 -0
  120. package/docs/resources/release-notes/0.0.1-beta.137/content.md +10 -0
  121. package/docs/resources/release-notes/0.0.1-beta.138/content.md +7 -0
  122. package/docs/resources/release-notes/0.0.1-beta.139/content.md +13 -0
  123. package/docs/resources/release-notes/0.0.1-beta.140/content.md +26 -0
  124. package/docs/resources/release-notes/0.0.1-beta.141/content.md +63 -0
  125. package/docs/resources/release-notes/0.0.1-beta.142/content.md +7 -0
  126. package/docs/resources/release-notes/0.0.1-beta.143/content.md +12 -0
  127. package/docs/resources/release-notes/0.0.1-beta.144/content.md +7 -0
  128. package/docs/resources/release-notes/0.0.1-beta.145/content.md +7 -0
  129. package/docs/resources/release-notes/0.0.1-beta.147/content.md +10 -0
  130. package/docs/resources/release-notes/0.0.1-beta.149/content.md +10 -0
  131. package/docs/resources/release-notes/0.0.1-beta.150/content.md +13 -0
  132. package/docs/resources/release-notes/0.0.1-beta.152/content.md +7 -0
  133. package/docs/resources/release-notes/0.0.1-beta.154/content.md +15 -0
  134. package/docs/resources/release-notes/0.0.1-beta.156/content.md +7 -0
  135. package/docs/resources/release-notes/0.0.1-beta.158/content.md +9 -0
  136. package/docs/resources/release-notes/0.0.1-beta.159/content.md +11 -0
  137. package/docs/resources/release-notes/0.0.1-beta.160/content.md +7 -0
  138. package/docs/resources/release-notes/0.0.1-beta.161/content.md +7 -0
  139. package/docs/resources/release-notes/0.0.1-beta.163/content.md +7 -0
  140. package/docs/resources/release-notes/0.0.1-beta.164/content.md +9 -0
  141. package/docs/resources/release-notes/0.0.1-beta.166/content.md +17 -0
  142. package/docs/resources/release-notes/0.0.1-beta.167/content.md +7 -0
  143. package/docs/resources/release-notes/0.0.1-beta.168/content.md +7 -0
  144. package/docs/resources/release-notes/0.0.1-beta.169/content.md +7 -0
  145. package/docs/resources/release-notes/0.0.1-beta.171/content.md +16 -0
  146. package/docs/resources/release-notes/0.0.1-beta.172/content.md +24 -0
  147. package/docs/resources/release-notes/0.0.1-beta.173/content.md +15 -0
  148. package/docs/resources/release-notes/0.0.1-beta.174/content.md +7 -0
  149. package/docs/resources/release-notes/0.0.1-beta.176/content.md +32 -0
  150. package/docs/resources/release-notes/0.0.1-beta.179/content.md +11 -0
  151. package/docs/resources/release-notes/0.0.1-beta.182/content.md +13 -0
  152. package/docs/resources/release-notes/0.0.1-beta.185/content.md +7 -0
  153. package/docs/resources/release-notes/0.0.1-beta.187/content.md +27 -0
  154. package/docs/resources/release-notes/0.1.1/content.md +23 -0
  155. package/docs/resources/release-notes/0.1.2/content.md +7 -0
  156. package/docs/resources/release-notes/0.1.4/content.md +19 -0
  157. package/docs/resources/release-notes/0.1.5/content.md +7 -0
  158. package/docs/resources/release-notes/0.1.6/content.md +7 -0
  159. package/docs/resources/release-notes/0.2.0/content.md +31 -0
  160. package/docs/resources/release-notes/0.2.1/content.md +17 -0
  161. package/docs/resources/release-notes/0.2.2/content.md +11 -0
  162. package/docs/resources/release-notes/0.2.3/content.md +15 -0
  163. package/docs/resources/release-notes/content.md +64 -0
  164. package/lib/commonjs/components/DateTimeList/index.js +11 -26
  165. package/lib/commonjs/components/DateTimeList/index.js.map +1 -1
  166. package/lib/commonjs/components/DateTimeList/styles.js +2 -12
  167. package/lib/commonjs/components/DateTimeList/styles.js.map +1 -1
  168. package/lib/commonjs/components/ListItem/index.js.map +1 -1
  169. package/lib/commonjs/components/ListItem/styles.js +2 -1
  170. package/lib/commonjs/components/ListItem/styles.js.map +1 -1
  171. package/lib/commonjs/components/Menu/index.js +11 -5
  172. package/lib/commonjs/components/Menu/index.js.map +1 -1
  173. package/lib/commonjs/components/ScrollShadow/gradient.js +29 -0
  174. package/lib/commonjs/components/ScrollShadow/gradient.js.map +1 -0
  175. package/lib/commonjs/components/ScrollShadow/index.js +171 -44
  176. package/lib/commonjs/components/ScrollShadow/index.js.map +1 -1
  177. package/lib/commonjs/components/ScrollShadow/styles.js +58 -14
  178. package/lib/commonjs/components/ScrollShadow/styles.js.map +1 -1
  179. package/lib/commonjs/components/ScrollShadow/useComposedScrollHandler.js +63 -0
  180. package/lib/commonjs/components/ScrollShadow/useComposedScrollHandler.js.map +1 -0
  181. package/lib/commonjs/components/ScrollShadow/useScrollShadow.js +48 -8
  182. package/lib/commonjs/components/ScrollShadow/useScrollShadow.js.map +1 -1
  183. package/lib/commonjs/components/Tabs/index.js +16 -23
  184. package/lib/commonjs/components/Tabs/index.js.map +1 -1
  185. package/lib/commonjs/components/Tabs/styles.js +1 -10
  186. package/lib/commonjs/components/Tabs/styles.js.map +1 -1
  187. package/lib/commonjs/figma/ListItem.SectionHeader.figma.js +20 -5
  188. package/lib/commonjs/figma/ListItem.SectionHeader.figma.js.map +1 -1
  189. package/lib/module/components/DateTimeList/index.js +12 -27
  190. package/lib/module/components/DateTimeList/index.js.map +1 -1
  191. package/lib/module/components/DateTimeList/styles.js +1 -11
  192. package/lib/module/components/DateTimeList/styles.js.map +1 -1
  193. package/lib/module/components/ListItem/index.js.map +1 -1
  194. package/lib/module/components/ListItem/styles.js +2 -1
  195. package/lib/module/components/ListItem/styles.js.map +1 -1
  196. package/lib/module/components/Menu/index.js +11 -5
  197. package/lib/module/components/Menu/index.js.map +1 -1
  198. package/lib/module/components/ScrollShadow/gradient.js +24 -0
  199. package/lib/module/components/ScrollShadow/gradient.js.map +1 -0
  200. package/lib/module/components/ScrollShadow/index.js +170 -48
  201. package/lib/module/components/ScrollShadow/index.js.map +1 -1
  202. package/lib/module/components/ScrollShadow/styles.js +57 -13
  203. package/lib/module/components/ScrollShadow/styles.js.map +1 -1
  204. package/lib/module/components/ScrollShadow/useComposedScrollHandler.js +57 -0
  205. package/lib/module/components/ScrollShadow/useComposedScrollHandler.js.map +1 -0
  206. package/lib/module/components/ScrollShadow/useScrollShadow.js +46 -8
  207. package/lib/module/components/ScrollShadow/useScrollShadow.js.map +1 -1
  208. package/lib/module/components/Tabs/index.js +17 -24
  209. package/lib/module/components/Tabs/index.js.map +1 -1
  210. package/lib/module/components/Tabs/styles.js +0 -9
  211. package/lib/module/components/Tabs/styles.js.map +1 -1
  212. package/lib/module/figma/ListItem.SectionHeader.figma.js +20 -5
  213. package/lib/module/figma/ListItem.SectionHeader.figma.js.map +1 -1
  214. package/lib/typescript/components/DateTimeList/index.d.ts.map +1 -1
  215. package/lib/typescript/components/DateTimeList/styles.d.ts +1 -4
  216. package/lib/typescript/components/DateTimeList/styles.d.ts.map +1 -1
  217. package/lib/typescript/components/ListItem/index.d.ts +5 -4
  218. package/lib/typescript/components/ListItem/index.d.ts.map +1 -1
  219. package/lib/typescript/components/ListItem/styles.d.ts +1 -1
  220. package/lib/typescript/components/ListItem/styles.d.ts.map +1 -1
  221. package/lib/typescript/components/Menu/index.d.ts.map +1 -1
  222. package/lib/typescript/components/ScrollShadow/gradient.d.ts +7 -0
  223. package/lib/typescript/components/ScrollShadow/gradient.d.ts.map +1 -0
  224. package/lib/typescript/components/ScrollShadow/index.d.ts +57 -15
  225. package/lib/typescript/components/ScrollShadow/index.d.ts.map +1 -1
  226. package/lib/typescript/components/ScrollShadow/styles.d.ts +8 -2
  227. package/lib/typescript/components/ScrollShadow/styles.d.ts.map +1 -1
  228. package/lib/typescript/components/ScrollShadow/useComposedScrollHandler.d.ts +13 -0
  229. package/lib/typescript/components/ScrollShadow/useComposedScrollHandler.d.ts.map +1 -0
  230. package/lib/typescript/components/ScrollShadow/useScrollShadow.d.ts +15 -2
  231. package/lib/typescript/components/ScrollShadow/useScrollShadow.d.ts.map +1 -1
  232. package/lib/typescript/components/Tabs/index.d.ts.map +1 -1
  233. package/lib/typescript/components/Tabs/styles.d.ts +0 -7
  234. package/lib/typescript/components/Tabs/styles.d.ts.map +1 -1
  235. package/lib/typescript/figma/ListItem.SectionHeader.figma.d.ts.map +1 -1
  236. package/package.json +8 -6
  237. package/src/components/DateTimeList/index.tsx +13 -26
  238. package/src/components/DateTimeList/styles.ts +1 -10
  239. package/src/components/ListItem/index.tsx +5 -4
  240. package/src/components/ListItem/styles.ts +1 -0
  241. package/src/components/Menu/index.tsx +11 -8
  242. package/src/components/ScrollShadow/gradient.ts +48 -0
  243. package/src/components/ScrollShadow/index.tsx +300 -69
  244. package/src/components/ScrollShadow/styles.ts +45 -28
  245. package/src/components/ScrollShadow/useComposedScrollHandler.ts +58 -0
  246. package/src/components/ScrollShadow/useScrollShadow.ts +72 -10
  247. package/src/components/Tabs/index.tsx +25 -35
  248. package/src/components/Tabs/styles.ts +0 -9
@@ -0,0 +1,686 @@
1
+ # ScrollShadow (https://design.candescent.com/docs/components/scroll-shadow)
2
+
3
+ Resources: [Storybook](https://victorious-wave-0daadf00f.6.azurestaticapps.net/?path=/story/layout-scrollshadow--default)
4
+
5
+ `ScrollShadow` wraps a single scroll view and fades its edges into the surface behind it, showing a fade only on the edge that still has content.
6
+
7
+ ```tsx
8
+ import { ScrollShadow } from '@cdx-ui/components';
9
+ ```
10
+
11
+ > [!NOTE]
12
+ >
13
+ > **Wrap the scroll view directly**
14
+ >
15
+ > On iOS, Android, and web, `ScrollShadow` is a wrapper, not a scroll container. It doesn't scroll
16
+ > anything itself, so you always bring your own `ScrollView`, `FlatList`, or `VirtualizedList`, and
17
+ > it must be the direct child. `ScrollShadow` reads that child's scroll events, so a component that
18
+ > renders a scroll view inside it gets no fades, even on web. The one exception is a forced
19
+ > `visibility`, covered under Visibility.
20
+
21
+ ## Basic
22
+
23
+ Put the scroll view directly inside `ScrollShadow`. Orientation is inferred from the child's `horizontal` prop and defaults to vertical. Match `surface` to the panel behind the scroller so the fade blends in.
24
+
25
+ ```tsx
26
+ import {
27
+ Avatar,
28
+ Card,
29
+ Heading,
30
+ ListItem,
31
+ ScrollShadow,
32
+ ScrollView,
33
+ Text,
34
+ } from '@cdx-ui/components';
35
+ import { TRANSACTIONS } from './_data';
36
+
37
+ export default function ScrollShadowBasicDemo() {
38
+ return (
39
+ <Card fullBleed className="w-full max-w-sm">
40
+ <Card.Header>
41
+ <Heading size="sm">Recent activity</Heading>
42
+ <Text size="sm" color="secondary">
43
+ Checking · 6643
44
+ </Text>
45
+ </Card.Header>
46
+ <Card.Content>
47
+ <ScrollShadow className="h-80" surface="primary">
48
+ <ScrollView>
49
+ {TRANSACTIONS.map((transaction, index) => (
50
+ <ListItem key={transaction.id} showSeparator={index < TRANSACTIONS.length - 1}>
51
+ <ListItem.LeadingSlot>
52
+ <Avatar color={transaction.isCredit ? 'success' : 'neutral'}>
53
+ <Avatar.Icon as={transaction.icon} />
54
+ </Avatar>
55
+ </ListItem.LeadingSlot>
56
+ <ListItem.Content>
57
+ <ListItem.Title>{transaction.merchant}</ListItem.Title>
58
+ <ListItem.Description>{transaction.date}</ListItem.Description>
59
+ </ListItem.Content>
60
+ <ListItem.TrailingSlot>
61
+ <Text color={transaction.isCredit ? 'success' : 'primary'}>
62
+ {transaction.amount}
63
+ </Text>
64
+ </ListItem.TrailingSlot>
65
+ </ListItem>
66
+ ))}
67
+ </ScrollView>
68
+ </ScrollShadow>
69
+ </Card.Content>
70
+ </Card>
71
+ );
72
+ }
73
+ ```
74
+
75
+ ## Horizontal
76
+
77
+ A horizontal child gets fades on its start and end edges. Those edges are logical, not physical: in a left-to-right layout start is left and end is right; in RTL, start is right and end is left. You never pass `left` or `right`. The same `ScrollShadow` flips with the page direction. For a filter row, put a `ToggleButton.Group` with `isAttached={false}` inside the horizontal `ScrollView`.
78
+
79
+ ```tsx
80
+ import { useState } from 'react';
81
+ import {
82
+ Avatar,
83
+ Box,
84
+ Card,
85
+ Divider,
86
+ Heading,
87
+ ListItem,
88
+ ScrollShadow,
89
+ ScrollView,
90
+ Text,
91
+ ToggleButton,
92
+ } from '@cdx-ui/components';
93
+ import { ACTIVITY_FILTERS, TRANSACTIONS } from './_data';
94
+
95
+ const VISIBLE_ROWS = 4;
96
+
97
+ export default function ScrollShadowHorizontalDemo() {
98
+ const [filters, setFilters] = useState<string[]>(['purchases', 'dining']);
99
+
100
+ const matches =
101
+ filters.length > 0
102
+ ? TRANSACTIONS.filter((transaction) => filters.includes(transaction.category))
103
+ : TRANSACTIONS;
104
+ const rows = matches.slice(0, VISIBLE_ROWS);
105
+
106
+ return (
107
+ <Card fullBleed className="w-full max-w-sm">
108
+ <Card.Header>
109
+ <Heading size="sm">Activity</Heading>
110
+ <Text size="sm" color="secondary">
111
+ {matches.length} of {TRANSACTIONS.length}
112
+ </Text>
113
+ </Card.Header>
114
+ <Card.Content>
115
+ <Box className="py-3">
116
+ <ScrollShadow surface="primary" hideScrollBar>
117
+ <ScrollView
118
+ horizontal
119
+ showsHorizontalScrollIndicator={false}
120
+ contentContainerClassName="px-5"
121
+ >
122
+ <ToggleButton.Group
123
+ type="multiple"
124
+ isAttached={false}
125
+ value={filters}
126
+ onValueChange={setFilters}
127
+ aria-label="Filter activity by type"
128
+ >
129
+ {ACTIVITY_FILTERS.map((filter) => (
130
+ <ToggleButton key={filter.value} value={filter.value} size="small">
131
+ <ToggleButton.Icon as={filter.icon} position="leading" />
132
+ <ToggleButton.Label>{filter.label}</ToggleButton.Label>
133
+ </ToggleButton>
134
+ ))}
135
+ </ToggleButton.Group>
136
+ </ScrollView>
137
+ </ScrollShadow>
138
+ </Box>
139
+ <Divider />
140
+ {rows.map((transaction, index) => (
141
+ <ListItem key={transaction.id} showSeparator={index < rows.length - 1}>
142
+ <ListItem.LeadingSlot>
143
+ <Avatar color={transaction.isCredit ? 'success' : 'neutral'}>
144
+ <Avatar.Icon as={transaction.icon} />
145
+ </Avatar>
146
+ </ListItem.LeadingSlot>
147
+ <ListItem.Content>
148
+ <ListItem.Title>{transaction.merchant}</ListItem.Title>
149
+ <ListItem.Description>{transaction.date}</ListItem.Description>
150
+ </ListItem.Content>
151
+ <ListItem.TrailingSlot>
152
+ <Text color={transaction.isCredit ? 'success' : 'primary'}>{transaction.amount}</Text>
153
+ </ListItem.TrailingSlot>
154
+ </ListItem>
155
+ ))}
156
+ </Card.Content>
157
+ </Card>
158
+ );
159
+ }
160
+ ```
161
+
162
+ ## Surfaces
163
+
164
+ `surface` picks the fade color from the theme: `background`, `primary`, or `secondary`. The fade follows dark mode and `ScopedTheme`.
165
+
166
+ ```tsx
167
+ import {
168
+ Card,
169
+ QuickActionButton,
170
+ ScrollShadow,
171
+ ScrollView,
172
+ Text,
173
+ VStack,
174
+ type ScrollShadowProps,
175
+ } from '@cdx-ui/components';
176
+ import { QUICK_ACTIONS } from './_data';
177
+
178
+ type Surface = NonNullable<ScrollShadowProps['surface']>;
179
+
180
+ const panels: { surface: Surface; className?: string }[] = [
181
+ { surface: 'background', className: 'bg-surface-background' },
182
+ { surface: 'primary' },
183
+ { surface: 'secondary', className: 'bg-surface-secondary' },
184
+ ];
185
+
186
+ export default function ScrollShadowSurfacesDemo() {
187
+ return (
188
+ <VStack space="lg" className="w-full max-w-sm">
189
+ {panels.map(({ surface, className }) => (
190
+ <VStack key={surface} space="xs">
191
+ <Text size="sm" color="secondary">
192
+ {surface}
193
+ </Text>
194
+ <Card fullBleed className={className}>
195
+ <Card.Content className="py-5">
196
+ <ScrollShadow surface={surface} size="large">
197
+ <ScrollView
198
+ horizontal
199
+ showsHorizontalScrollIndicator={false}
200
+ contentContainerClassName="gap-4 px-5"
201
+ >
202
+ {QUICK_ACTIONS.map((action) => (
203
+ <QuickActionButton key={action.label}>
204
+ <QuickActionButton.Icon as={action.icon} />
205
+ <QuickActionButton.Label>{action.label}</QuickActionButton.Label>
206
+ </QuickActionButton>
207
+ ))}
208
+ </ScrollView>
209
+ </ScrollShadow>
210
+ </Card.Content>
211
+ </Card>
212
+ </VStack>
213
+ ))}
214
+ </VStack>
215
+ );
216
+ }
217
+ ```
218
+
219
+ ## Sizes
220
+
221
+ `size` sets the fade depth along the scroll axis: `small` is 32px, `default` is 40px, and `large` is 72px.
222
+
223
+ ```tsx
224
+ import {
225
+ Avatar,
226
+ Card,
227
+ Divider,
228
+ Heading,
229
+ ScrollShadow,
230
+ ScrollView,
231
+ Text,
232
+ VStack,
233
+ type AvatarColor,
234
+ type ScrollShadowProps,
235
+ } from '@cdx-ui/components';
236
+ import { PAYEES } from './_data';
237
+
238
+ const sizes: NonNullable<ScrollShadowProps['size']>[] = ['small', 'default', 'large'];
239
+
240
+ const avatarColors: AvatarColor[] = ['brand', 'info', 'success', 'warning', 'danger', 'neutral'];
241
+
242
+ function Payees() {
243
+ return PAYEES.map((name, index) => (
244
+ <VStack key={name} space="xs" className="items-center">
245
+ <Avatar size="lg" color={avatarColors[index % avatarColors.length]}>
246
+ <Avatar.Text>{name}</Avatar.Text>
247
+ </Avatar>
248
+ <Text size="xs" numberOfLines={1}>
249
+ {name.split(' ')[0]}
250
+ </Text>
251
+ </VStack>
252
+ ));
253
+ }
254
+
255
+ export default function ScrollShadowSizesDemo() {
256
+ return (
257
+ <Card fullBleed className="w-full max-w-sm">
258
+ <Card.Header>
259
+ <Heading size="sm">Send again</Heading>
260
+ </Card.Header>
261
+ <Card.Content>
262
+ {sizes.map((size, index) => (
263
+ <VStack key={size}>
264
+ {index > 0 ? <Divider /> : null}
265
+ <VStack space="sm" className="py-5">
266
+ <Text size="sm" color="secondary" className="px-5">
267
+ {size}
268
+ </Text>
269
+ <ScrollShadow surface="primary" size={size}>
270
+ <ScrollView
271
+ horizontal
272
+ showsHorizontalScrollIndicator={false}
273
+ contentContainerClassName="gap-6 px-5"
274
+ >
275
+ <Payees />
276
+ </ScrollView>
277
+ </ScrollShadow>
278
+ </VStack>
279
+ </VStack>
280
+ ))}
281
+ </Card.Content>
282
+ </Card>
283
+ );
284
+ }
285
+ ```
286
+
287
+ ## Visibility
288
+
289
+ `visibility` defaults to `auto`, which shows each edge only while content overflows past it. Each fade eases out over its own depth as you scroll toward that edge. `both`, `start`, and `end` force fades on, and `none` hides them. The child can be any single view. Switch modes below and scroll the list to compare.
290
+
291
+ ```tsx
292
+ import { useState } from 'react';
293
+ import {
294
+ Avatar,
295
+ Card,
296
+ Heading,
297
+ ListItem,
298
+ ScrollShadow,
299
+ ScrollView,
300
+ Text,
301
+ ToggleButton,
302
+ VStack,
303
+ type ScrollShadowProps,
304
+ } from '@cdx-ui/components';
305
+ import { TRANSACTIONS } from './_data';
306
+
307
+ type Visibility = NonNullable<ScrollShadowProps['visibility']>;
308
+
309
+ const MODES: Visibility[] = ['auto', 'both', 'start', 'end', 'none'];
310
+
311
+ export default function ScrollShadowVisibilityDemo() {
312
+ const [visibility, setVisibility] = useState<Visibility>('both');
313
+
314
+ return (
315
+ <VStack space="md" className="w-full max-w-sm">
316
+ <ToggleButton.Group
317
+ type="single"
318
+ value={visibility}
319
+ onValueChange={(value) => setVisibility(value as Visibility)}
320
+ aria-label="visibility"
321
+ >
322
+ {MODES.map((mode) => (
323
+ <ToggleButton key={mode} value={mode} size="small">
324
+ <ToggleButton.Label>{mode}</ToggleButton.Label>
325
+ </ToggleButton>
326
+ ))}
327
+ </ToggleButton.Group>
328
+ <Card fullBleed>
329
+ <Card.Header>
330
+ <Heading size="sm">Statement · September</Heading>
331
+ </Card.Header>
332
+ <Card.Content>
333
+ <ScrollShadow className="h-64" surface="primary" visibility={visibility}>
334
+ <ScrollView>
335
+ {TRANSACTIONS.map((transaction, index) => (
336
+ <ListItem
337
+ key={transaction.id}
338
+ size="compact"
339
+ showSeparator={index < TRANSACTIONS.length - 1}
340
+ >
341
+ <ListItem.LeadingSlot>
342
+ <Avatar color={transaction.isCredit ? 'success' : 'neutral'}>
343
+ <Avatar.Icon as={transaction.icon} />
344
+ </Avatar>
345
+ </ListItem.LeadingSlot>
346
+ <ListItem.Content>
347
+ <ListItem.Title>{transaction.merchant}</ListItem.Title>
348
+ </ListItem.Content>
349
+ <ListItem.TrailingSlot>
350
+ <Text color={transaction.isCredit ? 'success' : 'primary'}>
351
+ {transaction.amount}
352
+ </Text>
353
+ </ListItem.TrailingSlot>
354
+ </ListItem>
355
+ ))}
356
+ </ScrollView>
357
+ </ScrollShadow>
358
+ </Card.Content>
359
+ </Card>
360
+ </VStack>
361
+ );
362
+ }
363
+ ```
364
+
365
+ ## Defaults and styling
366
+
367
+ Start with `surface`, `size`, and `visibility`. If those don't fit, use one of these, from the smallest change to the largest:
368
+
369
+ - **Different depth or opacity:** pass `edgeClassName`. Its classes go on each fade overlay and are applied after `size`, so they win.
370
+
371
+ - Depth: `w-24` makes a horizontal fade 96px deep instead of the `size` value. Use `h-24` for a vertical scroller. Tailwind spacing steps are 4px each, so `w-14` is 56px and `w-24` is 96px.
372
+ - Opacity: `opacity-85` draws the fade at 85% strength, so a little of the content shows through even at the very edge. Lower values give a softer fade.
373
+
374
+ Don't put colors here; a `bg-*` class paints a solid block instead of a fade.
375
+
376
+ - **Different color:** pass `colorClassName` with an `accent-*` class when the panel isn't one of the `surface` colors, such as a tinted card. It replaces `surface` and still follows dark mode and `ScopedTheme`.
377
+
378
+ - **Your own indicator:** skip the fade overlays and use `useScrollShadow`. See Hook below.
379
+
380
+ `hideScrollBar` hides the scrollbar on web only. On iOS and Android, hide it on the child with `showsVerticalScrollIndicator` or `showsHorizontalScrollIndicator`.
381
+
382
+ ```tsx
383
+ import { useState } from 'react';
384
+ import {
385
+ Box,
386
+ Card,
387
+ Heading,
388
+ ScrollShadow,
389
+ ScrollView,
390
+ Text,
391
+ ToggleButton,
392
+ VStack,
393
+ } from '@cdx-ui/components';
394
+ import { ACTIVITY_FILTERS } from './_data';
395
+
396
+ const OFFERS = [
397
+ { value: 'groceries', label: '5% back · Groceries' },
398
+ { value: 'gas', label: '3% back · Gas' },
399
+ { value: 'streaming', label: '10% back · StreamFlix' },
400
+ { value: 'dining', label: '2% back · Dining' },
401
+ { value: 'travel', label: '4% back · Travel' },
402
+ ];
403
+
404
+ function CategoryPicker() {
405
+ const [category, setCategory] = useState('purchases');
406
+
407
+ return (
408
+ <ToggleButton.Group
409
+ type="single"
410
+ isAttached={false}
411
+ value={category}
412
+ onValueChange={setCategory}
413
+ aria-label="Spending category"
414
+ >
415
+ {ACTIVITY_FILTERS.map((filter) => (
416
+ <ToggleButton key={filter.value} value={filter.value} size="small">
417
+ <ToggleButton.Icon as={filter.icon} position="leading" />
418
+ <ToggleButton.Label>{filter.label}</ToggleButton.Label>
419
+ </ToggleButton>
420
+ ))}
421
+ </ToggleButton.Group>
422
+ );
423
+ }
424
+
425
+ export default function ScrollShadowStylingDemo() {
426
+ const [activated, setActivated] = useState<string[]>([]);
427
+
428
+ return (
429
+ <VStack space="lg" className="w-full max-w-sm">
430
+ <VStack space="xs">
431
+ <Text size="sm" color="secondary">
432
+ edgeClassName=&quot;w-24&quot;
433
+ </Text>
434
+ <Card fullBleed>
435
+ <Card.Content className="py-5">
436
+ <ScrollShadow surface="primary" edgeClassName="w-24">
437
+ <ScrollView
438
+ horizontal
439
+ showsHorizontalScrollIndicator={false}
440
+ contentContainerClassName="px-5"
441
+ >
442
+ <CategoryPicker />
443
+ </ScrollView>
444
+ </ScrollShadow>
445
+ </Card.Content>
446
+ </Card>
447
+ </VStack>
448
+
449
+ <VStack space="xs">
450
+ <Text size="sm" color="secondary">
451
+ edgeClassName=&quot;opacity-85&quot;
452
+ </Text>
453
+ <Card fullBleed>
454
+ <Card.Content className="py-5">
455
+ <ScrollShadow surface="primary" size="small" edgeClassName="opacity-85">
456
+ <ScrollView
457
+ horizontal
458
+ showsHorizontalScrollIndicator={false}
459
+ contentContainerClassName="px-5"
460
+ >
461
+ <CategoryPicker />
462
+ </ScrollView>
463
+ </ScrollShadow>
464
+ </Card.Content>
465
+ </Card>
466
+ </VStack>
467
+
468
+ <VStack space="xs">
469
+ <Text size="sm" color="secondary">
470
+ colorClassName=&quot;accent-surface-action-subtle&quot; hideScrollBar
471
+ </Text>
472
+ <Card fullBleed className="bg-surface-action-subtle">
473
+ <Card.Content fullBleed={false}>
474
+ <VStack space="xs">
475
+ <Heading size="xs">Cash back offers</Heading>
476
+ <Text size="sm" color="secondary">
477
+ Activate offers, then pay with your Rewards Visa · 1024.
478
+ </Text>
479
+ </VStack>
480
+ </Card.Content>
481
+ <Box className="pb-5">
482
+ <ScrollShadow colorClassName="accent-surface-action-subtle" hideScrollBar>
483
+ <ScrollView
484
+ horizontal
485
+ showsHorizontalScrollIndicator={false}
486
+ contentContainerClassName="px-5"
487
+ >
488
+ <ToggleButton.Group
489
+ type="multiple"
490
+ isAttached={false}
491
+ value={activated}
492
+ onValueChange={setActivated}
493
+ aria-label="Cash back offers"
494
+ >
495
+ {OFFERS.map((offer) => (
496
+ <ToggleButton
497
+ key={offer.value}
498
+ value={offer.value}
499
+ variant="outline"
500
+ size="small"
501
+ >
502
+ <ToggleButton.Label>{offer.label}</ToggleButton.Label>
503
+ </ToggleButton>
504
+ ))}
505
+ </ToggleButton.Group>
506
+ </ScrollView>
507
+ </ScrollShadow>
508
+ </Box>
509
+ </Card>
510
+ </VStack>
511
+ </VStack>
512
+ );
513
+ }
514
+ ```
515
+
516
+ ## Hook
517
+
518
+ ### useScrollShadow
519
+
520
+ Use this when you want the overflow state without a fade, for example a "More below" pill or paging chevrons. It does not wrap the scroller and does not paint overlays. Pass the returned handlers onto your `ScrollView` (or `FlatList` / `VirtualizedList`) and read `capability.canScrollBefore` / `canScrollAfter`.
521
+
522
+ The built-in `ScrollShadow` already uses this hook internally. Exporting it is for custom UI only. You can still wrap the same scroll view in `ScrollShadow` to keep the fades: it keeps your handlers and `ref`, as the paging chevrons below do. Options, return values, and types are on the API tab.
523
+
524
+ ```tsx
525
+ import { useRef } from 'react';
526
+ import type { LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
527
+ import {
528
+ Box,
529
+ Card,
530
+ Heading,
531
+ HStack,
532
+ IconButton,
533
+ ScrollShadow,
534
+ ScrollView,
535
+ Text,
536
+ VStack,
537
+ useScrollShadow,
538
+ } from '@cdx-ui/components';
539
+ import { ChevronLeft, ChevronRight } from '@cdx-ui/icons';
540
+ import { ACCOUNTS } from './_data';
541
+
542
+ export default function ScrollShadowHookDemo() {
543
+ const scrollRef = useRef<ScrollView>(null);
544
+ const offset = useRef(0);
545
+ const viewport = useRef(0);
546
+ const { capability, handleLayout, handleContentSizeChange, handleScroll } = useScrollShadow({
547
+ orientation: 'horizontal',
548
+ });
549
+
550
+ const onLayout = (event: LayoutChangeEvent) => {
551
+ viewport.current = event.nativeEvent.layout.width;
552
+ handleLayout(event);
553
+ };
554
+
555
+ const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
556
+ offset.current = event.nativeEvent.contentOffset.x;
557
+ handleScroll(event);
558
+ };
559
+
560
+ const page = (direction: -1 | 1) => {
561
+ const x = Math.max(0, offset.current + direction * viewport.current * 0.8);
562
+ scrollRef.current?.scrollTo({ x, animated: true });
563
+ };
564
+
565
+ return (
566
+ <Card fullBleed className="w-full max-w-sm">
567
+ <Card.Header>
568
+ <Heading size="sm">Your accounts</Heading>
569
+ <HStack space="xs">
570
+ <IconButton
571
+ as={ChevronLeft}
572
+ variant="subtle"
573
+ size="small"
574
+ isDisabled={!capability.canScrollBefore}
575
+ onPress={() => page(-1)}
576
+ accessibilityLabel="Previous accounts"
577
+ />
578
+ <IconButton
579
+ as={ChevronRight}
580
+ variant="subtle"
581
+ size="small"
582
+ isDisabled={!capability.canScrollAfter}
583
+ onPress={() => page(1)}
584
+ accessibilityLabel="Next accounts"
585
+ />
586
+ </HStack>
587
+ </Card.Header>
588
+ <Card.Content>
589
+ <Box className="py-5">
590
+ <ScrollShadow surface="primary">
591
+ <ScrollView
592
+ ref={scrollRef}
593
+ horizontal
594
+ contentContainerClassName="gap-3 px-5"
595
+ onContentSizeChange={handleContentSizeChange}
596
+ onLayout={onLayout}
597
+ onScroll={onScroll}
598
+ scrollEventThrottle={16}
599
+ showsHorizontalScrollIndicator={false}
600
+ >
601
+ {ACCOUNTS.map((account) => (
602
+ <Card key={account.mask} className="w-52">
603
+ <Card.Stripe />
604
+ <Card.Content>
605
+ <VStack space="xs">
606
+ <Text size="sm" color="secondary">
607
+ {account.name} · {account.mask}
608
+ </Text>
609
+ <Heading size="md">{account.balance}</Heading>
610
+ <Text size="xs" color="tertiary">
611
+ {account.label}
612
+ </Text>
613
+ </VStack>
614
+ </Card.Content>
615
+ </Card>
616
+ ))}
617
+ </ScrollView>
618
+ </ScrollShadow>
619
+ </Box>
620
+ </Card.Content>
621
+ </Card>
622
+ );
623
+ }
624
+ ```
625
+
626
+ ## API
627
+
628
+ ### ScrollShadow
629
+
630
+ | Prop | Type | Default | Description |
631
+ | --- | --- | --- | --- |
632
+ | `children` | `ReactElement<unknown, string \| React.JSXElementConstructor<any>>` | — | The scroll view to decorate: a `ScrollView`, `FlatList`, or `VirtualizedList`, passed as the direct child. With a forced `visibility`, any single view works. |
633
+ | `className?` | `string` | — | Classes for the wrapping view, for example `h-full` or `flex-1` to bound the scroller. |
634
+ | `style?` | `StyleProp<ViewStyle>` | — | Style for the wrapping view. |
635
+ | `orientation?` | `'horizontal' \| 'vertical'` | `inferred from the child's `horizontal` prop, otherwise `'vertical'`` | Scroll axis that gets fades. |
636
+ | `surface?` | `'background' \| 'primary' \| 'secondary'` | `'background'` | Theme surface the fade blends into. Match it to the background behind the scroller. |
637
+ | `size?` | `'small' \| 'default' \| 'large'` | `'default'` | Fade depth along the scroll axis: `small` is 32px, `default` is 40px, `large` is 72px. |
638
+ | `visibility?` | `'auto' \| 'both' \| 'start' \| 'end' \| 'none'` | `'auto'` | Which edges show a fade. `auto` follows the scroll position; `both`, `start`, and `end` force those edges on; `none` hides them. |
639
+ | `offset?` | `number` | `0` | Pixels of scroll slack before an edge counts as scrollable. Only used with `auto`. |
640
+ | `hideScrollBar?` | `boolean` | `false` | Hides the child's scrollbar on web. On iOS and Android, set `showsVerticalScrollIndicator` or `showsHorizontalScrollIndicator` on the child instead. |
641
+ | `edgeClassName?` | `string` | — | Width, opacity, or position overrides for the fade overlays, applied after `size`, for example `w-24` or `opacity-85`. Must not carry color classes; use `surface` or `colorClassName` instead. |
642
+ | `colorClassName?` | `string` | — | One-off fade color outside the `surface` options, as an `accent-*` class. Overrides `surface` on every platform and follows dark mode and `ScopedTheme`. |
643
+ | `onCapabilityChange?` | `((capability: ScrollShadowCapability) => void)` | — | Called when either edge starts or stops being scrollable, with `{ canScrollBefore, canScrollAfter }`. Only fires with `visibility="auto"`. |
644
+
645
+ Does not extend a React Native base component. Every prop is listed above. `ScrollShadow` keeps the child's own `onScroll`, `onLayout`, and `onContentSizeChange` handlers, including a Reanimated scroll handler, and sets `scrollEventThrottle={16}` when the child doesn't set one.
646
+
647
+ | Attribute | Values | Description |
648
+ | ----------- | ---------------- | ---------------------------- |
649
+ | `data-edge` | `start` \| `end` | Which edge an overlay covers |
650
+
651
+ ## Hook
652
+
653
+ ### useScrollShadow
654
+
655
+ Tracks whether a scroller can scroll toward its start or end, without rendering any overlay. Spread the handlers onto your scroll view and draw your own indicator from `capability`:
656
+
657
+ ```tsx
658
+ const { capability, handleLayout, handleContentSizeChange, handleScroll } = useScrollShadow({
659
+ orientation: 'vertical',
660
+ });
661
+ ```
662
+
663
+ | Option | Type | Description |
664
+ | -------------------- | ---------------------------------------------- | --------------------------------------------------- |
665
+ | `orientation` | `'horizontal' \| 'vertical'` | Scroll axis to track. Defaults to `vertical` |
666
+ | `offset` | `number` | Pixels of slack before an edge counts as scrollable |
667
+ | `enabled` | `boolean` | Stops tracking when `false` |
668
+ | `onCapabilityChange` | `(capability: ScrollShadowCapability) => void` | Called when either edge flips |
669
+
670
+ | Return | Type | Description |
671
+ | ------------------------- | ---------------------------------------------------------- | ------------------------------------ |
672
+ | `capability` | `{ canScrollBefore: boolean; canScrollAfter: boolean }` | Whether content overflows each edge |
673
+ | `handleLayout` | `(event: LayoutChangeEvent) => void` | Pass to the scroll view's `onLayout` |
674
+ | `handleContentSizeChange` | `(width: number, height: number) => void` | Pass to `onContentSizeChange` |
675
+ | `handleScroll` | `(event: NativeSyntheticEvent<NativeScrollEvent>) => void` | Pass to `onScroll` |
676
+
677
+ ## Anatomy
678
+
679
+ | `data-slot` | Element |
680
+ | -------------------- | -------------------------------- |
681
+ | `scroll-shadow` | Root wrapper around the scroller |
682
+ | `scroll-shadow-edge` | Start or end fade overlay |
683
+
684
+ ## Accessibility
685
+
686
+ The fade overlays are decorative. They pass touches through to the scroller and are hidden from screen readers with `aria-hidden`, `accessibilityElementsHidden`, and `importantForAccessibility="no-hide-descendants"`. The scrollable content stays fully reachable, so a fade never hides content from assistive technology.