Skip to content

Repository files navigation

Salesforce React Base Components

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.


Why this exists

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.


Quick start

npm install

Wrap 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>
  )
}

Components

Area#Components
display15 Accordion AccordionSection Avatar Badge Breadcrumb Breadcrumbs Card Carousel CarouselImage Layout LayoutItem Pill PillContainer Slider Tile
formatted13 FormattedText FormattedNumber FormattedDateTime FormattedTime FormattedEmail FormattedPhone FormattedUrl FormattedAddress FormattedName FormattedLocation FormattedLookup FormattedRichText RelativeDateTime
input13 Input (16 types) Select Textarea Combobox CheckboxGroup RadioGroup DualListbox InputAddress InputLocation InputName ColorPickerPanel ColorPickerCustom ColorSwatch
actions12 Button ButtonGroup ButtonIcon ButtonIconStateful ButtonStateful ButtonMenu MenuItem MenuDivider MenuSubheader Icon DynamicIcon Helptext
navigation12 Tab Tabset VerticalNavigation + 5 sub-components ProgressBar ProgressIndicator ProgressStep ProgressRing
overlay10 Modal ModalHeader ModalBody ModalFooter Toast ToastContainer · useOverlay() returning alert confirm prompt toast
table4 Datatable (inline edit, sorting, selection, resize) Tree TreeItem TreeGrid
RecordFields3 One component covering record-form, record-edit-form and record-view-form

Record data

<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.

The data seam

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: getObjectInfo calls /ui-api/object-info/{object}. That path is not in the Data SDK's documented endpoint list, although /ui-api/records and /ui-api/layout are. It is one function in src/platformAdapter.ts, so swapping it for a GraphQL objectInfos query is a contained change.


Security

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.


Known gaps

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"

Not ported

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.


Development

npm run dev         # demo page against an in-memory adapter
npm test            # vitest - 160 tests
npm run typecheck   # tsc --noEmit
npm run build       # vite build
src/
  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.

How this was ported

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.

About

React port of LWC Base Components

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages