Articles
Insights·

Tailwind CSS i Lightning Web Components.

Tailwind er et kjært verktøy i webutviklingsverdenen, intuitivt, raskt og med et enormt samfunn. Men i Salesforce LWC? Ikke et alternativ. Vi ønsket å endre på det.

Tailwind CSS i Lightning Web Components

Hvis du noen gang har prøvd å bruke Tailwind CSS i en Lightning Web Component, kjenner du smerten. Du skriver class="px-4 py-2 bg-blue-500", deployer, og... ingenting. Ingen stiler. Komponenten rendres som om det er 1996.

Problemet er Shadow DOM. Hver LWC lever innenfor sin egen shadow-grense, og vanlig CSS kan rett og slett ikke krysse den. Din vakre Tailwind-stylesheet eksisterer like gjerne ikke.

Dette er historien om hvordan vi bygde lwc-tailwind, en Salesforce CLI-plugin som får Tailwind til å fungere inne i Shadow DOM, og alle omveiene vi tok underveis.

Problemet: Shadow DOM Er En Mur

Lightning Web Components bruker Shadow DOM for innkapsling. Det er flott for å hindre stilkollisjoner mellom komponenter, men det betyr også at:

  • Globale stylesheets ikke kan nå inn i komponenter
  • Du ikke bare kan <link>e en Tailwind-build og kalle det en dag
  • Hver komponent er en stilmessig øy

Det finnes ett smutthull: CSS custom properties (variabler som --my-color: blue) krysser shadow-grenser. De arves nedover DOM-treet, rett gjennom shadow roots. Vanlige selektorer gjør det ikke, men variabler gjør det.

Det smutthullet er det som gjør hele dette prosjektet mulig.

Vei 1: Generer Skript og La Brukere Kjøre Dem

Den første versjonen var ikke en plugin i det hele tatt. Det var et CLI-verktøy (lwc-tailwind) som genererte skript inn i brukerens prosjekt. Tenk på det som et stillasverktøy, du kjørte lwc-tailwind init og det ville skrive:

  • Et 632-linjes dev.mjs watcher-skript
  • En flerfilskatalog css-splitter/ med parser-, ekstraktor- og writer-moduler
  • En TailwindElement-basisklasse
  • Konfigurasjonsfiler

Idéen var: vi gir deg skriptene, du eier dem, du kjører npm run dev.

Hva gikk galt:

Denne tilnærmingen hadde et grunnleggende problem, skriptene var generert kode som brukere forventet å vedlikeholde. Alle 632 linjene av dev.mjs. All CSS-splitter-logikken. Hvis vi fikset en bug eller la til en funksjon, måtte brukere manuelt oppdatere sine genererte skript eller kjøre init på nytt og miste tilpasningene sine.

Det betydde også at CSS-splitting-logikken ble duplisert på tvers av hvert prosjekt. Og create-kommandoen forsøkte å stille opp et helt Salesforce-prosjekt fra bunnen av, noe som var å finne opp hjulet på nytt da sf project generate allerede gjør dette.

Tilnærmingen fungerte, men den var skjør. Vi trengte at logikken skulle leve ett sted.

Vei 2: Konverter til en Salesforce CLI Plugin

Det andre store steget var å konvertere alt til en ordentlig sf CLI-plugin ved bruk av oclif og TypeScript. I stedet for å generere skript, er pluginen verktøyet. CSS-splitting-logikken bor i pluginen, ikke i prosjektet ditt.

bash

  • sf tailwind init # Sett opp prosjektet ditt
  • sf tailwind build # Kompiler og splitt CSS
  • sf tailwind watch # Watch-modus med automatisk rebuilding
  • sf tailwind component # Lag en ny komponent

Dette var en enorm opprydding — hele cli/-katalogen med genererte skript ble slettet. Den 632-linjes dev.mjs-malen forsvant. Den flerfilede CSS-splitteren ble en enkelt css-builder.ts-tjeneste.

create-kommandoen (som stilte opp hele prosjekter) ble droppet. init-kommandoen ble beholdt men forenklet, den legger kun til Tailwind i et eksisterende Salesforce-prosjekt, som er det folk faktisk trenger.

Hva vi vant: En kodebase, én installasjon, automatiske oppdateringer via sf plugins install. Ingen generert kode for brukere å vedlikeholde.

Vei 3: Splitte Basisvariabler inn i en Statisk Ressurs

Her ble arkitekturen interessant. De tidlige versjonene kompilerte Tailwind og dumpet alt inn i per-komponent CSS-filer. Hver komponent fikk hele settet med --tw-* variabeldefinisjoner pluss sine nytteregler. Det er 100+ linjer med variabeldeklarasjoner gjentatt i hver eneste komponent.

Innsikten var å splitte den kompilerte CSS-en i to lag:

  1. Basisvariabler (--tw-* custom properties) → en enkelt statisk ressurs, lastet én gang per komponent via loadStyle()
  2. Nytteregler (.px-4, .bg-brand, osv.) → per-komponent CSS-filer, som kun inneholder det hver komponent faktisk bruker

Dette fungerer på grunn av det Shadow DOM-smutthullet: CSS custom properties krysser shadow-grenser. Basisvariablene definerer ting som --tw-ring-offset-shadow og --tw-translate-x som Tailwinds nytteklasser refererer til internt. Disse variablene må være tilgjengelige overalt, så de havner i en delt statisk ressurs.

Nytteklassene selv (.px-4 { padding: 1rem }) er scoped per komponent. Hver LWC får kun reglene den trenger. En knappkomponent med class="px-4 py-2 rounded-md" får nøyaktig de tre reglene i sin CSS-fil — ikke hele Tailwind-outputen.

Deteksjonslogikken er enkel men nøyaktig. Parseren går gjennom PostCSS AST-en og klassifiserer hver regel:

  • Ingen klasseselektorer + alle deklarasjoner starter med --tw-? → Basisvariabel. Går til statisk ressurs.
  • Har en klasseselektor? → Nytteregel. Indekseres etter klassenavn for per-komponent-matching.

Splitteren: Hvordan Per-Komponent CSS Faktisk Fungerer

Dette er kjernen i pluginen, og det er verdt å forklare hvordan en Tailwind-klasse går fra HTML-en din til riktig komponents CSS-fil.

Steg 1: Kompiler. PostCSS prosesserer tailwind.css ved hjelp av standard Tailwind-pipeline. Output: én stor CSS-fil med alt.

Steg 2: Parse. Pluginen går gjennom PostCSS AST-en og skiller basisvariabler fra nytteregler. Nytteregler indekseres i et kart: klassenavn → CSS-regeltekst (med media query-innpakninger bevart).

Steg 3: Ekstraher. For hver LWC-komponent scanner pluginen:

  • .html-filer for class="..."-attributter (regex-ekstraksjon)
  • .js-filer for streng-literaler som ser ut som Tailwind-klassenavn

JS-skanningen er smartere enn det høres ut, den filtrerer ut ikke-Tailwind-strenger ved å avvise camelCase-identifikatorer, URL-er og andre mønstre som tydelig ikke er CSS-klasser.

Steg 4: Match og skriv. De ekstraherte klassenavnene slås opp i regelkartet. Matchede regler skrives til komponentens .css-fil under en markeringskommentar:

css

/* Hand-written CSS above this line is preserved */

/* === GENERATED BY TAILWIND SPLITTER === */
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }

Alt over markøren bevares ved hver rebuild. Alt under regenereres. Dette betyr at utviklere kan skrive egendefinert CSS ved siden av Tailwind uten å bekymre seg for at den blir overskrevet.

SLDS-Broen: Få Salesforce-Tokens til å Fungere Med Tailwind

Salesforce har sine egne design-tokens, CSS custom properties som --lwc-colorBrand som plattformen setter globalt. Disse penetrerer Shadow DOM (de er CSS-variabler tross alt) og definerer org-ens temafarger.

Den genererte tailwind.config.js mapper disse tokenene til Tailwind-fargeverktøy:

javascript

colors: {
'brand': 'var(--lwc-colorBrand, #0176d3)',
'text-default': 'var(--lwc-colorTextDefault, #181818)',
'error': 'var(--lwc-colorTextError, #ea001e)',
}

Skriv class="bg-brand" og du får org-ens merkefarger, uansett hva administratoren har konfigurert. Reservehex-verdiene sikrer at komponenter fungerer i kontekster der SLDS-tokens ikke er tilgjengelige (som community-nettsteder eller testing).

Konfigurasjonen deaktiverer også Tailwinds preflight (CSS-reset), fordi SLDS allerede gir en basislinje. Å kjøre begge ville forårsake spesifisitetskonflikter.

TailwindElement-Basisklassen

Komponenter utvider TailwindElement i stedet for LightningElement:

javascript

import TailwindElement from 'c/tailwindElement'; export default class MyButton extends TailwindElement { // Det er alt. Tailwind bare fungerer. }

Under panseret bruker TailwindElement loadStyle() fra lightning/platformResourceLoader for å injisere den statiske ressurs-CSS-en inn i shadow root. Den gjør dette én gang per komponentinstans (beskyttet av et flagg), og gir livssykluskroker (onInit, onFirstRender, onRender, onDisconnect) slik at underklasser kan utvide oppførselen uten å bryte CSS-injeksjonen.

Det er også et statisk useSplitCss-flagg — hvis en komponent kun bruker per-komponent CSS og ikke trenger basisvariablene, kan den velge bort loadStyle() fullstendig.

Vei 4: Monorepo-Støtte

Den siste store funksjonen var støtte for Salesforce-monorepoer, prosjekter med flere pakkekatalogger i sfdx-project.json.

Den opprinnelige pluginen antok én pakkekatalog, én lwc/-mappe. Virkelige Salesforce-prosjekter har ofte flere:

json

{
"packageDirectories": [
{ "path": "force-app", "default": true },
{ "path": "shared-components" },
{ "path": "feature-x" }
]
}

Den refaktorerte pluginen:

  • Leser alle pakkekatalogger fra sfdx-project.json ved hjelp av SfProject API-en
  • Samler LWC-kataloger fra alle pakker
  • Splitter CSS på tvers av alle fra én enkelt kompilering
  • Støtter et --directory-flagg for å målrette spesifikke pakker eller komponenter

En komponentfiltertjeneste håndterer ergonomien — du kan sende en full path, en relativ path, en pakkekatalog eller bare et komponentnavn, og den finner ut hva du mener.

Watch-kommandoen fikk også auto-deploy: etter hver rebuild kan den pushe den statiske ressursen direkte til org-en din ved hjelp av @salesforce/source-deploy-retrieve. Ikke mer bytte av terminaler.

Små Fikser Som Betydde Noe

Noen endringer var små, men løste reelle problemer:

Globale keyframes. @keyframes-regler er iboende globale, de definerer animasjoner som enhver klasse kan referere til. Splitteren behandlet dem opprinnelig som nytteregler og forsøkte å tildele dem til komponenter. Nå havner de i den statiske ressursen ved siden av basisvariablene. Uten dette fungerte rett og slett ikke animasjoner.

Det foreldede build-problemet. Fordi sf CLI kjører kompilert JavaScript fra lib/, gir redigering av TypeScript-kilde og umiddelbar testing deg den gamle oppførselen. Dette feilet oss konstant. Løsningen var disiplin: alltid npm run build før sf-kommandoer. CLAUDE.md dokumenterer dette som det viktigste fallgruvepunktet.

Debouncet watch-rebuilds. Fil-watcheren må håndtere raske lagringer uten å utløse fem rebuilds på rad. Et 300ms debounce-vindu batcher endringer. Den sporer også hvilke filer splitteren skrev, slik at sin egen output ikke utløser en ny rebuild, noe som forhindrer uendelige løkker.

Hva Vi Endte Opp Med

Den endelige arkitekturen er en tre-lags tilnærming:

Lag 1: Statisk Ressurs (delt)

└─ CSS custom properties (--tw-*) + @keyframes
└─ Lastet én gang per komponent via loadStyle()
└─ Krysser shadow-grenser

Lag 2: Per-Komponent CSS (scoped)

└─ Kun Tailwind-nyttereglene hver komponent bruker
└─ Autolastet av LWC-rammeverket fra .css-filen
└─ Scoped til shadow root

Lag 3: SLDS Design-Tokens (plattform)

└─ Mappet til Tailwind-farger i konfig
└─ Satt av Salesforce, arvet gjennom Shadow DOM
└─ Muliggjør org-bevisst temaing

Én sf tailwind build-kommando kompilerer Tailwind, parser AST-en, splitter outputen og skriver alt til riktige steder. Én sf tailwind watch-kommando holder alt synkronisert mens du utvikler.

Lærdommer

Ikke generer kode brukere må vedlikeholde. Skript-genererings-tilnærmingen virket fleksibel men skapte en vedlikeholdsbyrde. Å flytte logikk inn i en plugin var utelukkende bedre.

Forstå plattformens begrensninger før du designer. Shadow DOMs variabelsmutthull er hele grunnlaget. Uten å forstå at CSS custom properties krysser shadow-grenser mens selektorer ikke gjør det, ville du aldri kommet frem til denne arkitekturen.

PostCSS AST-parsing slår regex. CSS-splitteren bruker PostCSS sin innebygde parser for å klassifisere regler. Å forsøke å regex-matche CSS-deklarasjoner ville vært skjørt og feilutsatt. AST-tilnærmingen håndterer media queries, nøstede regler og kanttilfeller på en ryddig måte.

Enkeltformålsverktøy komponerer godt. Pluginen har en tydelig pipeline: kompiler → parse → splitt → skriv. Hvert steg er en funksjon, hver funksjon gjør én ting. Dette gjorde monorepo-støtte mulig uten å skrive om kjernen, vi matet bare inn flere kataloger.

Den enkleste arkitekturen som fungerer er den riktige. Tre CSS-lag, en markørbasert filkonvensjon og en basisklasse. Ingen build-time komponenttransformasjoner, ingen Babel-plugins, ingen egendefinerte webpack-loadere. Bare PostCSS som gjør det PostCSS gjør, og en smart splitter på toppen.

Want to stay updated?.

Get in touch to learn more about how we can help your business with Salesforce.

Contact us
Tailwind CSS i Salesforce LWC | b64