Theming
Set --fd-* tokens to theme colors, type, spacing, radius and motion for a site, a section or one component, and ::part() to restyle one element.
How it works
Link to section How it worksEvery token has two names. You set the public one, --fd-*. Components read a private one, --_fd-*, which each component resolves from the public name or its default. Custom properties inherit through shadow roots, so a value you set on any ancestor reaches every fandry component below it, and the ones those components render.
:host {
--_fd-primary: var(--fd-primary, 226 90% 50%);
--_fd-radius-md: var(--fd-radius-md, 0.375rem);
/* ... */
}
Set
--fd-*names only. The--_fd-*names are internal and can change.Colors are bare HSL channels with no commas and no
hsl():--fd-primary: 210 90% 40%. Components wrap them inhsl()themselves, which is how they add transparency.Nothing needs a global stylesheet: every token has a default. Set only what you want to change.
Set a token for the whole site
Link to section Set a token for the whole sitePut the tokens on :root in a stylesheet the page loads. Every fandry component on the page inherits them.
:root {
--fd-primary: 210 90% 40%;
--fd-primary-foreground: 0 0% 100%;
--fd-radius-md: 0;
--fd-font-sans: "Brand Sans", system-ui, sans-serif;
}
LWR / LWC OSS: src/layouts/index.html
<head> <link rel="stylesheet" href="/assets/theme.css" /> </head>
:root in a component stylesheet does not work
A component's own CSS is scoped to that component, so:root { ... } in my/app/app.css never reaches the page. Use a stylesheet the layout loads, or set the tokens on :host (see below).
On Salesforce, where the page is not yours to edit, pick the place that matches where your components run:
Experience Cloud LWR site: in Experience Builder, Settings > Advanced > Edit Head Markup, add a
<style>with the:rootrule, or a<link>to a static resource holding it.Lightning pages, apps and record pages: set the tokens on
:hostof your outermost component. Everything it renders inherits them, including fandry components nested in other components.A whole Lightning app from one file: upload the
:rootrule as a CSS static resource and load it once withloadStylefromlightning/platformResourceLoader.
:host {
--fd-primary: 210 90% 40%;
--fd-radius-md: 0;
}
Salesforce: loadStyle a static resource named fandryTheme
import { LightningElement } from 'lwc';
import { loadStyle } from 'lightning/platformResourceLoader';
import fandryTheme from '@salesforce/resourceUrl/fandryTheme';
export default class MyApp extends LightningElement {
connectedCallback() {
loadStyle(this, fandryTheme);
}
}
Override for one section
Link to section Override for one sectionSet the same tokens on any element. Only the components inside it change; the nearest value wins, as with any inherited CSS property.
my/app/app.html<section class="promo"> <fandry-card>...</fandry-card> <fandry-button>Shop the sale</fandry-button> </section> my/app/app.css
.promo {
--fd-bg: 38 92% 95%;
--fd-primary: 24 95% 40%;
}
Override for one component
Link to section Override for one componentA fandry component reads its tokens on its own element, so a token set on the element itself changes that one instance, and whatever it renders: set --fd-primary on a fandry-table and its checkboxes and pagination buttons follow.
<fandry-button class="danger">Delete</fandry-button> <fandry-button>Cancel</fandry-button> <!-- or inline, for a one-off --> <fandry-input label="Search" style="--fd-radius-md: 999px"></fandry-input> my/app/app.css
/* one instance */
.danger {
--fd-primary: 0 72% 51%;
}
/* every fandry-button this component renders */
fandry-button {
--fd-radius-md: 999px;
}
Put the rule where the element is
A selector only matches elements in its own shadow tree..danger or fandry-button in app.css matches the buttons in app.html, not buttons inside some other component, and a global stylesheet's fandry-button rule matches only buttons outside every component. To change every button on the site, set the token on :root; to change the buttons of one component, set it in that component's CSS. On Salesforce the tag is c-fandry-button.Restyle one element inside a component
Link to section Restyle one element inside a componentWhen no token covers it, style the element directly with ::part(). Each component's page lists its parts and states. A part also carries the name of each state while it is on, so naming both targets that state only. The same rule as above applies: write it in the stylesheet of the component whose template renders the element.
fandry-link::part(link) {
color: inherit;
text-decoration: none;
}
fandry-checkbox::part(control checked) {
background: hsl(160 84% 26%);
}
.danger::part(base) {
text-transform: uppercase;
}
Parts need native shadow DOM, which every fandry component uses, on LWR and on Salesforce alike. Tokens work everywhere.
Keep box-shadow off a control's part
A control draws its focus ring as abox-shadow, and a ::part() rule from outside wins over the component's own, in every state. So fandry-button::part(base) { box-shadow: ... } leaves a focused button with no ring. Put a shadow on the element instead, fandry-button.cta { filter: drop-shadow(0 6px 12px hsl(160 84% 26% / 0.35)) }, or change the ring with --fd-ring-control.Which value wins
Link to section Which value winsAn inline style or a rule on the component element itself.
The nearest ancestor that sets the token: a section, then your component's
:host, then:root.The default in
tokens.css.Reduced motion wins over all of them: when the user asks for it, the transition durations go to
0mswhatever the site sets. Looping indicators (spinner, skeleton) keep running.
Using tokens in your own CSS
Link to section Using tokens in your own CSSThe private --_fd-* names only exist inside fandry components and components that extend fandry/base. A component that extends Base reads them like the library does. Any other CSS should read the --fd-* names, with a fallback: outside a fandry component the private names are not set, and markup you slot into one inherits them only while it sits in that slot.
/* my component extends Base from 'fandry/base' */
.toolbar {
gap: var(--_fd-space-2);
color: hsl(var(--_fd-text-muted));
}
/* a plain LightningElement */
.toolbar {
gap: var(--fd-space-2, 0.5rem);
}
Every token
Link to section Every tokenThe defaults are in fandry/base/tokens.css (node_modules/fandryui/modules/fandry/base/ on LWR, fandryBase/ on Salesforce). --brand-primary and --brand-accent still work as the older names for --fd-primary and --fd-accent. Info alerts and toasts use --fd-info, or --fd-accent when only that is set. Beyond that, no fandry component draws in --fd-accent: it is a highlight color for your own CSS.
Color (H S% L%) --fd-primary --fd-primary-foreground
--fd-accent --fd-accent-foreground --fd-info
--fd-success --fd-success-foreground
--fd-warning --fd-warning-foreground
--fd-danger --fd-danger-foreground
--fd-bg --fd-bg-muted --fd-text --fd-text-muted
--fd-border --fd-border-focus --fd-link-visited
Borders --fd-border-width --fd-border-width-md --fd-border-width-lg
Radius --fd-radius-sm --fd-radius-md --fd-radius-lg --fd-radius-full
Spacing --fd-space-1 --fd-space-2 --fd-space-3 --fd-space-4 --fd-space-5
Sizes --fd-size-xs --fd-size-sm --fd-size-md --fd-size-lg
--fd-control-height-sm --fd-control-height-md --fd-control-height-lg
--fd-control-max-width-sm --fd-control-min-width-sm
--fd-listbox-max-height --fd-chevron-size
--fd-overlay-min-width-sm --fd-overlay-max-width-sm
--fd-overlay-max-width-md --fd-overlay-offset-top
--fd-avatar-size-sm --fd-avatar-size-md --fd-avatar-size-lg
--fd-switch-width --fd-switch-padding --fd-tooltip-arrow-size
--fd-sidebar-width --fd-toast-width --fd-form-column-min-width
--fd-media-columns --fd-media-gap --fd-media-min-width
--fd-media-aspect-ratio --fd-media-fit --fd-link-underline-offset
--fd-spinner-thickness
Focus ring --fd-ring-color --fd-ring-width --fd-ring-offset
--fd-ring-field --fd-ring-control --fd-ring-halo-width --fd-ring-halo-blur
--fd-ring-halo-opacity
Surfaces --fd-surface-tint --fd-shadow-sm --fd-shadow-color-floating
--fd-shadow-color-modal --fd-shadow-color-subtle
--fd-hover-brightness --fd-disabled-opacity --fd-pulse-opacity
--fd-backdrop --fd-backdrop-opacity --fd-z-overlay
--fd-z-overlay-contained
Type --fd-font-sans --fd-font-mono --fd-font-heading
--fd-font-size-xs --fd-font-size-sm --fd-font-size-md
--fd-font-size-lg --fd-font-size-xl --fd-font-size-2xl
--fd-font-size-3xl --fd-line-height-normal --fd-line-height-tight
--fd-font-weight-medium --fd-font-weight-semibold
--fd-font-weight-bold --fd-heading-weight
--fd-heading-letter-spacing
Motion --fd-duration-fast --fd-duration-normal --fd-duration-slow
--fd-duration-spin --fd-duration-slowest
--fd-loading-overlay-delay
--fd-ease-standard --fd-ease-emphasized --fd-ease-in-out
Deprecated component hooks
--fd-button-radius, --fd-button-padding-x, --fd-button-border-width, --fd-card-bg, --fd-card-height and --fd-icon-size still work but will be removed. Use a token set on the element, or its base part: .round::part(base) { border-radius: 999px }.
--fd-success-foreground, --fd-warning-foreground and --fd-danger-foreground are the text color on a solid fill of that status. Badges are soft tints now, so no fandry component draws one; they are there for your own CSS (see Upgrading below) and for components that extend fandry/base.
Upgrading from 0.25
Link to section Upgrading from 0.25No token was removed or renamed, so a themed site keeps working. These changed how components look; each comes with the CSS that keeps the old look.
Default colors:
--fd-primaryis cobalt and--fd-accentmint. A site that sets its own sees no change.Info alerts and toasts read
--fd-info, and--fd-accentwhen only that is set, so a site that colored them through accent keeps its color. Set--fd-infoto color them apart from accent.Colored badges are soft tints of their color instead of solid fills, and every badge has a border. They still follow
--fd-success,--fd-warning,--fd-dangerand--fd-primary, and dark themes through--fd-bgand--fd-text; the-foregroundtokens no longer color them.Focus: buttons, links and other controls draw their ring with a soft halo, and still follow
--fd-ring-width. A few keep a flat ring that follows only--fd-ring-colorand--fd-ring-width, not--fd-ring-control: sidebar and breadcrumb items, table sort buttons, and menu items (drawn inside the item). Text fields draw it on their own border, so they no longer follow--fd-ring-width, and the textarea no longer uses--fd-ring-offset.The data table marks the row being edited with
--fd-primaryinstead of--fd-accent.A link draws its focus ring on its
linkpart (the<a>) instead ofbase, so the ring follows a box you give::part(link). A site that drew its own ring onfandry-link::part(base):focus-visiblenow gets two: drop that rule and set--fd-ring-controlor--fd-ring-coloron the link instead.A focused button keeps the shadow it lifts with on hover, under its ring. A filled (default) button also draws a line of its own color, mixed toward its text color, just inside the ring, so the ring stands apart from a fill of the same color.
:root {
--fd-primary: 330 81% 48%;
--fd-accent: 199 89% 36%;
--fd-accent-foreground: 0 0% 100%;
--fd-link-visited: 271 55% 40%;
}
Solid badges (the same for warning, danger and primary)
fandry-badge::part(base success) {
background: hsl(var(--fd-success, 142 71% 30%));
color: hsl(var(--fd-success-foreground, 0 0% 100%));
border-color: transparent;
}
/* No border on any badge */
fandry-badge::part(base) {
border-width: 0;
}
A flat focus ring, as before
:root {
--fd-ring-halo-opacity: 0;
/* your ring color */
--fd-ring-field: 0 0 0 2px hsl(222 89% 56%);
}
A ring token is a box-shadow list
A value for--fd-ring-control or --fd-ring-field has to be one or more box shadows. For an empty one write 0 0 transparent, not none: a button adds its hover shadow after the ring, and none in a list makes the whole declaration invalid, so the button draws neither.Ready to build? Browse every component, its props and examples under Components.