sfcc-sfra-scss — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited sfcc-sfra-scss (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Purpose: Provide a concise, implementation‑ready reference for styling and theming Salesforce B2C Commerce SFRA storefronts using SCSS. Optimized for automated reasoning, safe overrides, upgrade resilience, and performant output.
| Principle | Rule | Why It Exists | Action Pattern |
|---|---|---|---|
| Cartridge Overlay | Never edit app_storefront_base | Preserve upgrade path | Create mirror path in custom cartridge + import base first |
| Import First, Then Override | Always @import '~base/...'; at top | Keeps base layout + utilities | File header snippet template |
| Single Source of Truth | Centralize tokens in abstracts/_variables.scss | Global theming + diffable changes | Import base → redefine only changed vars |
| Shallow Specificity | Max selector depth 2–3 | Predictable cascade | Use BEM instead of nested HTML chains |
| Mobile‑First | Base = smallest viewport | Less override churn | Add enhancements with min-width media mixin |
| Immutable Generated CSS | Never hand edit /static/default/css | Prevent drift | Recompile via build scripts only |
| Deterministic Build | Same inputs ⇒ same CSS | Debug + CI parity | Lock dependency versions |
Red flag triggers for re‑evaluation: deep nesting (>3), repeated hardcoded colors, !important, duplicated breakpoint literals, editing compiled CSS, or missing initial @import.
AI Guardrail (fast reject): If asked to "change base SCSS" directly, respond with an override strategy instead of editing base source.
product/detail.css) & selector(s).app_storefront_base/cartridge/client/default/scss/<path>.scss.cartridge/client/default/scss/.@import '~base/<path>'; (must be line 1; no preceding BOM/comment).npm run compile:scss (or project alias) → deploy → verify cascade diff._variables.scss before component override.Decision Tree:
| Goal | Location to Change First | Fallback If Not Available |
|---|---|---|
| Color / Font / Spacing shift | _variables.scss | Component partial override |
| Layout structure | ISML + classes | New component partial |
| Responsive tweak | Mixin breakpoints | Local media query (still via mixin) |
| Reusable visual pattern | New mixin | Placeholder + @extend |
| Plugin override | vendors/ override partial | Scoped component patch |
Plugin Extension Nuances (Observed from plugin_wishlists):
global.scss (@import "~base/global";) then only plugin-specific partials.variables inside multiple plugin partials unless you need local compilation context—prefer central import in the entry file to avoid duplication.$spacer inside a feature file) unless intentionally creating a component-scoped token; document with a comment // local token.components/_animations.scss) instead of inline per component.@import "~base/global"; rather than directly importing bootstrap again to reduce risk of version drift.scss/
abstracts/ (_variables.scss, _mixins.scss, _functions.scss)
base/ (_reset.scss, _typography.scss, _base.scss)
layout/ (_header.scss, _footer.scss, _navigation.scss, _grid.scss)
components/ (_buttons.scss, _product-tile.scss, _carousel.scss, ...)
pages/ (_home.scss, _checkout.scss, _pdp.scss, ...)
vendors/ (_bootstrap-overrides.scss, _plugin-X.scss)
global.scss (imports only – no rules)Import Order (global.scss):
SFRA Reality Snapshot: The core global.scss in app_storefront_base imports: variables, skin/skinVariables, bootstrap custom, overrides, utilities, icon libraries, then individual component partials. AI agents should treat this as the authoritative ordering when composing custom global.scss overlays: always import base global first in plugin/global contexts, then plugin additions.
Rule: No CSS selectors in global.scss; only @import.
| Aspect | Rule | Example |
|---|---|---|
| Block | Semantic, domain concept | .product-tile |
| Element | __ double underscore | .product-tile__price |
| Modifier | -- double hyphen | .product-tile--featured |
| State (JS) | is- prefix (transient) | .is-loading |
| Utility | u- + purpose | .u-hidden |
Guidelines:
.mini-cart__count not header .mini-cart span.count..product-tile .tile-body .price .tiered .value). When extending these, do NOT replicate entire nesting; extract just the block root with a modifier class you control (e.g. .product-tile--compact)..wishlistTile), retain original naming but annotate exception with justification comment to aid future refactor and silence lint selectively—in code: /* stylelint-disable-line */.Override pattern (abstracts/_variables.scss):
@import '~base/variables'; // must stay first
// Theme – Colors
$primary: #0B5FFF; // Brand blue
$secondary: #00A676;
$body-bg: #F8F9FA;
$body-color: #212529;
// Typography
$font-family-sans-serif: 'Inter', 'Helvetica Neue', Arial, sans-serif;
$font-family-serif: 'Merriweather', Georgia, serif;
// Radii / Spacing
$border-radius: 2px;
$btn-padding-y: 0.5rem;
$btn-padding-x: 1.125rem;Rules:
$color-*, $space-*, $z-*).$spacer for a one-off layout tweak), prefix with component context when possible ($wishlist-spacer) to avoid collision with global tokens.| Need | Use | Why | Avoid If |
|---|---|---|---|
| Static property bundle reused widely | %placeholder + @extend | Single output selector group | Needs argumentization |
| Dynamic pattern w/ params | @mixin | Flexible + readable | Output duplication causes bloat |
| Global theming value | $variable | Single update point | Represents multi-rule semantic block |
| Conditional computed value | @function | Returns scalar for calc chains | Can be plain variable |
Responsive Mixin:
$breakpoints: (
sm: 576px,
md: 768px,
lg: 992px,
xl: 1200px
) !default;
@mixin mq($bp) {
@if map-has-key($breakpoints, $bp) {
@media (min-width: map-get($breakpoints, $bp)) { @content; }
} @else { @warn 'Unknown breakpoint: #{$bp}'; }
}Usage:
.product-tile { width: 100%; @include mq(md) { width: 50%; } }Bootstrap Breakpoints Interop: SFRA base uses Bootstrap's media-breakpoint-* mixins. When generating code referencing base breakpoints, prefer those existing mixins (@include media-breakpoint-down(md) { ... }) if the base already depends on Bootstrap, instead of introducing a parallel custom map to avoid divergence. Use the custom $breakpoints + mq() pattern only if project intentionally decouples from Bootstrap.
File: product/detail.scss
@import '~base/product/detail'; // inherit base
.product-detail__name {
font-family: $font-family-serif;
font-size: 2.4rem;
color: $primary;
}Compiled file resolved via assets.addCss('/css/product/detail.css') with cartridge path precedence—no ISML changes needed. AI Validation Step: After generation, assert snippet begins with base import and contains only new selectors or modified declarations—no duplication of entire base blocks.
Buttons (components/_buttons.scss):
@import '~base/components/buttons';
.btn--pill { border-radius: 999rem; }
.btn--outline-secondary {
color: $secondary; background: transparent; border: 1px solid $secondary;
&:hover { background: $secondary; color: #fff; }
}Product Tile (components/_product-tile.scss):
@import '~base/components/product-tile';
.product-tile {
border: 1px solid transparent; transition: box-shadow .2s;
&:hover { box-shadow: 0 4px 12px rgba(0,0,0,.1); border-color: $gray-300; }
&__price--sale { color: $danger; font-weight: 600; }
}
Refactor Tip: Instead of deeply nested overrides inside `.product-tile .tile-body .price .tiered .value`, target a purposeful modifier class (`.product-tile__price--tiered-value`) you add in ISML for long-term maintainability.Checkout (pages/_checkout.scss):
@import '~base/pages/checkout';
.checkout-nav .nav-link { background:$gray-200; color:$gray-600;
&.active { background:$primary; color:#fff; }
}| Rule | Implementation |
|---|---|
| Mobile‑first | Base styles = small viewport |
| Progressive enhancement | @include mq(md){...} not max-width overrides |
| Centralized breakpoints | $breakpoints map only |
| Avoid pixel drift | Use tokens, not ad‑hoc numbers |
Anti‑Pattern: Desktop CSS first + mobile overrides via @media (max-width) – leads to override churn. Plugin Reality: Wishlist plugin currently mixes media-breakpoint-up and media-breakpoint-down usage; maintain consistency—choose mobile-first pattern (prefer -up for enhancements) and refactor legacy -down only when safe.
| Concern | Checklist |
|---|---|
| Color Contrast | Ensure WCAG AA (> 4.5:1 body, 3:1 large text). Token review before merge. |
| Focus States | Visible focus ring; never remove outline without replacement. |
| Reduced Motion | Gate large transitions with @media (prefers-reduced-motion: reduce) |
| Hidden Content | Use utility .u-visually-hidden for screen-reader only text; avoid display:none for needed ARIA live content. |
| Touch Targets | Minimum 44px height for interactive elements (buttons, filters). |
Utility Example:
.u-visually-hidden { position:absolute!important; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0; }Animation Respect: Provide a reduced-motion alternative when introducing keyframe animations (e.g. fading alerts). Centralize keyframes and wrap any large movement inside @media (prefers-reduced-motion: no-preference); fallback: immediate end state.
| Optimization | Action | Tooling |
|---|---|---|
| Critical CSS | Generate subset for key templates → inline via store-skin asset | npm i critical |
| Prune Unused CSS | Integrate PurgeCSS scanning ISML & JS | PurgeCSS plugin |
| Minification | Ensure prod build via sgmf-scripts produces minified bundle | CI check size |
| Source Maps (dev only) | Enable for DX; disable prod | Sass compiler flag |
| Font Strategy | Use WOFF2 + subsetting + font-display: swap | Font tooling |
Suggested CI Budget: Global CSS ≤ 250KB uncompressed (enterprise baseline) – fail build if regression > +15% week‑over‑week. Duplicate Import Scan: Add a CI grep step to detect repeated @import "~base/variables"; inside plugin component partials—emit warning to consolidate.
Install:
npm i -D stylelint stylelint-config-standard-scss stylelint-declaration-strict-valueConfig (.stylelintrc.json):
{
"extends": "stylelint-config-standard-scss",
"plugins": ["stylelint-declaration-strict-value"],
"rules": {
"selector-class-pattern": ["^([a-z][a-z0-9]*)(__(?:[a-z0-9]+))?(--[a-z0-9-]+)?$", {"message": "Use BEM: block__element--modifier"}],
"max-nesting-depth": 2,
"declaration-no-important": true,
"scale-unlimited/declaration-strict-value": [["color", "background-color", "border-color", "font-size"], {"ignoreValues": ["inherit", "transparent", "currentColor"]}]
}
}Script:
"scripts": { "lint:scss": "stylelint 'cartridges/**/*.scss' --cache" }Optional: pre‑commit hook with Husky + lint-staged.
AI Generation Rule Set: When asked to produce SCSS:
abstracts/_variables.scss).!important with a cascade adjustment strategy comment.| Smell | Why Dangerous | Fix |
|---|---|---|
| Editing compiled CSS | Lost on next build | Modify SCSS source |
| Missing base import | Breaks structural inheritance | Add @import '~base/...'; line 1 |
| Deep selector chains | Specificity lock-in | Convert to BEM blocks/elements |
| Repeated hex codes | Inconsistent theming | Promote to variable |
!important usage | Overrides cascade discipline | Reduce specificity / re-order imports |
| Inline media queries w/ literals | Hard to refactor | Use mq() mixin |
| Component partial modifies unrelated block | Hidden coupling | Create new partial / utility |
| Re-importing base variables in every plugin partial | Build bloat / risk inconsistent overrides | Import once at entry (e.g. plugin global.scss) |
| Inline duplicated keyframes | Larger bundle & maintenance overhead | Centralize in shared components/_animations.scss |
| Symptom | Likely Cause | Diagnostic | Resolution |
|---|---|---|---|
| Style not applied | Not in compiled CSS | Search compiled file for selector | Check import order / rebuild |
| Base style lost | Missing base import | Inspect diff of partial | Add import + revert overrides |
| Wrong theme color persisting | Variable shadowed locally | Grep for variable redefinition | Centralize in _variables.scss |
| Breakpoint shift inconsistent | Hardcoded px values | grep -R "992px" | Replace with token map |
| Layout jumps FOUC | Critical CSS missing | Lighthouse trace | Implement store-skin inline CSS |
| Repeated variable override ignored | Later import overrides earlier | Trace import graph order | Consolidate variable overrides at earliest import point |
| Intent | Prompt Template |
|---|---|
| Create component override | "Generate SFRA SCSS override for <component> (base path <path>). Include base import and BEM modifiers for states: loading, error." |
| Introduce new theme color | "Add semantic token for highlight color with accessible contrast against $body-bg and update button hover style." |
| Refactor deep selectors | "Refactor selector <selector> into BEM with max depth 2; preserve semantics." |
| Add responsive variant | "Extend .product-tile to show 2 columns at md, 4 at lg via existing breakpoint mixin." |
| Performance audit | "List top 10 heaviest SCSS partials by compiled size estimate and suggest consolidation." |
| Plugin extension | "Generate wishlist plugin SCSS override importing '~base/global' then add toast animation using existing base token palette; avoid duplicating keyframes if fade already exists." |
[] All overrides start with base import [] No direct edits to app_storefront_base [] Variables overridden centrally only [] Lint passes (no warnings in CI) [] No !important (or justified + documented) [] CSS bundle size budget respected [] Critical CSS updated for homepage + PDP [] Source maps disabled for production [] New utilities documented (inline comment header) [] No duplicate keyframe blocks with identical declarations [] Plugin global includes base global exactly once
# Compile SCSS
npm run compile:scss
# Lint SCSS
npm run lint:scss
# Find hardcoded colors (non-variable)
// in app_custom/cartridge/client/default/scss/abstracts/_variables.scss
# Estimate partial usage (appearance in compiled)
@import '~base/variables';CI Grep Helpers (Optional):
grep -R "@import \"~base/variables\"" cartridges/plugin_* | sort
grep -R "@keyframes fade" cartridges/ | wc -l| Concern | Guidance |
|---|---|
| Multi-brand scaling | Abstract brand diffs to dedicated _brand-<id>.scss imported conditionally via build flag |
| A/B testing CSS | Isolate experiment layer in components/experiments/ with clear removal date comment |
| Plugin overrides | Keep each plugin override in vendors/_<plugin>-overrides.scss with upstream version tag |
| Future SFRA upgrade | Diff only variable + override partials; never fork base files wholesale |
Header Annotation Block (optional for generated partial):
// PURPOSE: Override of base product tile for brand visual refinement
// DEPENDS: ~base/components/product-tile
// SAFE REMOVE: Yes (reverts to base visual style)Classification Legend:
| Scenario | Better Option |
|---|---|
| Structural HTML limitation | Adjust ISML markup first |
| Repeated spacing hacks | Introduce spacing scale utility classes |
| Overriding grid internals | Use flexbox utilities / custom wrapper |
| One-off promotional layout | Page partial (pages/_promo-<slug>.scss) + sunset note |
| Behavior actually JS-driven (e.g. show/hide) | Adjust JS or data attributes; keep CSS purely presentational |
Provide along with any generated SCSS so humans can validate rapidly:
If any answer violates a stated rule, propose an auto-correction diff, not just a warning.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.