sf-react-base-components
Salesforce Lightning base components, ported to React.
Built on design-system-react for markup
and @salesforce/platform-sdk for data.
79 components · 160 tests · 0 type errors · React 18 · TypeScript strict
Community port. Not an official Salesforce product - see NOTICE.
Lightning base components only run inside the Lightning runtime. Multi-Framework apps run
React, where lightning/* modules and the @wire decorator do not exist.
This is the missing half: the same component API, the same SLDS markup, the same
accessibility, rewritten as React components you can npm install into a Multi-Framework
UI bundle.
Every component was ported by reading the original LWC source, not by guessing from docs. Prop names, variant values and validation messages match the originals, so Lightning markup translates line by line.
lightning-button variant="brand" label="Save" onclick={handleSave}
→ <Button variant="brand" label="Save" onClick={handleSave} />
Coverage: 81 of the 89 public components in lightning-base-components@1.28.19-alpha.
npm installWrap your tree once. DSR resolves icon sprite paths from context, so without
IconSettings no icon renders at all.
import IconSettings from '@salesforce/design-system-react/components/icon-settings'
import '@salesforce-ux/design-system/assets/styles/salesforce-lightning-design-system.min.css'
<IconSettings
onRequestIconPath={({ category, name }) =>
`/assets/icons/${category}-sprite/svg/symbols.svg#${name}`
}
>
<App />
</IconSettings>Then use the components.
import { Button, Card, Datatable, Input, useOverlay } from 'sf-react-base-components'
function AccountPanel({ rows }) {
const { toast } = useOverlay()
return (
<Card title="Accounts" actions={<Button variant="brand" label="New" />}>
<Input label="Search" type="search" value={term} onChange={setTerm} />
<Datatable
keyField="Id"
columns={[
{ label: 'Name', fieldName: 'Name', editable: true },
{ label: 'Revenue', fieldName: 'AnnualRevenue', type: 'currency' },
]}
data={rows}
onSave={(drafts) => toast({ message: `${drafts.length} saved`, level: 'success' })}
/>
</Card>
)
}| Area | # | Components |
|---|---|---|
display | 15 |
Accordion AccordionSection Avatar Badge
Breadcrumb Breadcrumbs Card Carousel
CarouselImage Layout LayoutItem Pill
PillContainer Slider Tile
|
formatted | 13 |
FormattedText FormattedNumber FormattedDateTime
FormattedTime FormattedEmail FormattedPhone
FormattedUrl FormattedAddress FormattedName
FormattedLocation FormattedLookup FormattedRichText
RelativeDateTime
|
input | 13 |
Input (16 types) Select Textarea Combobox
CheckboxGroup RadioGroup DualListbox
InputAddress InputLocation InputName
ColorPickerPanel ColorPickerCustom ColorSwatch
|
actions | 12 |
Button ButtonGroup ButtonIcon
ButtonIconStateful ButtonStateful ButtonMenu
MenuItem MenuDivider MenuSubheader Icon
DynamicIcon Helptext
|
navigation | 12 |
Tab Tabset VerticalNavigation + 5 sub-components
ProgressBar ProgressIndicator ProgressStep
ProgressRing
|
overlay | 10 |
Modal ModalHeader ModalBody ModalFooter
Toast ToastContainer · useOverlay() returning
alert confirm prompt toast
|
table | 4 |
Datatable (inline edit, sorting, selection, resize) Tree
TreeItem TreeGrid
|
RecordFields | 3 |
One component covering record-form, record-edit-form and
record-view-form
|
<RecordFields> replaces three Lightning components at once.
| Lightning | <RecordFields> |
|---|---|
record-view-form + output-field |
mode="view" |
record-edit-form + input-field + messages |
mode="edit" with fields |
record-form |
mode="edit" with layout |
import { createDataSDK } from '@salesforce/platform-sdk/data'
import { DataContext, RecordFields, createSalesforceData } from 'sf-react-base-components'
const sdk = await createDataSDK()
const { adapter, formatters } = await createSalesforceData(sdk)
<DataContext.Provider value={adapter}>
<RecordFields
objectApiName="Account"
recordId={recordId}
fields={['Name', 'Phone', 'AnnualRevenue']}
mode="edit"
columns={2}
formatters={formatters}
onSuccess={(record) => console.log(record)}
/>
</DataContext.Provider>Omit recordId to create a record. Omit fields and pass layout="Full" | "Compact" to
use the org layout.
It handles object describe, layout lookup, field-type to control mapping, field-level security, required-field validation, and splits save errors into a page banner and per-field messages.
DataAdapter in src/data.ts is the only place that talks to Salesforce. The tests use an
in-memory adapter; createPlatformAdapter(sdk) is the real one.
It calls UI API REST through sdk.fetch, not sdk.graphql. A describe-driven form has no
fixed field set, so a GraphQL document would have to be generated on every render and still
could not return UI API's per-field save errors. Move getRecord onto sdk.graphql if you
want its response cache or QueryResult.subscribe().
Verify against your org:
getObjectInfocalls/ui-api/object-info/{object}. That path is not in the Data SDK's documented endpoint list, although/ui-api/recordsand/ui-api/layoutare. It is one function insrc/platformAdapter.ts, so swapping it for a GraphQLobjectInfosquery is a contained change.
FormattedRichText renders untrusted HTML, so it carries an allowlist sanitizer that
replaces Lightning's purifyLib. The tag and attribute allowlists and the URI regex are
ported from richTextConfig.js verbatim.
src/formatted/sanitizeHtml.security.test.ts holds 15 bypass payloads as a permanent
regression suite: entity-encoded schemes, iframe srcdoc, mXSS through
<math><mtext><style>, formaction, xlink:href, <base>, <meta refresh> and nested
<scr<script>ipt>.
The style attribute is deliberately kept, because richTextConfig.js allows it too.
Stripping it would render rich text differently from Lightning.
Behaviour the originals have and these ports do not.
| Gap | Where | Why |
|---|---|---|
| No "More" overflow collapse | Tabset, ButtonMenu, PillContainer |
Needs live width measurement |
| No cell keyboard traversal | Datatable |
Lightning's keyboard.js is 42 KB of NAVIGATION/ACTION mode |
| No header action menus, no virtualization | Datatable |
DSR wraps sortable headers in a button |
| en-US field order only | InputName, InputAddress |
No internationalizationLibrary outside Lightning |
confirm / prompt never reach the host |
useOverlay |
View SDK returns Promise<void> - no answer channel |
Hand-written SLDS, not DSR modal |
Modal |
DSR has no size="full" and no role="alertdialog" |
| Lightning | What it needs |
|---|---|
lightning-record-picker |
DSR combobox + GraphQL search, debounce, recent items |
lightning-file-upload |
/connect/file/upload/config plus ContentVersion |
lightning-input-rich-text |
An external editor, plus the sanitizer above |
lightning-map |
DSR location-map. Geocoding is on you |
lightning-quick-action-panel |
DSR modal. The action lifecycle stays platform-side |
NavigationMixin |
createViewSDK().navigateTo |
Reference fields in RecordFields render as a plain Id input until record-picker lands.
npm run dev # demo page against an in-memory adapter
npm test # vitest - 160 tests
npm run typecheck # tsc --noEmit
npm run build # vite buildsrc/
data.ts DataAdapter contract, UI API types, error normalisation
platformAdapter.ts the @salesforce/platform-sdk implementation
fieldMeta.ts field type → control mapping and formatters
RecordFields.tsx record-form / record-edit-form / record-view-form
actions/ display/ formatted/ input/ navigation/ overlay/ table/
CONVENTIONS.md holds the porting rules every area follows.
The lightning-base-components npm package ships the readable LWC source of all 211
modules. Each area was ported by reading the original .js and .html, taking the public
API from the @api properties.
Where DSR could not carry the Lightning API, the SLDS markup was written by hand rather
than dropping props. That is why Button, Avatar, Tile, Layout, Modal,
ProgressBar and ProgressRing are hand-written: DSR's versions silently drop
accessKey, fallbackIconName, size="full" or the ARIA the originals render.