All components mutedsmmdlg
Sidebar Nav Item
Sidebar navigation buttons with icon, label, and accent-tinted active state.
123456789101112131415161718192021222324import { SidebarNavItem, sidebarNavItemActive,} from "#components/ui/base-ui/sidebar-nav-item.tsx";
export function Example() { return ( // variant/size resolve once on Item; Button, Icon, and EndElement // inherit them via context <ul> <SidebarNavItem.Item variant="muted" size="md"> {/* render swaps the button for a link while keeping the styling */} <SidebarNavItem.Button aria-current="page" style={sidebarNavItemActive} > <SidebarNavItem.Icon viewBox="0 0 16 16">{/* icon */}</SidebarNavItem.Icon> Dashboard <SidebarNavItem.EndElement>⌘1</SidebarNavItem.EndElement> </SidebarNavItem.Button> </SidebarNavItem.Item> </ul> );}Variants
Sizes
Install
1npx stylexblocks add sidebar-nav-itemSource
components/ui/base-ui/sidebar-nav-item.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217import * as stylex from "@stylexjs/stylex";import { createSlotRecipeContext } from "#lib/base-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe } from "#lib/recipe-props.ts";import { sidebarNavItemButtonSvgVars } from '@stylexblocks/ui/slot-recipes/sidebar-nav-item.stylex.ts'
import { colorPalette } from '@stylexblocks/ui/color-palette.stylex.ts'import { cursor } from '@stylexblocks/ui/tokens/cursor.stylex.ts'import { durations } from '@stylexblocks/ui/tokens/durations.stylex.ts'import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { lineHeights } from '@stylexblocks/ui/tokens/line-heights.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { semanticRadii } from '@stylexblocks/ui/semantic-tokens/radii.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'
const sidebarNavItemItem = stylex.create({ base: { position: 'relative', fontSize: fontSizes.sm, paddingBlock: '1px', },})
const sidebarNavItemItemSizes = stylex.create({ sm: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, }, md: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, }, lg: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, },})
const sidebarNavItemButton = stylex.create({ base: { display: 'flex', alignItems: 'center', gap: spacing._2, isolation: 'isolate', width: '100%', textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap', cursor: { default: cursor.button, ':is(:disabled, [disabled], [data-disabled])': 'not-allowed', }, transitionProperty: 'background-color, border-color, color, fill, stroke, opacity, box-shadow, transform', transitionDuration: durations.fast, outlineWidth: { default: null, ':focus-visible': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus-visible': '0px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, focusRingWidth: '1px', opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, [sidebarNavItemButtonSvgVars.width]: sizes._4, [sidebarNavItemButtonSvgVars.height]: sizes._4, [sidebarNavItemButtonSvgVars.color]: 'var(--sidebar-item-icon-color)', },})
const sidebarNavItemButtonSizes = stylex.create({ sm: { borderRadius: semanticRadii.control, paddingInline: spacing._2, height: sizes.controlXs, }, md: { borderRadius: semanticRadii.control, paddingInline: spacing._2_5, height: sizes.controlSm, }, lg: { borderRadius: semanticRadii.control, paddingInline: spacing._3, height: sizes.controlMd, },})
const sidebarNavItemButtonVariants = stylex.create({ muted: { backgroundColor: { default: 'transparent', ':hover': `color-mix(in oklch, ${semanticColors.sidebarAccentBg} 90%, transparent)`, ':active': semanticColors.sidebarAccentBg, }, color: { default: `color-mix(in oklch, ${semanticColors.sidebarAccentFg} 85%, transparent)`, ':hover': semanticColors.sidebarAccentFg, ':active': semanticColors.sidebarAccentFg, }, '--sidebar-item-icon-color': { default: `color-mix(in oklch, ${semanticColors.sidebarAccentFg} 85%, transparent)`, ':hover': semanticColors.sidebarAccentFg, ':active': semanticColors.sidebarAccentFg, }, },})
const sidebarNavItemButtonSvg = stylex.create({ base: { width: sidebarNavItemButtonSvgVars.width, height: sidebarNavItemButtonSvgVars.height, color: sidebarNavItemButtonSvgVars.color, },})
const sidebarNavItemEndElement = stylex.create({ base: { display: 'flex', gap: '1px', marginInlineStart: 'auto', },})
const sidebarNavItemEndElementSizes = stylex.create({ sm: { paddingInlineEnd: spacing._1, }, md: { paddingInlineEnd: spacing._1, }, lg: { paddingInlineEnd: spacing._1, },})
export type SidebarNavItemVariant = keyof typeof sidebarNavItemButtonVariants
export type SidebarNavItemSize = keyof typeof sidebarNavItemItemSizes
// The button ships browser chrome the recipe doesn't reset, and the recipe// has no accent treatment for the active item — both live with the component.const sidebarNavItemStatic = stylex.create({ button: { borderWidth: 0, textAlign: "start", width: "100%" }, active: { backgroundColor: semanticColors.sidebarAccentBg, color: semanticColors.sidebarAccentFg, "--sidebar-item-icon-color": semanticColors.sidebarAccentFg, },});
// defineSlotRecipe validates the shape at compile time: default values must// exist in the axis maps, and slot keys must be `styles` or a declared axisconst sidebarNavItemSlotRecipe = defineSlotRecipe({ slots: { item: { styles: sidebarNavItemItem, sizes: sidebarNavItemItemSizes }, button: { styles: sidebarNavItemButton, sizes: sidebarNavItemButtonSizes, variants: sidebarNavItemButtonVariants, }, endElement: { styles: sidebarNavItemEndElement, sizes: sidebarNavItemEndElementSizes, }, }, defaultVariants: { variant: "muted", size: "md" },});
/** * SidebarNavItem — recipe and component colocated in one file. * `<SidebarNavItem.Item variant size>` resolves the recipe once and every * part inherits it through context; each part composes via `render` (swap * the Button for a link with `render={<a href>}`). Mark the active route * with `aria-current="page"` and the `active` style export: * `style={sidebarNavItemActive}`. */const { withProvider, withContext } = createSlotRecipeContext( sidebarNavItemSlotRecipe,);
export const sidebarNavItemActive = sidebarNavItemStatic.active;
export const SidebarNavItem = { Item: withProvider("li", "item", "SidebarNavItemItem"), Button: withContext( "button", (v) => [ sidebarNavItemButton.base, sidebarNavItemButtonSizes[v.size], sidebarNavItemButtonVariants[v.variant], sidebarNavItemStatic.button, ], "SidebarNavItemButton", ), // sized through the recipe's icon vars on the button slot Icon: withContext( "svg", () => [sidebarNavItemButtonSvg.base], "SidebarNavItemIcon", ), EndElement: withContext("span", "endElement", "SidebarNavItemEndElement"),};