Fandry UI
css

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 works

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

Inside every component (fandry/base/tokens.css)
: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 in hsl() 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 site

Put the tokens on :root in a stylesheet the page loads. Every fandry component on the page inherits them.

LWR / LWC OSS: src/assets/theme.css
: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 :root rule, or a <link> to a static resource holding it.

  • Lightning pages, apps and record pages: set the tokens on :host of your outermost component. Everything it renders inherits them, including fandry components nested in other components.

  • A whole Lightning app from one file: upload the :root rule as a CSS static resource and load it once with loadStyle from lightning/platformResourceLoader.

Salesforce: force-app/main/default/lwc/myApp/myApp.css
: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 section

Set 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 component

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

my/app/app.html
<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 component

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

my/app/app.css
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 a box-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 wins
  • An 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 0ms whatever the site sets. Looping indicators (spinner, skeleton) keep running.

Using tokens in your own CSS

Link to section Using tokens in your own CSS

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

Your CSS
/* 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 token

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

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

No 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-primary is cobalt and --fd-accent mint. A site that sets its own sees no change.

  • Info alerts and toasts read --fd-info, and --fd-accent when only that is set, so a site that colored them through accent keeps its color. Set --fd-info to 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-danger and --fd-primary, and dark themes through --fd-bg and --fd-text; the -foreground tokens 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-color and --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-primary instead of --fd-accent.

  • A link draws its focus ring on its link part (the <a>) instead of base, so the ring follows a box you give ::part(link). A site that drew its own ring on fandry-link::part(base):focus-visible now gets two: drop that rule and set --fd-ring-control or --fd-ring-color on 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.

Keep the old default colors
: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.