Theming
Every component reads its colors, radii and type from tokens. The Salesforce themes fill them with SLDS 2's own values, extracted from Salesforce's stylesheet; Neutral fills them from shadcn's variables. Pick a theme with its theme step, npx shadcn@latest add @wisty/<theme> (see Installation), and dark mode with the dark class on <html>.
The themes
| Theme | Add it with | What it is |
|---|---|---|
| Cosmos | @wisty/cosmos |
Salesforce's current look, and the default |
| Lightning Blue | @wisty/lightning-blue |
Salesforce's earlier Lightning look, SLDS 1. It has no dark mode. |
| Neutral | @wisty/neutral |
shadcn's own look |
Cosmos and Neutral have a dark mode, under the dark class. A theme step writes one theme into your stylesheet's :root and .dark, so the whole app takes it. Edit the values in place to adjust them; the next theme step writes over them.
More than one theme
To switch themes while the app runs, or to show one theme inside another, import the theme files by hand (see Installation) and pick a theme with an attribute:
| Theme | Set it with |
|---|---|
| Cosmos | data-theme="cosmos", or nothing |
| Cosmos dark | data-theme="cosmos" and the dark class |
| Lightning Blue | data-theme="lightning-blue" |
| Neutral | data-theme="neutral" |
| Neutral dark | data-theme="neutral" and the dark class |
Import tokens.css, then each theme file you use, Neutral's last. Put the attribute and the dark class on the same element, usually <html>. Popovers, selects and dialogs render into document.body, so a theme on a smaller element doesn't reach them.
<html data-theme="cosmos" class="dark">
<body>
…
</body>
</html>Neutral
Neutral is for apps that use the components and blocks without Salesforce's look. It starts from shadcn's neutral theme and reads shadcn's variables: --primary, --background, --border, --ring, --radius and the rest. After npx shadcn@latest add @wisty/neutral, they're in your stylesheet's :root and .dark with shadcn's values. Change them as you would in any shadcn project. Components and blocks follow, and so do tokens shadcn has no variable for, like the selected item's fill and the link color.
:root {
--primary: oklch(0.55 0.2 260);
--radius: 0.5rem;
}
.dark {
--primary: oklch(0.7 0.15 260);
}With the files imported by hand, Neutral needs data-theme="neutral" on <html>. Its light-mode values have zero specificity and its dark-mode values have .dark's, so your own :root and .dark rules after the import win.
Success, warning and info colors, which shadcn has no variable for, come from Tailwind's palette. Display density works in Neutral. Salesforce's brand hooks don't apply there, so set --primary instead.
Your own overrides
Override the tokens, never the class strings: components read var(--primary), var(--border) and the rest, the names shadcn uses where SLDS has the same idea. After a theme step, the theme's values are your :root and .dark values: edit them there, and run the step again to get the theme's own back. With the files imported by hand, the default theme sits at zero specificity, so your own :root values win without !important.
Fonts work the way they do in shadcn, in every theme. Set --font-sans and --font-mono on :root. The page's text, the font-sans and font-mono utilities and every component use them. Without them, each theme keeps its own stack: Salesforce's system font in Cosmos and Lightning Blue, Tailwind's in Neutral. font-serif is Tailwind's own utility and reads your --font-serif the same way.
:root {
--font-sans: "Geist", system-ui, sans-serif;
--font-mono: "Geist Mono", ui-monospace, monospace;
}Your org's brand
In the Salesforce themes, an admin can give a Salesforce org its own theme: a brand color, optional accent colors and an optional text color. Lightning puts them on the page as Salesforce's own styling hooks. The components read the same hooks and use the theme's colors until you set them.
Set them on <html>. With the theme files, any element above the one with data-theme works too. Hooks set lower down are ignored.
:root {
/* The brand ramp Lightning generates from the org's brand color */
--slds-r-color-brand-50: #c935a4;
--slds-r-color-brand-40: #a02982;
--slds-r-color-brand-15: #430c35;
/* …and the other steps, 5 to 95 */
/* Optional, when the org's theme overrides them */
--slds-g-color-accent-container-1: light-dark(#0b7a3e, #c935a4);
--slds-g-color-on-surface-3: light-dark(#430c35, #faddf0);
}| Hooks | What they color |
|---|---|
--slds-r-color-brand-5 to --slds-r-color-brand-95 |
The brand ramp: buttons, links, focus, selection |
--slds-g-color-accent-1 to -3, accent-container-1 to -3, on-accent-1 to -3 |
The org's accent overrides, and text on them |
--slds-g-color-border-accent-1 |
Borders in the accent color |
--slds-g-color-on-surface-3 |
Default text and headings |
To copy an org's values, open any Lightning page in that org and read these properties on <html> in the browser's developer tools.
An app that runs inside Salesforce in its own frame, such as a Multi-Framework app or a Canvas app, doesn't receive the org's hooks or its light or dark mode. Salesforce doesn't pass them into the frame, so the component that embeds the app has to forward them. Matching a Salesforce org lists what to read and set.
Display density
Each Salesforce user picks a display density: comfy, the default, or compact. Compact puts field labels beside their fields and tightens spacing. Set data-density="compact" on <html>, the element that holds the theme:
<html data-density="compact"></html>| What changes in compact | How |
|---|---|
| Cards, page headers | Less padding |
| Card, section and accordion titles | One step smaller |
| Body text | Line height normal instead of 1.5 |
| Record detail, inline edit, form | Labels beside values and controls, from a 48rem screen; above them on a narrower one |
Buttons, inputs, menus and tables keep their size, as in Lightning. The values are Salesforce's own: the themes keep SLDS's density hooks (--slds-g-spacing-var-*, --slds-g-font-scale-var-*, --slds-g-font-lineheight-var-base), so a page that sets them directly gets the same result.
A field with layout="auto", the record blocks' default, follows the nearest data-density. Put data-density="comfy" on a region to keep its fields stacked on a compact page, or give one block layout="stacked" or layout="horizontal".
Matching a Salesforce org
What the embedding component reads from the Lightning page, and what the app sets:
| The org's | Read on the Lightning page | Set in the app |
|---|---|---|
| Design system | SLDS 2 when --slds-g-color-surface-1 on <html> holds a light-dark() pair; otherwise SLDS 1 |
data-theme="cosmos" for SLDS 2, data-theme="lightning-blue" for SLDS 1, with both theme files imported |
| Brand | The hooks in the table under Your org's brand | The same hooks on <html> |
| Light or dark | slds-color-scheme_dark on <body> |
The dark class |
| Density | Compact when --slds-g-spacing-var-1 differs from --slds-g-spacing-1 |
data-density="compact" |
Lightning Blue and Salesforce's other built-in themes are SLDS 1. Cosmos and custom themes are SLDS 2. An SLDS 1 theme's color arrives in the accent hooks, so forwarding every hook under Your org's brand carries it too.
Icons
Salesforce's icons come from the @salesforce-ux/icons package, unaltered. Copy its dist/salesforce-lightning-design-system-icons folder to public/assets/icons, or point an IconProvider at wherever you serve them.