Status: alpha. v11 (React 19, TypeScript, SLDS 2) is published under the
alphanpm tag; the latest release is11.0.0-alpha.4. The stable 0.10.x line stays on thelatesttag and the0.10.xbranch. See CHANGELOG.md for what each alpha changed.
| Area | State | Notes |
|---|---|---|
| Component sources | TypeScript | components/utilities/dialog/index.jsx is the last .jsx source; the rest of the .jsx files are sample data |
| Positioning | @floating-ui/dom |
popper.js is gone; computeFloatingData in utilities/dialog still reshapes Floating UI output into the old popper shape |
| TypeScript | tsc --noEmit clean, strict: true |
.d.ts files are generated into dist/types |
| Storybook | 10, CSF stories | 2 component folders have no story: grid, menu-list |
| Tests | 48 Vitest files | jsdom unit project plus a Playwright browser project; story HTML snapshots cover non-portal stories in a top-level __docs__/ folder |
| Lint | 0 errors / 223 warnings | Errors block CI; warnings are the tracked migration backlog |
| Toolchain | Vite 8, Vitest 4, ESLint 10, Storybook 10 | Node.js 24 (.nvmrc) |
| SLDS 2 CSS | npm dependency | @salesforce-ux/design-system-2 installs with the package; consumers add the stylesheet themselves (see the README) |
| Publishing | GitHub Release → npm, alpha tag |
.github/workflows/npm-publish.yml; alpha.3 and alpha.4 published from it |
- Fix the
components/<name>entry points forColorPicker,Filter, andTree.scripts/generate-entry-shims.mjsonly emits a shim beside a builtindexfile, and these three have none indist/, so the subpath import fails to resolve. - Loosen the
@salesforce-ux/design-systempeer dependency. It was pinned to2.25.0-alpha.2, which conflicted with any newer version already in the consumer's tree. It is now^2.25.0-alpha.2. - Write a 0.10.x → v11 migration guide (removed components,
DataTablerename, React 19, module format). - Publish hosted docs for v11.
react.lightningdesignsystem.comredirects to GitHub, and there is no public v11 documentation site (Chromatic builds Storybook on push, but only for review). - Use one tag format for releases.
11.0.0-alpha.4has novprefix, unlike the earlierv11.0.0-alpha.*tags. - At GA: change
publishConfig.tagfromalphatolatestand update the README's version table.
Props are being reviewed against docs/codebase-overview.md
in groups of five components, in directory order, so that no breaking prop change is
needed after 11.0. Changes below are breaking if they ship after GA; land them
(with utilities/warning/* shims for the old names where practical) before the tag
moves to latest.
Review complete (all components except utilities). Breaking changes by theme:
- Removals of deprecated surface.
forms/input/inline(InlineEdit), theforms/checkboxalias,tabs/pane.ts;modaldeprecatedtitle/closeButtonAssistiveTextandModalTrigger;assistiveText: string | objectbecomes object-only (avatar, breadcrumb, spinner, textarea, tree);tooltipdead/legacy props (openByDefault,isInline,target,info/errorvariants). - Uncontrolled data and selection. Rule of thumb: if it would reasonably live in
Redux/app state, it is controlled. Remove
default*twins and internal copies from checkbox, radio, input, combobox, slider, textarea and tabs. Selection is controlled in menu-dropdown, radio-group, simple-data-table, split-view, tree, vertical-navigation, color-picker, date-picker and button-stateful (tree also stops mutating consumer nodes). Ephemeral and allowed to stay uncontrolled (mode byvalue !== undefined, plusdefaultIsOpen):isOpenon popover, tooltip, expandable-section, date-picker, setup-assistant and split-view; carouselcurrentPanel/autoplay; tree expansion. - Child and slot contracts. Allowed when every accepted child is an existing library
component with typed props (accordion, setup-assistant, radio-group, simple-data-table,
tabs, visual-picker, welcome-mat, panel, button-group, builder-header, files, icon/badge
slots in alert, toast, scoped-notification, vertical-navigation, input); just type the
children, keep injected props out of the public type, and match by reference rather than
displayName. Still breaking: slots that take arbitrary consumer elements (menu-dropdown, popover and tooltip triggers). Where a parent merges a child's props with its own, consumer props win (comboboxpopover/input/tooltipMenuItemDisabled;onChangeis wrapped so both handlers run). - Callback shapes. Move to
(event, data)and report the requested state: toast, tabs (also dropreturn falseveto), tree, split-view, menu-dropdown, date-picker, popover, welcome-mat, carousel, setup-assistant; wrap pill-container's callbacks in aneventsobject. - Booleans.
is*/has*renames (slidervertical, pillbare, visual-pickercoverable/vertical, 11 in simple-data-table, comboboxmultiple/predefinedOptionsOnly; split-viewmultiplepending the native-attribute rule); booleans defaulting to true (spinnerhasContainer, welcome-matisOpen); sharedisDismissiblefor toast and alert. classNameand root. Done for slider, tree, welcome-mat, popover, spinner, modal and vertical-navigation:className,styleand the forwardedrefland on the outermost rendered element; other parts useclassName*/style*, and the old names warn once.- Labels,
assistiveTextand naming. Inverted split-view toggle keys, split labels (tree heading, trial-bar time),errorTextbecomeslabels.error+hasError(slider, textarea), duplicatelabel/assistiveText.label,onRender*props renamed to slot or render names. Done (old names still work and warn throughcheck-props.js): split-viewassistiveText.closeToggle/openToggle; treelabels.heading/assistiveText.heading; trial-barlabels.timeLeftAfterandactions; slider and textarealabels.label,labels.error+hasError; slots for zero-argumentonRender*props: page-headeractions/controls, builder-header toolbaractions, welcome-matinfoActions, info-badgecompleteActions, setup-assistant stepaction/content/figure, progress-indicatorsteps[].setupAssistantAction, checkbox/radiovisualPickerFigure*; render props that take data arerender<Part>(data)functions instead ofonRender*props or component types: carouselrenderItem, comboboxrenderMenuItem(replacesonRenderMenuItemandmenuItem), simple-data-tablerenderSubHeadingRow, menu-dropdownrenderMenuItem(replaceslistItemRenderer), split-view listboxrenderListItem(replaceslistItem). Still open: the tree heading XOR as a union type (theme 8), anderrorTexton input, checkbox and combobox. - Typing the contract. Discriminated unions (tree nodes, welcome-mat variants, combobox,
menu-dropdown and color-picker variants/options), no index signatures
(vertical-navigation, split-view), textarea
maxLengthas a number. - Fix now (bugs, not breaking). Done: simple-data-table
getAssistiveTextargument order, grid"undefined slds-col"(#3239), split-viewbottomRightTexttitle, vertical-navigationaria-current, tabsdisabledtype, toast timer re-arm (#3251).
Reviewed: accordion, alert, avatar, badge, breadcrumb, builder-header, button, button-group, button-stateful, card, carousel, checkbox, color-picker, combobox, date-picker, docked-composer, dynamic-icon, expandable-section, files, filter, forms, grid, icon, icon-settings, illustration, input, media-object, menu-dropdown, menu-list, modal, page-header, panel, pill, pill-container, popover, portal-settings, progress-bar, progress-indicator, progress-ring, radio, radio-button-group, radio-group, scoped-notification, setup-assistant, simple-data-table, slider, spinner, split-view, tabs, textarea, toast, tooltip, tree, trial-bar, vertical-navigation, visual-picker, welcome-mat.
-
alert— renamedismissible→isDismissible. -
avatar— renameinverse→isInverse. -
alert— decidevariant: required, or optional with a documented default (it is required in the type but defaults to'info'). -
alert— stopcloneElement-injectingcontainerClassName/inverse/sizeinto theiconprop, or document it as the contract. Re-scoped by the child-contract rule: children here are library components, so thecloneElement/child-props contract is allowed. Keep the other findings. Still required: type the accepted children (ReactElement<XProps>or exported subcomponents), keep injected props out of the public child type, and match by component reference instead ofdisplayName. -
alert— decide whether focusing the close button on mount (and restoring focus on unmount) stays the default or becomes opt-in. -
accordion— replace thecloneElement+Children.countchild contract (implicitrefs/onClickSummary/onKeyDownSummaryinjection) with context or anitemsprop. Re-scoped by the child-contract rule: children here are library components, so thecloneElement/child-props contract is allowed. Keep the other findings. Still required: type the accepted children (ReactElement<XProps>or exported subcomponents), keep injected props out of the public child type, and match by component reference instead ofdisplayName. -
accordion— decide whether the first summary button should be focused on mount (currently it is, fromcurrButtonIndexstarting at0). -
breadcrumb— decidetrail: ReactNode[](positional, index keys) vsitemswith ids; replacestyleContainerwithclassNameon the rootnav. -
avatar,breadcrumb— drop theassistiveText: string | objectunion (object only). -
badge— decidecontentprop vschildren;stylecurrently lands on the icon span, not the root. -
avatar— move thetitledefault ('user avatar') out of the prop default and intoassistiveText/labels. -
button-stateful— remove the uncontrolled path (it is data):active→isActive(required, controlled), andonClickcarries the next value. Today it falls back to internaluseStatewhenactiveis not a boolean. -
button-stateful— replacestateOne/stateTwo/stateThree(positional names) with named states; move the English defaults (Follow/Following/Unfollow) intolabelsand decide whether they stay as defaults. -
button-stateful— stop callingcurrentTarget.blur()on mouse leave and blur (it drops keyboard focus);onMouseDownis accepted but ignored outside the icon variants. -
button— rename the non-native booleansinverse,hint,responsiveandrequestFocustois*/has*.disabledkeeps its name: attributes that pass through to the DOM keep their HTML names (see Naming Conventions).requestFocus/onRequestFocusalso misuse theonRequest*prefix. -
button— pick one ref API (refvsbuttonRef); modelvariant: 'icon'+iconVariant/iconSizeas a discriminated union; give the deprecatedtooltipprop a sunset path (utilities/warning/sunset-property) or remove it. -
button,button-stateful— extendButtonHTMLAttributessoname,value,role,autoFocus, etc. pass through (onlyaria-*/data-*/form*do today). SettletabIndextype (buttontakesnumber | string,button-statefulonlystring). -
button-group— replace the implicitcloneElementcontract (triggerClassNameon the last child,variant: 'button-group'on checkbox children); makevariantan explicit union including the default; typelabelsasReactNode. Re-scoped by the child-contract rule: children here are library components, so thecloneElement/child-props contract is allowed. Keep the other findings. Still required: type the accepted children (ReactElement<XProps>or exported subcomponents), keep injected props out of the public child type, and match by component reference instead ofdisplayName. -
builder-header— keep theevents: { onClickBack, onClickHelp }object (decided). -
builder-header— replace children-by-displayNameslot detection (and the same in-nav,-toolbar) with named slot props;-toolbarsilently drops non-ButtonGroupchildren and-navdrops anything but link/dropdown. Re-scoped by the child-contract rule: children here are library components, so thecloneElement/child-props contract is allowed. Keep the other findings. Still required: type the accepted children (ReactElement<XProps>or exported subcomponents), keep injected props out of the public child type, and match by component reference instead ofdisplayName. -
builder-header— remove the hardcodedposition: relative; height: 100pxwrapper (soclassNamelands on the real root), and decide the English default labels. -
builder-header/nav-dropdown— stop extendingMenuDropdownPropswholesale somenu-dropdownchanges don't leak into this API. -
card— renamebodyClassName→classNameBody; makeempty: boolean | ReactNodetwo props or oneReactNode; decide the English defaults (heading = 'Related Items','No Related Items');headeriscloneElement-merged with a media-object contract.
Control model (decided). Data and values are always controlled — they are app state
(checked, value, selection, isActive, a date): required prop plus onChange, no
default* twin and no internal copy. Ephemeral UI state may be uncontrolled (isOpen,
current carousel panel, autoplay on/off, roving focus, image-load error): internal state,
optional controlled prop, and an optional default* prop. For ephemeral state the mode is
chosen by whether the value prop is !== undefined, never by whether a callback is
present, and a controlled prop is always honored (no internal flip, no seeding via effect).
Callbacks stay grouped in an events object where components already use one
(color-picker, combobox, builder-header); do not flatten them.
-
carousel—currentPanel/isAutoplayOnare ephemeral UI (re-audited with the "would it live in Redux?" test: a slide position and autoplay toggle would not): keep both uncontrolled-capable but key the mode off the value prop (today they only update internal state whenonRequestPanelChange/onRequestAutoplayToggleis absent, so a callback without a value freezes the carousel). AdddefaultCurrentPanel/defaultIsAutoplayOn, and stophasAutoplaydoubling as "start playing". -
carousel—onRequestAutoplayTogglereports the currentisAutoplayOn, not the requested one;eventmay be{}or a stale event;onRenderItemis typed(item)but called with{ item, ... };CarouselItemData.idisstringbut examples pass numbers;itemsPerPanelis unvalidated. -
color-picker—valueis data: make it fully controlled (required, no seeding intouseState, no uncontrolled typing whenonChangeis missing; a parent that rejects a change must be able to revert). The in-popover working color is a draft: keep it internal and treatvalueWorkingas controlled-if-provided, not seeded.isOpenstays uncontrolled-capable but must be honored when provided (today a controlledisOpen={false}still opens); adddefaultIsOpen. DeriveerrorTextinstead of seeding it once. -
color-picker— keep theeventsobject. Wire or remove the four entries that are typed but ignored (events.onClose/onOpen/onRequestClose/onRequestOpen; only the top-level props are wired) and makeeventsreferentially stable (events = {}is a new object every render). Rename the boolean-returningonValidateColor/onValidateWorkingColor(return values are not allowed; they are not events);onRequestClosecloses before the parent can veto andtriggeris an untypedstring;onRequestOpen()takes no(event, data)and fires on close too. -
color-picker—variant+defaultSelectedTab+swatchColorsas a discriminated union;hideInput→isInputHidden; decide whereidlands (no DOM node today). -
combobox— keep theeventsobject, but make it referentially stable (rebuilt every render today) and type each callback'sdata.selectionandvalueare data and stay controlled: dropdefaultValue.isOpenis ephemeral: uncontrolled-capable, mode keyed off theisOpenvalue (supplyingonRequestOpenwithoutisOpencurrently means it can never open), adddefaultIsOpen.onRequestClosefires and the component closes anyway;onClosefires after an internal change even whenisOpenis controlled;onSelectis really a request (selectionis controlled). -
combobox— enforcevariantas a discriminated union (popover/assistiveText. popoverLabelare runtime-warned only;optionsis "required unless popover" only at runtime);value+defaultValuefeed one input and flip controlled/uncontrolled (resolved by droppingdefaultValue). -
combobox—multiple→isMultiple,predefinedOptionsOnly→hasPredefinedOptionsOnly,singleInputDisabled(drop or rename). (selected→isSelectedon custom menu items: done,renderMenuItemreceivesisSelected.)disabledkeeps its name but currently does not reach the input at all (only suppresses the dialog). -
combobox— typepopover,inputandtooltipMenuItemDisabledas the library components (today all three are bareReactElement):ReactElement<Partial<PopoverProps>>,ReactElement<Partial<InputProps>>,ReactElement<Partial<TooltipProps>>; the child-contract exception covers them. Typinginputexposes a real mismatch: the element's props are spread last onto the privateInnerInput({...userDefinedInputProps}), whoseonChange(event)is native, whileInput's is(event, data). Maintainer decisions: consumer props override library props when present, so the spread order stays (id,value,onKeyDown,onFocus,onBlur,onClickstay overridable);onChangeis the one exception: combobox wraps the consumer'sonChangeso it is called with(event, data)and runs alongside combobox's ownhandleInputChange(InnerInputis private, not exported, and outside semver, so the adapter is non-breaking). Document thatpopover'sbodyis supplied andchildrendropped. -
combobox—ComboboxOption.label: string | fnis cast to string in many places andfilter.jsthrows on a function label;type: ... | stringcollapses the union;icon: ReactNodeis cloned and must be an<Icon>. -
combobox— callbackdataisRecord<string, unknown>and always{};onRequestOpenhas no(event, data);classNamelands on the inner.slds-comboboxand the root isclassNameContainer; decide thereftarget (forwarded but unused). -
date-picker—valueis data: fully controlled, typedDate | null(null = empty; todayvalue={undefined}flips to uncontrolled with stale text and a date cannot be cleared). RemoveformattedValueand the seededinternalValue. The typed-text buffer stays internal (it must hold partial input) but re-syncs fromvalue.isOpenis ephemeral: keep uncontrolled-capable, adddefaultIsOpen, and route keyboard open/ close through the same path as the mouse. -
date-picker—onChangefires per keystroke with a possibly InvalidDate; make the contractdate: Date | nullplusisValid, addsource.parsercannot signal "invalid" (decideDate | nullreturn now). -
date-picker—onRequestOpen/onRequestClose/onClosetake no(event, data);onRequestClosefires when already closed and sets internal state even whenisOpenis controlled; keyboard handling bypassesonRequest*entirely. -
date-picker—classNamelands on the dialog andtriggerClassNameon the trigger: move toclassName(root) +classNameTrigger+classNameMenu.forwardRefinindex.tsxdrops the ref; theinputprop's merge order lets a consumer'sonChange/inputRefoverride the picker's (breaking focus-on-close). TypeonCalendarFocus'sdirectionas'next' | 'previous'. Settlechildren,label,placeholder,assistiveTextOpenCalendar,formattedValue: remove at GA or keep permanently. -
date-picker— range selection: do not widendate/valuetoDate | [Date, Date]; plan a separateDateRangePicker(ormode="range"with distinct props). -
checkbox—checkedis data: controlled only. RemovedefaultChecked(theonChangedata computes!checkedfrom the prop, which is wrong without it). Keepindeterminatecontrolled and clear it from the DOM when it goes back toundefined. -
checkbox— remove deadoldEventParameterOrder(never read; theforms/checkboxalias, removed in this release, is the only caller) and the Enter/SpacehandleKeyDownthat fakes aChangeEvent; wire or removereadOnly(destructured, never applied). -
checkbox—variant(base | toggle | button-group | visual-picker) as a discriminated union (indeterminatewithtoggle, anderrorText/className/required/tabIndexonbutton-group/visual-picker, compile but are dropped);coverable/vertical→isCoverable/isVertical;requestFocus/onRequestFocus→ delete (useref);tabIndexstring→number;assistiveText: string | object→ object only; decide whereclassNamelands and whether unknown props spread to the<input>. -
expandable-section—isOpenis ephemeral, so uncontrolled is fine, but the mode is keyed offonToggleOpenbeing present: with a callback and noisOpenthe section is stuck open. Key it offisOpen !== undefined, always callonToggleOpen, and add adefaultIsOpen(note it must not be a boolean that defaults totrue; today the internal default is open).onToggleOpenreports the currentisOpen, not the requested one. -
expandable-section— renamenonCollapsible→isNonCollapsible; decidetitlenow (it is the visible heading, so it will collide with the HTMLtitleattribute if the component ever extendsHTMLAttributes; considerlabels.title/heading, typedReactNode);isOpen={false}withnonCollapsiblehides content forever (illegal combination: make it a union). -
docked-composer— decide whether the body stays mounted when closed (it is unmounted, which drops any uncontrolled state inside it);isOpenis controlled-only today, so plandefaultIsOpenif it should also work uncontrolled; keep theeventsobject but add adataargument toonRequestMinimize/onRequestExpand/onRequestClose(currently(event)only);role="dialog"on a non-modal bar needs a focus decision, andaria-describedbypoints at a body id that does not exist when closed. -
dynamic-icon—variant+scorePolarity/strengthLevel/trendDirectionas a discriminated union; thewafflevariant renders a<button>with notype, noonClickand no rest props (it submits forms and cannot be wired up), so decide its API now (union member withonClick, or a separate component);titleis required but doubles as the fallback assistive text;StrengthLevelaccepts strings and numbers. -
files—Files.cropis dead:Filesetscrop: '16-by-9'throughdefaultProps, soFilesalways reads a definedchildProps.cropand never applies its own (from reading; React resolves classdefaultPropsat element creation, confirm in a test). Decide whethercropdefaults live onFilesorFile. -
files—moreActionsDropdowniscloneElement-injected withMenuDropdownprops (overlay,buttonVariant,buttonInverse,className,triggerClassName) andFilesclonescropinto children: both are implicit contracts.File.labels.titleis documented as required but optional in the type;hrefdefaults to'#'. Re-scoped by the child-contract rule: children here are library components, so thecloneElement/child-props contract is allowed. Keep the other findings. Still required: type the accepted children (ReactElement<XProps>or exported subcomponents), keep injected props out of the public child type, and match by component reference instead ofdisplayName. -
filter—popoverIsOpenis seeded frompopover.props.isOpenand never re-synced, then overridden by the merged popover props; define one rule (ephemeral: uncontrolled with anisOpenoverride anddefaultIsOpen).onClicktakes no(event, data). -
filter— the defaultremoveFiltertext is built by interpolatingpropertyandpredicate(bothReactNode), which renders[object Object]for non-strings: take a full string viaassistiveText.removeFilteror a callback.isErrorpointsaria-describedbyat${id}-error, which nothing renders."Done"and thepredicate = 'New Filter'default are English outsidelabels. -
Remove the deprecated
forms/input/inline(InlineEdit, also exported asSLDSInlineEdit) in 11.0. It already warns and points to a read-onlyInputswapped with a baseInput, or aPopoverwith an iconButtontrigger. Delete the component,inline-check-props.js, its stories/examples/tests and snapshots, theindex.jsandindex.d.tsexports, and thelodash.isfunctiondependency if nothing else uses it. Breaking:refactor(forms)!:with aBREAKING CHANGE:footer and a migration note. No prop redesign is planned for it. -
Remove the deprecated
components/forms/checkboxalias in 11.0 (it is only reachable by deep import and wrapscheckboxwith the oldonChange(value, event, { value })parameter order). Deleteforms/checkboxand the warning branch incheckbox/check-props.js, then dropoldEventParameterOrderfromcheckbox(see thecheckboxitem above). Breaking for deep importers: needs a migration note pointing atcomponents/checkboxand theonChange(event, { value })signature. -
icon—assistiveText: object | stringunion (pick the object);inverse→isInverse;category: stringshould be a union. -
icon/button-icon—hint/inverse→isHint/isInverse(matchbutton);sizeunion lacksxx-smallwhilebutton'siconSizeaccepts it; drop the index signature. -
icon/input-icon— rendering switches betweenButtonand a plain icon on whetheronClickis passed (callback-presence mode switch): make it an explicit prop or split the component;iconPositionvsButtonIcon'sposition; drop the index signature. -
grid—flavor: stringshould be a union of the supported flavors;Grid.Columnstatic-property composition: decide the final API (named exports) now. -
icon-settings—onRequestIconPathis a return-value lookup, not a request: rename or document the exception before it is frozen. -
illustration— deprecated (XSS viapath);internalIllustration = truedefaults a boolean totrueand looks unused;illustration/path/nameprecedence is implicit: either remove in 11.0 or freeze as-is. -
input—defaultValueis a data twin ofvalue(index.tsx:116); the counter variant also mutates and reads the DOM value in uncontrolled mode. RemovedefaultValueand the DOM-reading paths.comboboxpassesdefaultValuethroughInnerInput, so fix both together (see thecomboboxcontrol item). -
input—variant: 'base' | 'counter'as a discriminated union:minValue/maxValue/stepand the numericonChangedata are counter-only,typeis silently forced to'number'for counter, andinlineEditTriggeronly renders withisStatic. Same forlabelvsassistiveText.labelandfixedTextLeftvsfixedTextRight, which are exclusive only incheck-props.js. -
input—iconLeft/iconRightareReactNodebutcloneElementinjectsiconPosition: replace with a named icon prop.styleContainer/styleInput→styleplusclassName*naming (and decide whereclassNamelands; it targets the container today). DroponSubmit/onKeyPress(meaningless/deprecated on an input) and keep one ofrequired/aria-required. DecidetabIndex/maxLength/minLengthtypes (string | numbertoday) and the ref target (forwardRefpoints at the wrapper div;inputRefis callback-only). Re-scoped by the child-contract rule: children here are library components, so thecloneElement/child-props contract is allowed. Keep the other findings. Still required: type the accepted children (ReactElement<XProps>or exported subcomponents), keep injected props out of the public child type, and match by component reference instead ofdisplayName. -
input— remove the sunset API from the contract:assistiveText.fieldLevelHelpButton, the string form ofsearch'sassistiveText, and the deadcheck-props.jschecks foriconCategory/iconName/iconPosition/iconAssistiveText/onIconClick. -
input/search—clearable→isClearable;onClear/onSearchtake(event)only, move to(event, data); make it aforwardRef. -
media-object—verticalCenter→isVerticallyCentered; decidebodyprop vschildren(consistent with thebadgecontentdecision);canTruncatealready follows thecan*rule. -
menu-dropdown—isOpenis half-controlled: the mode istypeof isOpenProp === 'boolean'(menu-dropdown.tsx:374), the internal state andonOpen/onClosestill run when controlled, and there is nodefaultIsOpen. UseisOpen !== undefined, adddefaultIsOpen, and addonRequestOpen/onRequestCloseso a parent can veto; giveonOpen/onClose(event, data). -
menu-dropdown— selection is data and must be controlled:valueis copied intoselectedIndex/selectedIndicesstate (:347,:412,:591) and multi mode toggles locally even whenvalueis set. Makevaluethe only source (array when multiple) and haveonSelectreport the next value. -
menu-dropdown—onSelectis(option, { option, optionIndex }), not(event, data)(and the custom-content path fires a fake empty option). Option shape:type(header | divider | item) plus a separatedivider: 'top' | 'bottom'field, optionalvalue(string | numberonly),label: string,leftIcon.category: string. Make it a discriminated union before consumers lock in the data. -
menu-dropdown—multiple→isMultiple(better: a union tyingmultipleto an arrayvalue);checkmark,hint,inverse,buttonInverse,inheritTargetWidthneedis*/has*names;optionsvschildrenexclusive only at runtime;tabIndexis astring;widthaccepts'bottom'(looks like a typo);lengthacceptsnull;assistiveText: Record<string, unknown>;iconVariantincludes'small'butButtonTrigger's does not;buttonVariant: 'icon'+iconNameallows invalid combos. -
menu-dropdown— child contracts: a custom trigger is found bydisplayName === MENU_DROPDOWN_TRIGGER(:1000), the list bydisplayName === LIST(:883), and every other child is cloned with an injectedonClickandkey: generateId()(:893, new key each render, so it remounts).ButtonTriggerfinds aButtonchild bydisplayName(button-trigger.tsx:127). Replace with atriggerprop and explicit list props. Also decide the ref story (FC, noforwardRef;requestFocusis a positional hack) and whetherListItem/ListItemLabelstay public (index.tsxexports them), and drop the deprecatedtooltip,offset,isInline. Child-contract rule: this slot accepts arbitrary consumer elements, so the contract is NOT covered by the library-components exception; the finding stands. -
menu-list—components/menu-list/*are 4-line aliases ofutilities/menu-list, not exported fromindex.js(deep import only). Remove them, or promote the real component. It is class-based,cloneElements a tooltip template into items (item.tsx:221), has index-basedonSelect(index),checkmark?: boolean(a tri-state withnull), andselectedIndex/selectedIndices(indexes, not values): decide before exposing it. -
modal— remove deprecatedtitle(useheading) andcloseButtonAssistiveText(useassistiveText.closeButton);disableClose→isDismissible(inverted, matchingalert),directional→isDirectional;ariaHideAppdefaults totrue: rename to a non-negativeis*that defaults tofalse, or document it as areact-modalpass-through exception;onRequestClose: () => void→(event, data);dismissOnClickOutsidehas a derived default (!disableClose). -
Remove the deprecated
modal/trigger(ModalTrigger.open, alsoSLDSModalTrigger) in 11.0: it creates a root it can never unmount or close and only wrapsModal. -
page-header— props are one flat bag, not a union onvariant:detailsis record-home only,nameSwitcherDropdown/trailobject-home only,label/infoonly some variants;baseignoresonRenderActions,trail,label,details. Make it a discriminated union before callers depend on the ignored props. -
page-header—onRenderActions/onRenderControlsmust returnPageHeaderControlelements, found bydisplayName === PAGE_HEADER_CONTROL(private/controls.tsx:69); anything else is silently dropped. Replace withactions/controlsslots (or arrays) and drop theon*names, which are not callbacks.iconiscloneElement-injected with a class inbase,object-homeandrecord-home. -
page-header—detailsitems:truncate→isTruncated,flavoris a raw SLDS size string, noid/key(index keys);joined→isJoined;truncatedefaults totruein the exportedTitle/DetailBlock(freeze or unexport them); remove the unreachable legacy props (iconCategory,iconName,contentRight,navRight, …) and branches. Hardcodedh1title (add a heading-level prop) andtitleof0drops. -
panel—PanelFilterGroupvariant?: 'panel'is a one-value variant that changes structure (built-in header/footer vsheader/footerslots): make it a union or two components;modified→isModified; flataddFilterLabel/cancelLabel/saveLabel/removeAllLabel/errorLabel/heading→labels; removeassistiveTextCloseFilterPanel(deprecated twin ofassistiveText.closeButton);onClickAdd/onClickRemoveAll/onRequestCancel|Close|Save→(event, data)with one naming scheme; drop the[key: string]: unknownsignature (...restis destructured and never spread); English strings live indefaultProps. -
panel—PanelFilterListusesChildren.mapandcloneElement(child, { isError }), wraps each child in<li>, and generates an error<p>not tied to the child byaria-describedby: replace with an item component takingerrorLabel.list-heading:heading/lockedHeading→labels; hardcodedlockedassistive text. Re-scoped by the child-contract rule: children here are library components, so thecloneElement/child-props contract is allowed. Keep the other findings. Still required: type the accepted children (ReactElement<XProps>or exported subcomponents), keep injected props out of the public child type, and match by component reference instead ofdisplayName. -
pill—bare→isBare;onRemove: () => voidis called without an event on keyboard and as the click handler elsewhere, and its presence toggles the remove button: move to(event, data)and an explicit prop;labels.labelvschildrenandiconvsavatarare exclusive only in practice (union);variant: 'link' | 'option'drives the role,hrefis link-only and defaults to'#', and the link variant nests an<a>in arole="button"span;tabIndex: number | string→number; the ref is an imperative handle (PillRef), not the root element. -
pill-container—PillOptionhas[key: string]: unknown, unprefixedbare/error(→isBare/hasError) and an optionalidused as the React key (make it required);avatar/iconareReactNode | { config }detected at runtime (pick one);variant: 'base' | 'bare'is a boolean in disguise;onRequestRemove'sindexis optional and removals are dropped when it is missing; wrap the flatonClickPill/onRequestRemovePillin aneventsobject (decided: keepeventsobjects, as incombobox/color-picker), each(event, data); alignassistiveText.listboxLabelwith the internalselectedListboxLabel; the consumeridis suffixed (-listbox-of-pill-options) and never reaches an element. -
popover—isOpenmode is correctly keyed off!== undefined, buthandleOpen/handleClosestill write the internal copy when controlled (popover.tsx:320,:343), there is nodefaultIsOpen, and noonRequestOpen(onOpenfires from the Dialog mount, so a controlled consumer cannot request opening). DecideonOpen/onClose/onRequestCloseas(event, data)together (the event isundefinedfor hover closes). -
popover— trigger contract: the first child iscloneElemented withid,onClick,onFocus,onMouseDown,onMouseEnter,onMouseLeave,tabIndex,aria-haspopup(:740-764;tabIndex: child.tabIndex || 0cannot express-1), other children become body siblings, andcheck-props.jsvalidates it bydisplayNamelist. Add atriggerprop (or render-prop) now, since it changes the meaning ofchildren. Child-contract rule: this slot accepts arbitrary consumer elements, so the contract is NOT covered by the library-components exception; the finding stands. -
popover— naming:ariaLabelledby→aria-labelledby/labels;disabled→isDisabled(it only forces closed, not a DOM pass-through);hasNoCloseButton/hasNoNubbin/hasNoTriggerStylesnegatives (decide: a positive form would default totrue);overlay: boolean | function(the function receives a shared DOM element); remove deprecatedfooterClassName,closeButtonAssistiveText,offset;headingvsariaLabelledbyas a union;icononly for thefeaturevariant (union). -
popover/edit-dialog— class component with an index signature cast toPopoverProps;handleOpen→onClick/onRequestOpen;isModified→hasChanges; a consumeronClose/onOpensilently replaces EditDialog's own handlers (spread last); stalepopoverdoc and an unused internalisOpen. -
progress-indicator— step status is three parallelProgressStep[]arrays (completedSteps/disabledSteps/errorSteps) matched byidthen byJSON.stringifyof the whole step (findStep,index.tsx:126); a step can be both error and completed (error silently wins). Move to onestatusper step (or one status map) beforeStepEventData(isCompleted/isDisabled/isError/isSelected) freezes.selectedStepis a required step object with the same matching and a silent fall back to index 0: use an id or index.steps: invalidsilently falls back todefaultSteps. -
progress-indicator—orientationandvariantare independent, butvariant="setup-assistant"forces vertical; make it a union.onRenderSetupAssistantAction(aReactNode, not a render function) andsetupAssistantEstimatedTimeexist only on the private vertical step type, so TypeScript users cannot pass them. StepassistiveTextis a plain string;tooltipIsOpenSteps("for development") andtooltipPositionare flat.step.id || indextreats an id of0as missing (bug: changes DOM ids when fixed). -
progress-bar—color: 'success'andradius: 'circular'are one-value unions acting as booleans: decideisCircular/ a widervariantbefore GA.classNamelands on the innerrole="progressbar"div whileid/stylego on the root: moveclassNameto the root (rendered-output change) and addclassName*for the bar. -
progress-ring—thememixes status and style (active | warning | expired | complete) andTHEME_OPTIONSis a second public surface;iconis only honored withhasIcon(implicit coupling: drophasIconor make a union); hardcoded English icon titles ("Warning", "Expired", "Complete") needassistiveTextkeys. -
radio—defaultCheckedis a data twin ofchecked(radio/index.tsx:60); makechecked+onChangerequired and drop it.onChangedata is{ checked: !checked }, which reportstrueevery time when uncontrolled. Remove the deprecatedlabel. -
radio—variant(base | button-group | swatch | visual-picker) as a discriminated union:coverable,vertical,size,onRenderVisualPicker*and swatchstyleare visual-picker/swatch only;descriptionis declared and never rendered;sizehas no default soslds-visual-picker_undefinedis emitted;button-group/swatchdepend on a parent group.deselectable/coverable/vertical/requestFocusneedis*/can*names;tabIndex: stringisparseInted (→number);refs: { input }andrequestFocus/onRequestFocusare a non-native ref/focus API (decideforwardRefto the input). (onRender*props named like callbacks: done, nowvisualPickerFigure*slots.) -
radio-group(seen fromradio) —RadioGroupinjectsname,onChange,aria-describedby,disabledwithChildren.map+cloneElement(radio-group/index.tsx:98): theRadioprops are part of that contract; decide the group's data model (avalue+options/context) before freezingRadio. Reviewed with the next batch. -
radio-group/radio-button-group— no group-level selection model: selection lives on each childRadio(checked/defaultChecked) and the grouponChangepayload is{ checked }, so consumers readevent.target.value. Recommended: requiredvalue+options: { value, label, disabled? }[](or a context thatRadioreads) +onChange(event, { value }).RadioGroupcloneElementsname,onChange,aria-describedby,disabledinto its children (radio-group/index.tsx:98), overwriting the child's own handlers and breaking for fragments/wrapped children. Re-scoped by the child-contract rule: children here are library components, so thecloneElement/child-props contract is allowed. Keep the other findings. Still required: type the accepted children (ReactElement<XProps>or exported subcomponents), keep injected props out of the public child type, and match by component reference instead ofdisplayName. -
radio-group—labels.erroris a string with a separateerrorIdand no invalid flag (addhasError, derive the id);assistiveText.labelreplaceslabels.labelin the legend;radio-button-groupduplicates thevariant: 'button-group'surface (pick one public API) and the button-group-onlystyleprop needs a union;classNamelands on.slds-form-element__control, not the rootfieldset;displayNameexists only for the old clone contract. -
scoped-notification—iconis aReactElementthat getscloneElemented to mergeassistiveText(:71): render an icon node, or takeiconName/iconCategoryand build it; the deprecatediconNameis still live (:83), remove or keep;theme: 'dark' | 'light'has an implicit third state (no default). Re-scoped by the child-contract rule: children here are library components, so thecloneElement/child-props contract is allowed. Keep the other findings. Still required: type the accepted children (ReactElement<XProps>or exported subcomponents), keep injected props out of the public child type, and match by component reference instead ofdisplayName. -
setup-assistant— steps are found bydisplayName === SETUP_ASSISTANT_STEP(other children are silently dropped) andcloneElemented withindex,onToggleIsOpen,stepNumber(index.tsx:61-73);onRenderFigureis checked bydisplayName === ICONand cloned withsize: 'small'(step.tsx:210). Replace with context or astepsarray; remove the injected props from the publicSteptype;onRenderAction/onRenderContent/onRenderFigureare nodes, not callbacks (→action/content/figure). Re-scoped by the child-contract rule: children here are library components, so thecloneElement/child-props contract is allowed. Keep the other findings. Still required: type the accepted children (ReactElement<XProps>or exported subcomponents), keep injected props out of the public child type, and match by component reference instead ofdisplayName. -
setup-assistant— stepisOpenmode is keyed off callback presence:toggleIsOpencallsonToggleIsOpenif provided, else sets state, whilegetIsOpenusesisOpen !== undefined(step.tsx:133-148), soonToggleIsOpenwithoutisOpennever toggles.isOpenis ephemeral: choose byisOpen !== undefined, adddefaultIsOpen.data.isOpenreports the current rather than the requested state, anddata.step(the props object) differs from the parent'sstep: ReactElement. -
setup-assistant—progressBaris only shown withisCard(union);progress(0–100) derives the ring theme from its value and ties the ring icon tostepNumber: decide astatusandprogressRingPropsbefore freezing;id/classNamesit on theol, which is not the root in card mode (section) or non-card mode (fragment). -
simple-data-table— columns are children matched bydisplayName === SimpleDataTableColumn.displayName(cells andRowActionslikewise), the row actions arecloneElemented, and keyboard nav sizes itself withReact.Children.count(index.tsx:146-198,:600). Replace with acolumnsarray prop, a namedrowActionsprop and a cell renderer. This is the largest remaining break in the component. Re-scoped by the child-contract rule: children here are library components, so thecloneElement/child-props contract is allowed. Keep the other findings. Still required: type the accepted children (ReactElement<XProps>or exported subcomponents), keep injected props out of the public child type, and match by component reference instead ofdisplayName. -
simple-data-table— selection is data:selectionanddisabledSelectionare optional with[]defaults and compared by reference (itemidentity); makeselection+ oneonChange(event, { selection })required and key byid. Two callbacks fire today (deprecatedonChange(selection, event)andonRowChange); settle the name before the deprecated one goes.selectRows: boolean | 'checkbox' | 'radio'→selectionMode: 'none' | 'checkbox' | 'radio'. -
simple-data-table— sort state is split across columns (isSorted,sortDirection,isDefaultSortDescending) so two columns can both be sorted, andheader-cellkeeps an internalsortDirectioncopy that drifts from the prop. Use a table-levelsort={{ property, direction }}+onSort(event, data)(todayonSort(data, event)). -
simple-data-table— booleans needis*/has*/can*names:columnBordered,fixedHeader,fixedLayout,joined,keyboardNavigation,noRowHover,stacked,stackedHorizontal,striped,unborderedRow,unbufferedCell(negations: positive names that default tofalse), columnprimaryColumn/sortable/truncate, cellrowHeader(library boolean, not a DOM pass-through),noHinton row actions. -
simple-data-table— callbacks and types:onLoadMore()has no event; theonToggleFixedHeaderListenersandonFixedHeaderResizeescape hatches (a fake{}event) exist because of awindowresize listener;onAction(item, selection)→(event, data);classNamelands on<table>butfixedHeaderwraps it in two divs;[key: string]: unknownon items, columns andassistiveText;type: 'header-row'is magic and collapses tostring;options: unknown[],dropdown: ReactNodeon row actions;SimpleDataTableRowActionsPropsand the column props are defined twice (types.tsvscolumn.tsx/row-actions.tsx); remove the deprecatedassistiveTextFor*props;fixedLayoutstill adds the vestigialslds-table_resizable-colsclass androle="grid"after resizing was removed. -
slider—value+defaultValuetwin (index.tsx:34-35,119,169-171) anddisplayValue = value ?? defaultValue ?? 0goes stale when uncontrolled. Requirevalue+onChange(event, { value }), dropdefaultValue. Also:vertical→isVertical;errorText→labels.error+hasError;labelduplicatesassistiveText.label;onInputduplicatesonChange. -
spinner—assistiveText: object | string(string form deprecated;check-propsis unwired, so no warning fires) → object only;hasContainer = truedefaults a boolean to true (rename to a non-true default, e.g.hasNoContainer, or avariant);isInline/isInputlook mutually exclusive → a singleplacementunion. -
textarea—value+defaultValuepassthrough → requiredvalue+onChange;onChange(event)has nodataarg;maxLength: stringparsed to a number →number;assistiveTextobject|string andlabelduplicate each other;errorText→labels.error+hasError; hand-typed aria props (aria-autocomplete: string,aria-haspopup: boolean) → extendTextareaHTMLAttributes;onSubmiton a textarea never fires;childrenrender after the control. -
toast—onRequestClose: () => voidhas no event/data (timer path has no event;alertdeclares(event: MouseEvent) => void) →(event | null, { trigger: 'timeout' | 'close-button' }). Auto-dismiss: the timer effect depends onhandleClose, which depends ononRequestClose, so an inline callback re-arms the timer on every parent render and may never dismiss; cleanup also returns focus (re-arm fixed in #3251). Decideduration0 semantics and rename (autoCloseDelay?).iconiscloneElemented (index.tsx:144, same pattern inalert) → decide both together.variantis required in the type but defaulted to'info'.assistiveText.closeButtonisReactNodecast to string.labels.headingLink+ top-levelonClickHeadingLinksplit. Close button is always rendered: addisDismissibleto toast and renamealert'sdismissibleto match (both before GA).ToastContaineris a class withdisplayName, a looserclassNametype, and noforwardRef. Re-scoped by the child-contract rule: children here are library components, so thecloneElement/child-props contract is allowed. Keep the other findings. Still required: type the accepted children (ReactElement<XProps>or exported subcomponents), keep injected props out of the public child type, and match by component reference instead ofdisplayName. -
tooltip—isOpenmode is keyed correctly (isOpenProp === undefined) but there is nodefaultIsOpen,onRequestOpen/onRequestCloseorevents.isOpen={false}is documented as "disables interaction", which conflicts with the controlled-false rule (comboboxprivate/menu.tsx:411relies on it) → addisDisabled. Dead/legacy:openByDefault(warned, not typed),isInline, deprecated-but-livetarget,variantstill acceptsinfo/error;learnMorecontent only renders whenonClickTriggeris set. The trigger is passed aschildrenandcloneElemented throughChildren.map(index.tsx:441), overwriting the child's handlers/aria-describedby, forcingkey, with no ref →triggerrender prop orasChild-style slot;contentdefaults to<span>Tooltip</span>.triggerClassName/triggerStyle/dialogClassName→className*;idis the popover id;onRequestTargetElementis a getter;positionmixes in the portal strategy. Consumers to migrate together: combobox menu (cloneElement of a user Tooltip),FieldLevelHelpTooltip(reads.propsfrom a Tooltip element), progress-indicator step, button. Child-contract rule: this slot accepts arbitrary consumer elements, so the contract is NOT covered by the library-components exception; the finding stands. -
tree— selection and expansion live on mutated node objects:TreeNode.expanded/selected/loading(types.ts:15-23),handleSelectwritesnode.selected = falseon the consumer's data (tree.tsx:184-190), and the tree keeps its own selection/focus copy seeded once fromnode.selected. Recommended: requiredselectedId+onSelect,expandedIds(+ optionaldefaultExpandedIds), immutable nodes, and rename node fieldsisExpanded/isSelected/isLoading. -
treecallbacks —onClickis really selection and also fires from Enter/arrows/focus with{ node, select, treeIndex }(selectderives from the mutated field;treeIndexis a positional string) →onSelect(event, { node, isSelected }).onExpandClickalso fires for keyboard and, withselect: true, for "focus parent" on Left-arrow → split intoonRequestToggle(event, { node, isExpanded })and focus handling.onScrolldata{ percentage }is NaN/Infinity when unscrollable and readsevent.target→ 0-1 ratio fromcurrentTarget. -
treeshape —getNodesmakes a second source of truth next tonodes(tree.tsx:70-71, used only below the first level) → drop or apply uniformly;type: 'branch' | 'item' | stringcollapses tostringandnodesis allowed on any node → discriminated union{ type: 'branch'; nodes } | { type: 'item' };assistiveTextis typed{ label }but also accepts a string,labelis really the heading,heading/assistiveTextare an XOR enforced only at runtime →labels.heading+assistiveText.headingunion; hard-coded<h4>;idis required (generate one; derived ids are markup contracts); internalFlattenedNodeis exported. -
trial-bar—onRenderActions: () => ReactNode→renderActionsoractionsslot;labels.timeLeft/timeLeftUnit/timeLeftUnitAfterare concatenated with the English default "left in trial" → one formatter or a singlelabels.timeLeftnode;TrialBarDropdownProps extends MenuDropdownPropsyet forcesinverseafter the spread (consumer's ignored) →Omit;TrialBarButtonoverridesstyle/classNameafter the spread (drops the consumer's); inherits menu-dropdown's trigger-child pattern. -
vertical-navigation—selectedIdis optional and defaults to the first item (index.tsx:55,73) andonSelectis optional → require both (allownull);NavigationItemhas an index signature (:20) with undeclaredicon/notificationBadge→ declare them;item.tsx:73,92cloneElements className intoicon/notificationBadgeand readsicon.props.size→ accept a name/props or render-prop; items withouturlrenderhref="#"→ button;aria-current={true}(item.tsx:66) should be"page"(fixed);<nav>has noaria-label→assistiveText.label;displayName;lodash.isfunction. Re-scoped by the child-contract rule: children here are library components, so thecloneElement/child-props contract is allowed. Keep the other findings. Still required: type the accepted children (ReactElement<XProps>or exported subcomponents), keep injected props out of the public child type, and match by component reference instead ofdisplayName. -
split-view—isOpenis hybrid (mode bytypeof === 'boolean', uncontrolled start hard-codedtrue, nodefaultIsOpen) →value !== undefineddetection +defaultIsOpen.events.onOpen/onClosetake only(event)→ singleonToggle(event, { isOpen }).assistiveText.toggleButtonOpenis the text shown while open ("Close split view") andtoggleButtonClosethe text shown while closed — inverted names; rename (closeToggle/openToggle) before GA. Closed state unmountsmaster(index.tsx:124), losing listbox focus/state — decide whether that is the contract.master/detailtypedReactElement | ReactElement[]→ReactNode. -
split-viewlistbox —selectionmatches by object identity (includes(item), alsounread) and is optional with an internalcurrentSelectedItemcopy feeding range/select-all logic → requiredselectionof ids (item.id) + requiredonSelect(selection drives the detail pane, so it is controlled);events.onSelectis required in the type whileeventsis optional.multiple→isMultiple(it is the ARIA name, so confirm under the native-attr rule).sortDirection: 'up' | 'down'→'ascending' | 'descending', and the sort button appears only whenevents.onSortexists (mode by callback presence) → explicitisSortable.SplitViewListboxItemhas an index signature;listItemis wrapped once by an HOC created in the constructor (later changes ignored) and receives internal props.SplitViewHeaderPropshas an index signature and loses PageHeader types →Omit<PageHeaderProps, …>; narrow the wideclassNametypes. -
tabs— selection is half-controlled:selectedIndex+defaultSelectedIndex, mode chosen byNumber.isInteger(index.tsx:115), internal state written even when controlled, and theindex !== internalSelectedIndexguard compares the stale copy, so controlledonSelectcan fire or be skipped wrongly. Index is identity (ids derive from it; reorder/removal changes selection) →value/idonTabsPanel, requiredvalue+onSelect, nodefaultSelectedIndex(selection drives other UI, so it is controlled — maintainer decision). -
tabscallbacks —onSelect(selectedIndex, lastIndex) => boolean | voidis positional and can veto byreturn false→onSelect(event, { selectedValue, previousValue }); drop the veto or replace with aonRequestSelectpre-state callback. Tabs has noeventsobject: choose top-levelonSelect. -
tabschildren —Tabsreadslabel/disabled/hasError/assistiveTextoffChildren.toArrayresults (index.tsx:111,243-246) so wrapped panels and fragments lose props;private/tab-panel.tsx:76reaches intochildren.props.children.TabsPanelPropslacksdisabled(typed only on the internalTabChildProps) — added;labelisstring | ReactElementon the panel butReactNodeinternally; decide atabsdata array or context registration.assistiveText.withErrorIcon→ final key name (errorIcon). Delete thetabs/pane.tsalias ("Remove at next breaking release"; onlyTabsPanelis exported).classNameisstring | string[] | Recordcastas string; noforwardRef(target rootdiv). Re-scoped by the child-contract rule: children here are library components, so thecloneElement/child-props contract is allowed. Keep the other findings. Still required: type the accepted children (ReactElement<XProps>or exported subcomponents), keep injected props out of the public child type, and match by component reference instead ofdisplayName. -
visual-picker— children contract:VisualPickercloneElementsindex,coverable,variant: 'visual-picker',name,size,verticalinto eachCheckbox/Radio(recursing through fragments), so wrapped options silently lose them and a consumer's ownname/variantis overwritten. It owns no selection: each option keeps its ownchecked/onChange(data stays controlled there; the picker must not add any). Recommended: context read byCheckbox/Radio(or anoptionsarray withvalue+onChange(event, { value })). Also:linksboolean switches to a different component (a baredivwith no fieldset/legend/className/id) → drop it and useVisualPickerLinkinside a distinct container or atypeunion;coverable/vertical→isCoverable/isVertical;label→labels.label(legend is rendered empty when absent →assistiveText.label);verticalis coerced!!vertical;classNamegoes on thefieldsetbut castas string; noforwardRef. TheVisualPickerChildPropsinterface is exported but is an injection contract, not an API. Re-scoped by the child-contract rule: children here are library components, so thecloneElement/child-props contract is allowed. Keep the other findings. Still required: type the accepted children (ReactElement<XProps>or exported subcomponents), keep injected props out of the public child type, and match by component reference instead ofdisplayName. -
visual-pickerlink —VisualPickerLinkis a class withdisplayName;title/descriptionare strings only (→ReactNode);hrefoptional renders an anchor with no href;classNametype isunknown[] | Record<string, unknown> | string; unusedid/rest pass-through (onClick,target,relare not accepted, so SPA routing is impossible) → extendAnchorHTMLAttributes;<h2>hard-coded (heading level should be configurable). -
welcome-mat—isOpen = truedefaults a boolean to true and the component is always a modal;onRequestClose: () => voidhas no(event, data)(modal passes none) → align with the modal decision;onRenderInfoActions→renderInfoActions/slot;doNotShowAgainCheckboxandinfoBadgeare ReactNode slots withinfoBadgecloneElemented to injectisComplete→ passisCompleteexplicitly from the parent; progress is derived bycloneElement/ countingchildrenforisComplete(wrapped tiles and fragments miscount, andChildren.countcounts non-elements) andvariantis cloned into each tile → context or astepsdata array;variant: 'splash'vs'info-only'render the same actions branch whileslds-welcome-mat_splashis keyed off "no children" instead ofvariant→ define the variant matrix as a discriminated union (steps/trailhead-connectedtake tiles and progress;info-only/splashtake actions);labels.unitsCompletedAftersplits a sentence (renderx/y+ label as one formatter);labels.title/descriptionare strings (→ReactNode). Re-scoped by the child-contract rule: children here are library components, so thecloneElement/child-props contract is allowed. Keep the other findings. Still required: type the accepted children (ReactElement<XProps>or exported subcomponents), keep injected props out of the public child type, and match by component reference instead ofdisplayName. -
welcome-mattile/info-badge — both are classes withdisplayNameandgenerateId();Tiletitle/descriptionstrings,hrefonly (noonClick/target; the info-only variant renders a non-link) andvariantis a duplicate prop injected by the parent (consumers can set it, then it is overwritten) → remove from the public type;TiledefaultassistiveTextreplaces the merged object shape;InfoBadgeimage: stringrenders an<img alt="">with fixed 50×50 (→imageSrc/ReactNode, alt text), shows the check icon even whenisCompleteis false, andonCompleteRenderActions→renderCompleteActions;classNameonInfoBadgereplaces the root class entirely.
From the same review; none of these change existing props.
-
forwardRef(typed to the real root element),className, and...restonaccordion,alert,avatar,badge,breadcrumb. -
avatar— resetimgLoadErrorwhenimgSrcchanges (track the failed src). - Move inline prop types into
types.tsfor the reviewed components. -
alert,avatar,breadcrumb—check-props.jsis not imported by these components, contrary to the note under Runtime prop validation. Wire it up or delete it (alert'sonDismisscheck also testsprops.isOpen). -
alert/container— convert the class component to a function component. -
accordion—summaryButtonsRefnever removes stale refs. -
builder-header/nav-link— React 19 ignoresdefaultPropson function components; move the defaults into the signature. Also renders<a href="#">for an action (should be a button), and-toolbarreadsaria-labelfromchild.props.label, butButtonGroup's label lives atlabels.label, so it is alwaysundefined. -
builder-header/toolbar—role="toolbar"has no roving focus. -
button,button-stateful,button-group—check-props.js(button, button-stateful) is not imported anywhere; wire it up or delete it. -
button-stateful— droplodash.isfunctionfor atypeofcheck. -
card/private/header— comment refers toquerySelectorAll(cssClasses.base), which the docs forbid;card/emptyignores its documentedid. -
forwardRef,...restandclassNameonbutton-group,button-stateful,card,builder-header(and its sub-components). -
checkbox—title="required"on the<abbr>needsassistiveText.required; widenlabels/errorTexttoReactNode; error markup is missing onbutton-groupandvisual-picker;check-props.jsis not imported (its toggle+indeterminate check should become a type error). -
carousel— arrow keys are swallowed inside custom item content; Next/Prev wrap even whenisInfiniteis false (guard lives in callers); autoplay'ssetIntervalcloses over a stalecurrentPanelin controlled mode (verify in a browser test); all panels arearia-hidden="false"; noaria-roledescription/label on the root; replace thewindowresize listener withResizeObserver; add a*.browser.test.jsx. -
color-picker—Toggle TransparencyandSaturation ${hue}% Brightness: ${v}%are hardcoded (addassistiveTextkeys);ColorPickerPropslives incolor-picker.tsx, nottypes.ts;check-props.jsis wired but only validateslabels.labelvsassistiveText.label. -
combobox— click-outside is dead:comboboxRefinindex.tsxis never attached andhandleClickOutsideis not exposed, so closing relies on a 200ms blur timeout that is never cleared; page-globalcurrentOpenDropdownlimits the page to one open combobox (andhandleOpenlooks inverted);check-props.jsreadsassistiveText.popoverLabelunguarded and referencesisInline/readOnlyMenuItemVisibleLength; split the 1,576-linecombobox.tsx; typefilter.js/add-subheadings.js;"${n} options selected"is concatenated English. -
date-picker—check-props.jswarns on props that no longer exist (abbrWeekDayLabels,onDateChange,strValue,isInline) and itsrequiredcheck testsprops.onFocus;defaultFormatteris hardcodedM/D/YYYY; year picklistYearplaceholder is hardcoded; addfirstDayOfWeek/locale;selectedDate: new Date()incalendar-wrapperis evaluated once at module load;Dialog/Inputare cast toany. -
files—FileandMoreFilesare still class components (defaultProps,generateIdin the constructor);Fileinjects a<style>tag per instance with non-SLDSdsr-file__*classes;FileActionshardcodes theDownloadtitle; no tests. -
dynamic-icon— no tests; English fallbacks (User is typing,Open App Launcher);forwardRef/...rest. -
expandable-section— fixedh3(add a heading-level prop),aria-hiddenon content that stays in the tree. -
docked-composer—displayNameis a literal, not aDOCKED_COMPOSERconstant; addclassNamefor the section andforwardRef/...rest; story gaps already listed under P2 (call states, popped-out, footer) can be added as new props. -
filter— props live infilter.tsxwhiletypes.tsholds only helper types;@ts-expect-erroron thePopoverspread;forwardRef/...rest. -
forms/private/label—title="required"hardcoded (needsassistiveText.required). -
grid—GridColumn.getClassNameyields"undefined slds-col"withoutclassName(fixed in #3239); class components; addforwardRef/...rest, a story and tests. -
icon-settings— memoize the context value (rebuilt every render); add tests. -
icon,icon/button-icon,icon/input-icon—forwardRef/...rest;checkPropsruns every render inbutton-icon; inlineproductThemebackground. -
illustration—sanitizePathusesconsole.log; hardcoded 400px inline height;...restis accepted but unused;heading: string. -
input—Searchdrops everyassistiveTextkey butlabel, and a consumeronKeyDownoverwrites its Enter handler (spread last); counter Enter handler checks onlykeyCode === 13; the stepping timeout is not cleared on unmount; nothing setsaria-invalidwitherrorText; no rest props (data-*,autoFocus,pattern,inputMode,form); widenlabeltoReactNodeandinputRefto acceptRefObject; dev warning forvaluewithoutonChange. -
media-object—forwardRef/...rest;figure &&renders0; missing SLDS variants (small/large, reversed figure, responsive). -
menu-dropdown— the shared module-leveloverlayspan andcurrentOpenDropdownglobal;event.keyCode(:716); hover timeouts can leak;hoversilently becomesclickinButtonTrigger; thequerySelector('button')fallback insaveRefToTriggerContainer;checkPropsruns every render; typeList(any) and the@ts-ignoreimports; widen optionvalue. (listItemRenderer→renderMenuItem: done.) -
menu-list— convertList/ListItemto function components;ListItemhashref: '#'default and an inline<style>block inList. -
modal—returnFocusTois captured at mount, not when it opens, so a modal mounted closed returns focus to the wrong element and focus is not restored when the parent closes it;toastis dropped when there is noheading/title/taglineor a customheader; bodyoverflowis reset toinherit(clobbers prior value, breaks stacked modals);stopPropagationon content clicks hides events from parent handlers;roleflips toalertdialogwithdisableClose(add an explicitrole); defaults ofdialogLabelledBy: ''; noforwardRef/...rest. -
page-header—check-props.jsis never imported (wire it or delete);related-listpasseslabelto bothLabelandTitle(likely a duplicate render, confirm against the snapshot);forwardRef/...rest;className*for internals; extract the triplicated icon-resolution block; unusedfieldContentRef; class components inprivate/. -
panel—forwardRef/...rest/classNameonPanelandPanelFilterGroup; memoize the mergedassistiveText; convertPanelFilterListto a function component withclassName. -
pill— the remove control is a<span role="button">withouttabIndexor its own key handler (keyboard removal depends on the root being focusable); deprecatedkeyCode;onClickis attached conditionally; inlinecursor: pointer;id/data-*/ rest props. -
pill-container—forwardRef; the effect at:139usesresetActiveSelectedOptionbefore it is declared with missing deps; optional controlled active index; Home/End. -
popover— clear thehoverCloseDelaytimer on unmount; hover/hybrid modes ignoredisabledand are mouse-first for keyboard users;aria-describedbyis set with no body andaria-haspopupon a disabled popover; module-levelcurrentOpenPopover/sharedoverlayleak across React roots. -
portal-settings— memoize the context value ({ renderTo }is new each render); accept anHTMLElementas well as a selector string. No breaking issues found. -
progress-indicator—forwardRef; vertical mode ignoresonStepFocus, tooltip props andisDisabled(still a clickable button), and the always-passed defaultonStepClickmakes the non-interactivespanbranch dead; disabled horizontal steps are<a role="button" tabIndex={0} aria-disabled>with no handler; localise theStep N:string; addassistiveText.percentageto the defaults. -
progress-bar— no accessible name withoutlabels.label(add anaria-labelfallback);aria-valuetextand the assistive-text span are both emitted; hardcodedheight: '100%'; clampvalueto 0–100;forwardRef/...rest. -
progress-ring—role="progressbar"has no accessible name;aria-valuenowcan show float noise; clampvalue;forwardRef/...rest. -
radio—check-props.jsis never imported (wire or delete);onKeyPressis deprecated (useonKeyDown, Space only); noonBlur; forwardname/required/readOnly; swatch<label style={{ border: '1px' }}>is invalid CSS;assistiveTextgoes toIconin the visual-picker variant (confirm intent);forwardRef;radio-group/radio.tsandradio-button-group/radio.tsare one-line aliases ofradio. -
radio-group—aria-describedbyonly goes to children;requiredshould addaria-required;forwardRef(root is afieldset). -
scoped-notification— allow overriding the hardcodedrole="status";id/rest props/forwardRef; the default'Info'label is English outsideassistiveText. -
setup-assistant—aria-expandedon the step toggles (onlyaria-controlsis set; the heading and icon buttons both toggle the same state); step detail is collapsed by CSS only; label support for theol; normalize the stepclassNametype; convertStepfrom a class (defaultProps, lodashassign,generateId) to a function component. -
simple-data-table— bug:getAssistiveTexttakes(…, actionsHeader, columnSort, ascending, descending, selectAllRows, selectRow)but the call passesselectAllRowsthird andcolumnSortsixth (index.tsx:801), so the deprecatedassistiveTextFor*props land in the wrong slots (fixed). Also:keyboardNavigationand the initialactiveCellare seeded from props in the constructor and go stale;rowIdfalls back togenerateId()on every render withoutitem.id;UNSAFE_componentWillUpdateresetsinteractiveElements(not concurrent-safe); directstyle/tabIndex/focus()DOM writes;role="grid"only withfixedLayoutwhile grid-style arrow navigation runs wheneverkeyboardNavigationis on; the loading spinner has noaria-live; hardcodedActionstitle/data-label; convert to a function component withforwardRef. -
slider— type the fullaria-*set (onlyaria-describedbytoday) and stop duplicating the consumer's describedby id on the error div; nocheck-props. -
spinner— drop redundantaria-hidden="false". -
textarea— simplify the describedby/error logic; wire or deletecheck-props.js. -
toast—check-props.jsis dead and copy-pasted from alert (message says "alert", testsprops.isOpen);role="status"+tabIndex={0}+ focus-on-mount is a questionable pairing; pause the timer on hover/focus; close button and heading link are<a href="#">→ buttons; pass throughdata-*/aria-*. -
tooltip— Esc is handled only on the trigger withstopPropagationand an uncleared timeout;aria-describedbyexists only while open (should be constant); fragile string class checks in mouse-leave; hard-coded#16325cplus a raw<style>;generateId()on each render →useId;checkPropsruns only on mount;displayName. -
tree— items remount every render (key={generateId()},branch.tsx:123) which loses focus →childNode.id;Item.handleFocusomitsfromFocus: true(item.tsx:159, unlikerender-branch.tsx:188), risking a double callback; hard-coded 'Expand Tree Branch' (render-branch.tsx:283) andicon: ''→assistiveText;flattenedNodesis state synced by an effect →useMemo;checkPropscast toany; loader uses hard-coded inline styles → SLDS skeleton; sibling lookup viafindIndex({id})assumes unique ids (dev warning); no Home/End/ typeahead and no Right-arrow on items; extract the duplicated key handling into a hook. -
trial-bar— no landmark/aria-label; noforwardRef/attribute pass-through; inconsistentclassnamesimport. -
vertical-navigation—check-props.jsis dead and warns on a nonexistentvariant;isDisabled/isUnreaditem flags. -
split-view—list-item-content.tsx:48setstitle={item.bottomLeftText}on thebottomRightTextspan (fixed);aria-sortis set to a localized string (must beascending|descending|other; move the text to the assistive span); shift-range.sort()is lexicographic (listbox.tsx:294, wrong for indices >= 10 — numeric sort);focusItemassumeslistItemComponents[index]exists;selectAllcomparesoptions === selectionby identity; Ctrl+A only (no Cmd+A), no Home/End/typeahead, Space does not activate therole="option"anchors; focus is initialized only at mount; odd default listaria-label; confirmButtonforwardsaria-expanded/aria-controls; convert listbox/HOC to function components; inlineheight: 100%with nostyleoverride. -
tabs—event.keyCodeis deprecated (index.tsx:206-209); no Home/End; vertical still binds Left/Right; arrows select and focus together (firesonSelectper press); click/key handlers sniff DOM classes andaria-disabled(:65-84) → per-tab handlers;focusstate cleared in a ref callback; all panels mounted withslds-hide(optional lazy mount); no rest/data-*pass-through. -
visual-picker— noforwardRef/attribute pass-through on either part;useIdis fine butRadio/Checkboxreceiveindexandnameas props → verify the names stay unique with multiple pickers; add keyboard/roving-focus guidance for the radio case; legend is empty (not omitted) when nolabel. -
welcome-mat— Tile uses<a>for non-navigating tiles (button when nohref); completed state is announced only by the icon text, not on the link; progressProgressBarhas no label;trailhead-connectedhides the progress text on completion without an announcement;WelcomeMatInfoBadge/Tileclass → function components; focus and return-focus rely onModal(seemodalP1 note on focus captured at mount). - Tests for
badgeandbreadcrumb(already listed under the P2 test gaps below).
Component folders with no Storybook story:
-
grid -
menu-list
Component folders with no Vitest test (template: components/button/__tests__):
-
badge,breadcrumb,checkbox,dynamic-icon,files,grid,icon-settings,menu-list,panel,portal-settings,progress-bar,progress-ring,radio,scoped-notification,trial-bar
Stories missing from the HTML snapshot suite: the snapshot test only globs
__docs__/*.stories.*, so these nested stories are rendered by Storybook but not snapshotted:
-
components/input/__docs__/search/SearchInput.stories.jsx
Story/feature gaps vs. SLDS 2 — from the component-by-component comparison in
docs/superpowers/specs/2026-08-07-slds2-vs-react-comparison.md.
Ordered by leverage; skip anything filed "Legacy Components" upstream.
- Token-theming ("Customization") story pattern. SLDS 2 attaches a live
styling-hook / design-token panel to ~20 components; React has none. Build a shared
decorator + exemplar "Theming" stories for
button,card,input,avatarfirst, then extend. - Wire existing
__examples__into stories:-
builder-header— toolbar, utilities, successful/after-successful/failed-save stories. -
combobox— Storybook coverage lost in the TypeScript rewrite restored (#3221).
-
- Add missing high-value state stories:
-
docked-composer— voice/telephony call-state suite (~10 states), popped-out, overflow menu, log-a-task. -
card— Loading, Collapsed, NestedCards, DataTiles. -
checkbox— fieldset group variants, read-only "view mode", help-text/tooltip. -
accordion— ActionMenu, Nested, WrappedInCard, heading truncation. -
modal— SizeFull;tabs— conditional tab, scoped-with-overflow. -
radio-group— help-icon(+tooltip), disabled-checked, error-checked, label-placement.
-
Component feature gaps (capability missing, not just a story) — file as enhancement issues, not story fill-ins:
-
vertical-navigation— badge / compact / icon / shaded / overflow / validation variants. -
visual-picker— small size tier, disabled state. -
date-picker— date-range selection (SLDS 2 is a dual single/range picker; React is single-only). -
input— nativetypevariants (checkbox / toggle / range / color / file).
- Convert
components/utilities/dialog/index.jsxto.tsx, and drop the popper-shaped adapter (computeFloatingData, consumed byutilities/dialog-helpers.js). - Delete the vendored
assets/styles/slds-plus.css; nothing outside planning docs references it. - Work down the lint warning backlog.
- popper.js →
@floating-ui/domfor all positioning - Toolchain majors: Vite 8, Vitest 4, ESLint 10 (flat config), Babel 8, jsdom 30
- Publish pipeline repaired and switched to the
alphanpm tag (Node 24) - Storybook theme toolbar for Lightning Blue / Cosmos and light / dark / system color schemes
- Removed in v11:
AppLauncher,BrandBand,Expression,GlobalHeader,GlobalNavigationBar,LocationMap,Lookup,Notification,Picklist,TimePicker;DataTablerenamed toSimpleDataTable
- Node.js — Requires Node.js >= 24.
- React 19 only — no backward compatibility with React 16/17/18. Use 0.10.x for those.
- No bundled CSS — the package does not ship a stylesheet; consumers add one from
@salesforce-ux/design-system-2.
Converting components to TypeScript removed their static propTypes. TS types are
erased at build time, so a plain-JS/Babel consumer no longer gets React's
dev-mode PropTypes console warnings (wrong prop type / missing required prop) for
converted components. This is a deliberate, conventional tradeoff for a TS
migration (React itself deprecated PropTypes in v19; MUI, Chakra, etc. made the
same move). We accepted it rather than dual-maintaining hand-written PropTypes
alongside the types.
What still protects consumers:
- Shipped
.d.tsdeclarations. The package emits per-file declarations mirroring the source tree (dist/types/**, wired through theexportsmap). Modern editors (VS Code et al.) pick these up — for TS users directly, and for plain-JS users via Automatic Type Acquisition (ATA) — giving rich autocomplete and inline type/required-prop warnings at author time, with zero runtime overhead. This covers most of what PropTypes did, earlier (in-editor) and for both audiences. checkPropsdev warnings survive. Thecheck-props.jsdev-only system (deprecation notices and one-of-required-property business rules) is still invoked by converted components — a different, narrower check than PropTypes, and unaffected by the migration.
If runtime enforcement for JS consumers is later deemed necessary, generating
PropTypes from the TS types at build (e.g. babel-plugin-typescript-to-proptypes)
is the path — evaluated and deferred, not precluded.
Good first issues (P2 template-driven work):
- Add a Vitest + RTL test for a component that lacks one (template:
components/button/__tests__) - Add a CSF story for
gridormenu-list
Community-driven, no fixed deadlines. Progress depends on contributor availability.