Repository navigation
Conversation
Bring `Drawer.Item` and `Drawer.Section` in line with the current `md.comp.navigation-drawer` token set and extract component-specific tokens. The public API is unchanged. - Extract `Drawer/tokens.ts` with `DrawerItemTokens`, `DrawerSectionTokens` and `DrawerCollapsedItemTokens`. The last one cites `md.comp.navigation-rail`, which is what actually specifies `Drawer.CollapsedItem`. - Correct the item's trailing padding from 36dp to the spec'd 28dp, so both container insets match. - Emphasize the active destination's label, per `md.comp.navigation-drawer.active.label-text.weight`. - Add the MD3 focus indicator. It uses the *inner* offset, so the ring is drawn inside the active indicator: destinations sit flush, and an outer ring would overlap its neighbours. - Resolve the active indicator radius from the `full` shape token instead of hardcoding `extraLarge`. - Drop the hardcoded `Palette.neutralVariant50` section divider colour, which ignored the theme and was wrong in dark mode. Drawer divider tokens are deprecated in MD3, so the standalone divider spec (`outlineVariant`, 1dp) applies. - Use `marginStart`/`marginEnd` for directional spacing. React Native auto-swaps left/right in RTL, but react-native-web does not. - Collapse the leftover v2/v3 style splits and the duplicated typescale spread in `Drawer.Item` and `Drawer.Section`. Supporting theme changes, both additive: - `TypescaleKey` gains the 15 `*Emphasized` keys. The values already existed in `md.sys.typescale` but no type exposed them. - `md.sys.state.focusIndicator` gains `innerOffset`. `Drawer.CollapsedItem` renders identically; only its magic numbers moved into tokens. Refs: callstack#4981 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Add a `Drawer` screen to the example app, inside a 360dp surface standing in for the drawer container so the active indicator sits at its spec'd proportions. Covers active/inactive destinations, no icon, a truncated label, disabled, both trailing-slot shapes, sections separated by a divider, and collapsed destinations. - Give the example app's own drawer the MD3 container: 360dp wide, `surfaceContainerLow`, and rounded corners on the ending edge only. Collapse its leftover v3 style split while there. - Document the section divider and focus indicator colour roles. - Add a `Drawer` section to the 6.x migration guide. The API is unchanged, but four visual changes are worth calling out for anyone pixel-matching against 5.x. - Recapture `drawer-item` and `drawer-section` screenshots against the new rendering. Both are now device-agnostic crops; the old `drawer-section` shot was an Android emulator window, which cannot be reproduced from an iOS Simulator. `drawer-collapsed` is unchanged, since `Drawer.CollapsedItem` renders identically. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add `.agents/CONTEXT.md`, a glossary of the Material Design terms this project keeps getting wrong in review — the ref/sys/comp token tiers, state layer, focus indicator, active indicator, standard vs modal drawer. Add `.agents/adr/` with the three Drawer decisions a future reader would otherwise have to reverse-engineer: why Paper ships drawer destinations but no container, why `TypescaleKey` grew the emphasized keys, and why `Drawer.Item` alone among modernized components keeps `TouchableRipple` rather than rendering its own state layer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The hand-maintained `docs/src/data/themeColors.ts` still referenced v5 color roles and pre-v6 component behaviour, so the generated `ThemeColorsTable` documented roles that no longer exist on `ThemeColors`. Removed roles that are not on the type: - `surfaceDisabled` / `onSurfaceDisabled` - v6 draws disabled states from `onSurface` at a reduced opacity - `backdrop` (Modal) - now `scrim` at 32% - `elevarion.level3` - typo, and the surface FAB variant no longer exists Re-checked every remaining entry against what the component renders: - Appbar / Appbar.Header elevated: `surfaceContainer`, not `elevation.level2` - Button elevated: `surfaceContainerLow`; outlined border: `outlineVariant` - Dialog and Searchbar: `surfaceContainerHigh` - IconButton outlined border: `outlineVariant` - SegmentedButtons checked border: `outline`; disabled border: `outlineVariant` - FAB: replaced the old variant list with the MD3 `primary`/`secondary`/ `tertiary`/`tonal*` variants, and swapped the removed `AnimatedFAB` entry for `FAB.Extended` - Snackbar: document container and label colors alongside the icon color Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`disabled` blocked touch but had no visual effect, so a disabled destination was indistinguishable from an enabled one. MD3's navigation drawer and rail token sets define no disabled state, so fall back to the generic disabled-content opacity (`md.sys.state.opacity.disabled`) via a shared `stateOpacity` token. Applied at container level, as Checkbox and Menu.Item do, so the active indicator, icon, label and trailing slot dim together. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Git worktrees created under .claude/worktrees/ were crawled by Jest from the repo root, doubling the collected suites (110 instead of 55) and producing spurious failures from mismatched snapshots and duplicate module registration across checkouts. This also hit the lefthook pre-commit hook, which runs `test` from the repo root. Add <rootDir>/.claude/ to modulePathIgnorePatterns, which scopes both test discovery and module resolution. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This reverts commit ad5c200.
Revert changes that belong in their own PR, and finish wiring up the token modules introduced earlier in this branch. - Revert the theme colour table corrections for Appbar, Button, Dialog, FAB, IconButton, Menu, Modal, Searchbar, SegmentedButtons and Snackbar, along with the `.claude` jest ignore. The Drawer's own entries (`Drawer.Item` focus indicator, `Drawer.Section` divider) stay, since those document changes made here. - Drive the collapsed item's press animation from `theme.motion` rather than a hardcoded 150ms. `short3` is that same duration; the easing now follows the MD3 standard curve instead of Animated's default. - Resolve the collapsed item's active indicator radius through `resolveCornerRadius`, as `Drawer.Item` already does. - Colour the `Drawer.Item` label from the `*LabelTextColor` tokens, which were declared but never read. - Hide the focus indicator on disabled destinations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The screenshots taken in b746c65 showed different views than the ones they replaced, so they no longer read as a before/after of the same UI. Recapture both against the views the originals used: - `drawer-item`: a tight crop of a single active "Sent mail" destination, with the same relative margins as the 5.x crop. - `drawer-section`: the example app's own drawer, open on the "Example items" section with Inbox active. Both are Android emulator captures. They lack the macOS window chrome the 5.x shots had, since that needs an interactive window capture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| const contentColor = active | ||
| ? theme.colors.onSecondaryContainer | ||
| : theme.colors.onSurfaceVariant; | ||
| ? theme.colors[DrawerItemTokens.activeIconColor] | ||
| : theme.colors[DrawerItemTokens.inactiveIconColor]; | ||
| const labelColor = active | ||
| ? theme.colors[DrawerItemTokens.activeLabelTextColor] | ||
| : theme.colors[DrawerItemTokens.inactiveLabelTextColor]; |
There was a problem hiding this comment.
[nit] could we also apply focused icon & label colors here? M3 spec uses onSurface for inactive focused items instead of onSurfaceVariant . it also specifies focus state layer (currently only focus ring rendered)
| selectable={false} | ||
| numberOfLines={1} | ||
| <View style={styles.inner}> | ||
| <View style={styles.wrapper} testID="drawer-item-content"> |
There was a problem hiding this comment.
Don't add default test ids - such a default will leave multiple test ids and may conflict with the user's own test ids" (#4998). All three exist only so the new tests can query them.
|
|
||
| {isFocused ? ( | ||
| <View | ||
| testID="drawer-item-focus-ring" |
| style={[styles.divider, styles.v3Divider]} | ||
| style={styles.divider} | ||
| theme={theme} | ||
| testID="drawer-section-divider" |
There was a problem hiding this comment.
Same here with the default test id
| const handlePressOut = () => { | ||
| Animated.timing(animScale, { | ||
| toValue: 1, | ||
| duration: 150 * scale, | ||
| duration: | ||
| theme.motion.duration[ | ||
| DrawerCollapsedItemTokens.activeIndicatorDuration | ||
| ] * scale, | ||
| easing: Easing.bezier( | ||
| ...theme.motion.easing[DrawerCollapsedItemTokens.activeIndicatorEasing] | ||
| ), | ||
| useNativeDriver: true, | ||
| }).start(); | ||
| }; |
There was a problem hiding this comment.
Use Reanimated here. #5015 already rewrites this exact block as transitionProperty / transitionDuration driven by plain state — adding Easing to Animated.timing will only make that harder to land.
| role="button" | ||
| aria-selected={active} |
There was a problem hiding this comment.
aria-selected isn't allowed on role="button", so the active destination isn't exposed as current to a screen reader. BottomNavigationBar uses role={Platform.OS === 'ios' ? 'button' : 'tab'} for exactly this - worth matching, here and in DrawerCollapsedItem.
| const opacity = disabled | ||
| ? DrawerItemTokens.stateOpacity.disabled | ||
| : DrawerItemTokens.stateOpacity.enabled; |
There was a problem hiding this comment.
opacity on the TouchableRipple dims the active indicator to 38% along with the content. Menu.Item puts it on the content row instead - should this move to styles.inner? Same question for DrawerCollapsedItem's Pressable.
|
Review by Claude Web
Steps: open the Drawer example, press Tab until focus reaches the first item under "Collapsed destinations". What happens: the browser draws its default square blue outline around the 80dp container. What should happen: a focus ring in |
satya164
left a comment
There was a problem hiding this comment.
Review by Claude
See the inline comments. (via Claude, verify if it makes sense)
|
|
||
| The `Drawer` API is unchanged in Paper 6.x — no props were renamed, removed, or given new defaults. `Drawer.Item` and `Drawer.Section` were realigned with the current Material Design 3 navigation drawer spec, so their appearance changes: | ||
|
|
||
| - **Trailing padding.** `Drawer.Item` reserved 36dp on the trailing edge and 28dp on the leading edge. Both are now 28dp, as the spec requires. Items sit 8dp wider. |
There was a problem hiding this comment.
"Items sit 8dp wider" is not what changed. The active indicator keeps its size: 12dp from each drawer edge on main and in this PR (336dp wide in a 360dp drawer). Only the content area grows by 8dp on the trailing side, so long labels truncate later and the right content moves 8dp toward the edge.
| - **Trailing padding.** `Drawer.Item` reserved 36dp on the trailing edge and 28dp on the leading edge. Both are now 28dp, as the spec requires. Items sit 8dp wider. | |
| - **Trailing padding.** `Drawer.Item` reserved 36dp between the drawer edge and its content on the trailing side, and 28dp on the leading side. Both are now 28dp, as the spec requires. The active indicator keeps its size. The label and the `right` content get 8dp more room. |
(via Claude, verify if it makes sense)
| If you were pixel-matching a drawer against Paper 5.x, the first two points are the ones that will shift your layout. To restore the old trailing padding, pass it through `style`: | ||
|
|
||
| ```tsx | ||
| <Drawer.Item label="Inbox" icon="inbox" style={{ marginEnd: 20 }} /> | ||
| ``` |
There was a problem hiding this comment.
This snippet does not restore the 5.x layout. style goes to the active indicator container, so marginEnd: 20 moves the indicator edge, not the content. With this snippet on web, the indicator ends 20dp from the drawer edge and is 328dp wide instead of 336dp. The content does end 36dp from the edge, but an active item now has a visibly shorter indicator than in 5.x. There is no prop that changes only the content inset, so I suggest dropping the workaround:
| If you were pixel-matching a drawer against Paper 5.x, the first two points are the ones that will shift your layout. To restore the old trailing padding, pass it through `style`: | |
| ```tsx | |
| <Drawer.Item label="Inbox" icon="inbox" style={{ marginEnd: 20 }} /> | |
| ``` | |
| If you were pixel-matching a drawer against Paper 5.x, the first two points are the ones that will shift your layout. |
(via Claude, verify if it makes sense)
| - **Trailing padding.** `Drawer.Item` reserved 36dp on the trailing edge and 28dp on the leading edge. Both are now 28dp, as the spec requires. Items sit 8dp wider. | ||
| - **Active label weight.** The label of an active `Drawer.Item` now uses the prominent `labelLargeEmphasized` weight (700). Inactive labels are unchanged at 500. | ||
| - **Section divider colour.** `Drawer.Section` drew its divider in a fixed grey that ignored the theme and was wrong in dark mode. It now uses `theme.colors.outlineVariant`, like every other `Divider`. | ||
| - **Focus indicator.** `Drawer.Item` gains a keyboard focus ring in `theme.colors.secondary`. It appears only on keyboard focus, not on press. |
There was a problem hiding this comment.
The list leaves out the disabled change. A disabled Drawer.Item or Drawer.CollapsedItem is now drawn at 38% opacity, while in 5.x it looked the same as an enabled item. Anyone who passes disabled sees a different drawer after the upgrade.
| - **Focus indicator.** `Drawer.Item` gains a keyboard focus ring in `theme.colors.secondary`. It appears only on keyboard focus, not on press. | |
| - **Focus indicator.** `Drawer.Item` gains a keyboard focus ring in `theme.colors.secondary`. It appears only on keyboard focus, not on press. | |
| - **Disabled state.** Disabled `Drawer.Item` and `Drawer.CollapsedItem` destinations are now dimmed to 38% opacity. In 5.x they looked the same as enabled ones. |
(via Claude, verify if it makes sense)
| <Drawer.Item label="Inbox" icon="inbox" style={{ marginEnd: 20 }} /> | ||
| ``` | ||
|
|
||
| The theme also gains the 15 `*Emphasized` typescale keys (`labelLargeEmphasized`, `titleSmallEmphasized`, and so on). The values already existed in the token set; only the types were missing, so `<Text variant="labelLargeEmphasized">` now typechecks. This is additive — no existing key changed. |
There was a problem hiding this comment.
Two problems with this paragraph:
- It is not fully additive.
Theme['fonts']isTypescale, which isRecord<TypescaleKey, TypescaleStyle>. A theme that setsfontsto a hand-written object with the 15 base variants anddefaultnow fails to typecheck, because the 15 emphasized keys are required. Say that here, so people with a hand-writtenfontsobject know to spreadMD3LightTheme.fontsor useconfigureFonts. - These keys are now public
Textvariants, but the Fonts guide (docs/6.x/docs/guides/fonts.md) still lists only the base variants. Add the emphasized variants there, so people can find them. (via Claude, verify if it makes sense)
| onPress={onPress} | ||
| onPressOut={onPress ? handlePressOut : undefined} | ||
| disabled={disabled} | ||
| style={{ opacity }} |
There was a problem hiding this comment.
Drawer.CollapsedItem has no focus indicator. On web, keyboard focus shows the browser's default square blue outline around the 80dp container, while Drawer.Item now draws the MD3 ring. Track keyboard focus with isKeyboardFocusEvent as Drawer.Item does, draw the ring in theme.colors.secondary around the active indicator, and add webNoOutline to this Pressable.
| <Drawer.CollapsedItem | ||
| focusedIcon="bell" | ||
| unfocusedIcon="bell-outline" | ||
| badge | ||
| active={collapsed === 'alerts'} | ||
| onPress={() => setCollapsed('alerts')} | ||
| /> |
There was a problem hiding this comment.
This item has no label and no aria-label, so it is a button with no accessible name. The iOS accessibility tree shows an unnamed button, and on web the button has no aria-label and only the icon glyph as text. A screen reader user cannot tell what it does. Since the example is what people copy, give it a name:
| <Drawer.CollapsedItem | |
| focusedIcon="bell" | |
| unfocusedIcon="bell-outline" | |
| badge | |
| active={collapsed === 'alerts'} | |
| onPress={() => setCollapsed('alerts')} | |
| /> | |
| <Drawer.CollapsedItem | |
| focusedIcon="bell" | |
| unfocusedIcon="bell-outline" | |
| badge | |
| aria-label="Alerts" | |
| active={collapsed === 'alerts'} | |
| onPress={() => setCollapsed('alerts')} | |
| /> |
(via Claude, verify if it makes sense)
| it('insets destination content 28dp from both drawer edges', async () => { | ||
| await render(<DrawerItem label="Example item" />); | ||
|
|
||
| expect(screen.getByRole('button')).toHaveStyle({ marginHorizontal: 12 }); | ||
| expect(screen.getByTestId('drawer-item-content')).toHaveStyle({ | ||
| marginHorizontal: 16, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
This test checks how the 28dp inset is split between two internal views (12 on the pressable, 16 on an inner wrapper), not what the user sees. Moving the inset to one view breaks the test with no visible change. The same applies to aligns the section title with the destination icons in DrawerSection.test.tsx, which checks marginStart: 28 on the title. The project test rules ask not to assert exact internal style values. The snapshots already cover these values, so I suggest removing both tests. This also removes the last use of the drawer-item-content test ID. (via Claude, verify if it makes sense)


Motivation
Spec alignment
Drawer.Itemreserved 36dp trailing / 28dp leading. Both are now 28dp per spec, so items sit 8dp wider.Drawer.Itemlabels now uselabelLargeEmphasized(700). Inactive labels stay at 500.Drawer.Sectiondrew its divider in a fixedPalette.neutralVariant50grey that ignored the theme and was wrong in dark mode. It now usestheme.colors.outlineVariant, like every otherDivider.Drawer.CollapsedItemcomputed its radius asitemSize / 2; it now resolves thefullshape token through the theme.150mslinear ramp. It now uses theshort3duration andstandardeasing tokens.state.opacity.disabledtoken.Drawer.Itemgains a focus ring intheme.colors.secondary.Token extraction
New
src/components/Drawer/tokens.tsexportsDrawerItemTokens,DrawerSectionTokensandDrawerCollapsedItemTokens.Supporting theme changes
TypescaleKeygains the 15*Emphasizedkeys. The values already existed in the token set — only the types were missing.state.focusIndicatorgainsinnerOffset, needed to inset the focus ring.A note on drawer width. The example app deliberately sets no drawer width.
react-native-drawer-layout's default —min(360dp, containerWidth − 56dp)— is MD3's 360dp container plus MD2's 56dp edge gap; a flat 360dp leaves almost no scrim to tap and dismiss on narrow phones (also noticed by flutter/flutter#123380).Related issue
Closes #4981
Test plan
yarn test(Drawer unit tests + snapshots updated in this branch)Drawer.Item,Drawer.CollapsedItemandDrawer.Sectionin the example app (expanded, collapsed/rail, active/inactive/disabled states) in both light and dark themes