Fix CLS Issues on SAP Commerce (Layout Shift) | OpsBlu Docs

Fix CLS Issues on SAP Commerce (Layout Shift)

Stabilize SAP Commerce layouts by reserving Spartacus component containers, sizing media assets, and preloading SmartEdit-safe fonts.

General Guide: See Global CLS Guide for universal concepts and fixes.

SAP Commerce-Specific CLS Causes

1. Spartacus Component Loading

Angular components rendering asynchronously:

// Problem: Async component causes shift
<ng-container *ngIf="product$ | async as product">
  <cx-product-summary [product]="product"></cx-product-summary>
</ng-container>

2. OCC API Data Loading

Late-loading product data causing layout shifts:

Initial render → API response → Content injection → Shift

3. Dynamic SmartEdit Content

CMS content slots loading after page render.

4. Price and Stock Updates

Asynchronous pricing and availability updates.

SAP Commerce-Specific Fixes

Fix 1: Reserve Space for Components

Use skeleton screens in Spartacus:

// product-summary.component.html
<div class="product-summary" [class.loading]="isLoading">
  <ng-container *ngIf="product; else skeleton">
    <h1>{{ product.name }}</h1>
    <cx-product-price [product]="product"></cx-product-price>
  </ng-container>

  <ng-template #skeleton>
    <div class="skeleton-title"></div>
    <div class="skeleton-price"></div>
  </ng-template>
</div>
// product-summary.component.scss
.product-summary {
  min-height: 200px; // Reserve space

  .skeleton-title {
    height: 32px;
    width: 60%;
    background: #f0f0f0;
    margin-bottom: 16px;
    animation: pulse 1.5s infinite;
  }

  .skeleton-price {
    height: 24px;
    width: 30%;
    background: #f0f0f0;
  }
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

Fix 2: Optimize Async Pipe Usage

Pre-fetch data before route activation:

// product.resolver.ts
@Injectable({ providedIn: 'root' })
export class ProductResolver implements Resolve<Product> {
  constructor(private productService: ProductService) {}

  resolve(route: ActivatedRouteSnapshot): Observable<Product> {
    const code = route.params['productCode'];
    return this.productService.get(code).pipe(
      take(1),
      tap(product => {
        // Pre-populate component state
        this.productService.setCurrentProduct(product);
      })
    );
  }
}

Fix 3: Stabilize CMS Slots

Reserve space for SmartEdit content:

// Layout styles
.content-slot {
  &[data-slot="Section1"] {
    min-height: 400px; // Hero banner area
  }

  &[data-slot="Section2A"] {
    min-height: 300px; // Product carousel
  }

  &[data-slot="BottomContent"] {
    min-height: 200px; // Footer content
  }
}

Fix 4: Handle Price Updates Gracefully

Prevent layout shift during price updates:

// price.component.ts
@Component({
  selector: 'cx-product-price',
  template: `
    <div class="price-container" [style.min-width.px]="minWidth">
      <span *ngIf="price$ | async as price; else loading">
        {{ price.formattedValue }}
      </span>
      <ng-template #loading>
        <span class="price-loading">---</span>
      </ng-template>
    </div>
  `,
  styles: [`
    .price-container {
      display: inline-block;
      min-width: 80px;
      text-align: right;
    }
    .price-loading {
      color: transparent;
      background: #f0f0f0;
    }
  `]
})
export class PriceComponent {
  @Input() product: Product;
  minWidth = 80; // Reserve consistent width
}

Fix 5: Stabilize Product Grid

Set fixed dimensions for product tiles:

// product-list.component.scss
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 24px;
}

.product-tile {
  aspect-ratio: 3/4;
  display: flex;
  flex-direction: column;

  .product-image {
    flex: 1;
    aspect-ratio: 1/1;

    img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
  }

  .product-info {
    padding: 16px;
    min-height: 120px;
  }
}

Fix 6: Handle Font Loading

Prevent FOUT (Flash of Unstyled Text):

// styles.scss
@font-face {
  font-family: 'SAP-Icons';
  src: url('/assets/fonts/SAP-Icons.woff2') format('woff2');
  font-display: block; // Or 'optional' for less critical fonts
}

// Fallback with similar metrics
body {
  font-family: 'SAP 72', -apple-system, BlinkMacSystemFont, sans-serif;
}

Monitoring CLS

// Track CLS in Spartacus
import { onCLS } from 'web-vitals';
import { isPlatformBrowser } from '@angular/common';

@Injectable({ providedIn: 'root' })
export class WebVitalsService {
  constructor(@Inject(PLATFORM_ID) private platformId: Object) {
    if (isPlatformBrowser(this.platformId)) {
      this.trackCLS();
    }
  }

  private trackCLS(): void {
    onCLS(metric => {
      window.dataLayer?.push({
        event: 'web_vitals',
        vital_name: 'CLS',
        vital_value: metric.value,
        vital_rating: metric.rating
      });
    });
  }
}

CLS Targets

Rating CLS Value
Good ≤ 0.1
Needs Improvement 0.1 - 0.25
Poor > 0.25

Next Steps