---
title: >-
  Storefront: Practical Lab – Campaign-Based Product Styling | Shopware
  Community Hub
description: >-
  Implement campaign-based storefront styling in a theme plugin using a product
  custom field as a toggle and theme configuration for flexible SCSS variables.
canonical_url: >-
  https://hub.shopware.com/learn/unit/storefront-practical-lab-campaign-based-product-styling
---

# Storefront: Practical Lab – Campaign-Based Product Styling

<LearningObjectives>

- Implement campaign-based storefront styling using a product custom field.
- Extend and override storefront templates in a theme plugin (listing and product detail page).
- Use theme configuration values as SCSS variables for flexible campaign styling.
- Scope SCSS customizations with wrapper classes to avoid side effects.
- Structure campaign customization in a maintainable and reversible way.

</LearningObjectives>

# Storefront: Practical Lab – Campaign-Based Product Styling

In this practical lab, you will combine several important storefront concepts you have learned so far. You will work with:

- **Theme configuration** (`theme.json`) and SCSS variables.
- **Upgrade-safe Twig overrides** with `sw_extends` and targeted block overrides.
- **Scoped styling** using wrapper classes to avoid global side effects.
- **Product custom fields** to control storefront behavior conditionally.

The goal is to implement campaign-based styling (in this case **Black Friday**) that only affects products that are explicitly marked via a **product custom field**.

The lab is intentionally built as a code-first customization. Instead of relying on manual CMS configuration, you create version-controlled template overrides and dedicated styling hooks in your theme plugin. This makes the implementation reproducible across environments and less dependent on someone keeping a CMS element configuration unchanged in the administration.

The mental model is: Use the administration for campaign data and configuration values, but keep the storefront structure and styling hooks in code.

<Callout title="Note" type="info">

In this practical lab, demo data is generated via the command `APP_ENV=prod bin/console framework:demodata`.

Be aware that it does not represent a real production setup and may differ from your local shop instance.

</Callout>

## Target Result

By the end of this practical lab, you will have implemented the following:

- A theme plugin that applies **Black Friday campaign styling** to the **product card** in the listing and the **buy widget** on the product detail page (PDP).
- A **product custom field** `academy_product_black_friday` (boolean) that toggles the campaign per product.
- **Scoped markup and SCSS hooks**:
  - `.academy-product-box` for listing product cards.
  - `.academy-product-detail-buy` for the PDP buy box area.
- Theme-configurable colors that are used as SCSS variables.

The storefront will change from this:

![Normal Sale Product Card in Listing](assets/images/current-result-listing.jpg)

![Normal Sale PDP Buy Widget](assets/images/current-result-pdp.jpg)

**To this:**

![Black Friday product Card in Listing](assets/images/target-result-listing.jpg)

![Black Friday PDP Buy Widget](assets/images/target-result-pdp.jpg)

## Folder Structure

After completing the implementation, your theme plugin should follow this folder structure:

```text
[shop_root]
 └─ custom/plugins
    └─ [your_theme]
        ├─ src
        │  ├─ Lifecycle
        │  │  └─ CustomFieldsLifecycle.php
        │  ├─ Resources
        │  │  ├─ theme.json
        │  │  ├─ app
        │  │  │  └─ storefront
        │  │  │     └─ src
        │  │  │        └─ scss
        │  │  │           ├─ overrides.scss
        │  │  │           ├─ base.scss
        │  │  │           └─ campaigns
        │  │  │              └─ _black-friday.scss
        │  │  │─ snippet
        │  │  │  ├─ storefront.de.json
        │  │  │  └─ storefront.en.json
        │  │  └─ views/storefront
        │  │     ├─ block/cms-block-gallery-buybox.html.twig
        │  │     └─ component
        │  │        ├─ buy-widget/buy-widget-price.html.twig
        │  │        └─ product/card
        │  │           ├─ box-standard.html.twig
        │  │           ├─ badges.html.twig
        │  │           └─ price-unit.html.twig
        │  └─ FrontendDevIntermediateCampaignStylingTheme.php
        └─ composer.json
```

In the following steps, you will build this structure incrementally and verify each part.

## Step 1: Create a Theme and Add Theme Configuration

Create a new theme:

```bash
bin/console theme:create FrontendDevIntermediateCampaignStylingTheme
```

Open the `theme.json` file and add the following configuration inside the `config` section:

```json
{
  "name": "YourThemeName",
  "author": "Author Name",
  "views": [
    "@Storefront",
    "@Plugins",
    "@FrontendDevIntermediateCampaignStylingTheme"
  ],
  "style": [
    "app/storefront/src/scss/overrides.scss",
    "@Storefront",
    "app/storefront/src/scss/base.scss"
  ],
  "script": [
    "@Storefront"
  ],
  "asset": [
    "@Storefront"
  ],

  // Add the following configuration
  "config": {
    "tabs": {
      "colours": {
        "label": {
          "en-GB": "Black Friday",
          "de-DE": "Black Friday"
        }
      }
    },
    "blocks": {
      "blackFriday": {
        "label": {
          "en-GB": "Black Friday",
          "de-DE": "Black Friday"
        }
      }
    },
    "sections": {
      "blackFridayColors": {
        "label": {
          "en-GB": "Black Friday Colors",
          "de-DE": "Black Friday Farben"
        }
      }
    },
    "fields": {
      "academy-black-friday-background-color": {
        "label": {
          "en-GB": "Background Color",
          "de-DE": "Hintergrundfarbe"
        },
        "type": "color",
        "value": "#000000",
        "editable": true,
        "tab": "colours",
        "block": "blackFriday",
        "section": "blackFridayColors"
      },
      "academy-black-friday-text-color": {
        "label": {
          "en-GB": "Text Color",
          "de-DE": "Textfarbe"
        },
        "type": "color",
        "value": "#000000",
        "editable": true,
        "tab": "colours",
        "block": "blackFriday",
        "section": "blackFridayColors"
      },
      "academy-black-friday-text-color-inverse": {
        "label": {
          "en-GB": "Inverse Text Color",
          "de-DE": "Inverse Textfarbe"
        },
        "type": "color",
        "value": "#FFFFFF",
        "editable": true,
        "tab": "colours",
        "block": "blackFriday",
        "section": "blackFridayColors"
      }
    }
  }
}
```

This configuration adds:

- A new tab called **Black Friday**
- One block
- One section
- Three color fields: "Background Color," "Text Color" and "Inverse Text Color."

These fields allow shop owners to adjust campaign colors without changing the source code.

These color fields are automatically exposed as SCSS variables:

- `$academy-black-friday-background-color`
- `$academy-black-friday-text-color`
- `$academy-black-friday-text-color-inverse`

You will use these variables inside your SCSS to style campaign elements.

<Callout title="Important" type="info">

Use a project-specific prefix for custom theme configuration fields, such as `academy-...` in this lab. This reduces the risk of accidentally colliding with Bootstrap variables like `$primary` or Shopware variables like `$sw-*`.

</Callout>

### Step 1.1.: Add a Product Custom Field to Toggle the Campaign Behavior

Before implementing the storefront customization, you need a way to toggle campaign styling per product. For this, create a boolean custom field.

The toggle is the connection between product data and storefront styling. Without a product-level flag, the theme would not know which products should receive the campaign design and which products should keep the default styling.

In many real projects, this flag is not maintained manually by a shop manager. It is often controlled by the product management system, ERP, PIM, or an import process that sends product data to Shopware. For this lab, you set the flag manually in the administration so you can see the full flow from product data to storefront output.

Create the `CustomFieldsLifecycle.php` file inside a new `Lifecycle` folder:

```text
[shop_root]
 └─ custom
    └─ plugins
       └─ [your_theme]
           └─ src
              │─ Lifecycle // Create this folder
              │  └─ CustomFieldsLifecycle.php // Create this file
              └─ [YourTheme].php
```

And add the following implementation:

```php
<?php declare(strict_types=1);

namespace FrontendDevIntermediateCampaignStylingTheme\Lifecycle;

use Shopware\Core\Content\Product\ProductDefinition;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\CustomField\CustomFieldTypes;

class CustomFieldsLifecycle
{
    private const string CUSTOM_FIELD_SET_NAME = 'Academy Custom Field Set';
    private const string CUSTOM_FIELD_NAME = 'academy_product_black_friday';

    public function __construct(
        private readonly EntityRepository $customFieldSetRepository
    ) {
    }

    public function install(Context $context): void
    {
        $this->customFieldSetRepository->upsert([
            [
                'id' => Uuid::fromStringToHex(self::CUSTOM_FIELD_SET_NAME),
                'name' => 'Academy_Product',
                'global' => true,
                'position' => 0,
                'config' => [
                    'label' => [
                        'en-GB' => self::CUSTOM_FIELD_SET_NAME,
                        'de-DE' => self::CUSTOM_FIELD_SET_NAME,
                    ],
                ],
                'relations' => [
                    [
                        'id' => Uuid::fromStringToHex(self::CUSTOM_FIELD_SET_NAME . '_product_relation'),
                        'entityName' => ProductDefinition::ENTITY_NAME,
                    ],
                ],
                'customFields' => [
                    [
                        'id' => Uuid::fromStringToHex(self::CUSTOM_FIELD_NAME),
                        'name' => self::CUSTOM_FIELD_NAME,
                        'type' => CustomFieldTypes::BOOL,
                        'config' => [
                            'componentName' => 'mt-switch',
                            'type' => 'checkbox',
                            'label' => [
                                Defaults::LANGUAGE_SYSTEM => 'Black Friday Product',
                                'en-GB' => 'Black Friday Product',
                                'de-DE' => 'Black Friday Product',
                            ],
                        ]
                    ],
                ],
            ],
        ], $context);
    }

    public function uninstall(Context $context): void
    {
        $this->customFieldSetRepository->delete([
            [
                'id' => Uuid::fromStringToHex(self::CUSTOM_FIELD_SET_NAME)
            ]
        ], $context);
    }
}
```

**Explanation**

- The `CustomFieldsLifecycle` class creates the custom field structure that the lab needs.
- `CUSTOM_FIELD_SET_NAME` defines the label of the custom field set in the administration.
- `CUSTOM_FIELD_NAME` defines the technical name of the field: `academy_product_black_friday`.
- `global` is set to `true` because this custom field set is created by the extension and should not be freely edited or deleted from the custom field settings in the administration.
- The `relations` section connects this custom field set to the product entity through `ProductDefinition::ENTITY_NAME`.
- Because of this relation, products can receive the custom field.
- The field itself is created in `customFields`.
- Its type is `CustomFieldTypes::BOOL`, so the value is a boolean: `true` or `false`.
- The `componentName` is `mt-switch`, so the field is displayed as a switch in the product detail page in the administration.
- The result is that every product can carry the flag `academy_product_black_friday`.
- Later in Twig, this flag tells the storefront whether a product belongs to the campaign.

This does not lock the value on the product. The custom field definition is managed by the extension, while the value on each product can still be set in the product detail page or by an ERP, PIM, or import process.

The `upsert()` call creates the custom field set if it does not exist yet, or updates the provided data if it already exists.

<Callout title="Important" type="warning">

In real projects, custom field values may already be used by products, customers, orders, ERP systems, PIM systems, or imports. Before deleting or recreating custom fields, always clarify with the project team what should happen to the existing values. Do not blindly delete custom field definitions, relations, or existing values!

For more details about repository writes and plugin lifecycle cleanup, see [Writing Data With the Data Abstraction Layer](/learn/unit/writing-data-with-the-dal) and [Plugin Lifecycle Management](/learn/unit/plugin-lifecycle-management) in the Backend Development Intermediate learning path.

</Callout>

---

And in your main plugin class:

```php
<?php declare(strict_types=1);

namespace FrontendDevIntermediateCampaignStylingTheme;

use FrontendDevIntermediateCampaignStylingTheme\Lifecycle\CustomFieldsLifecycle;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\Plugin;
use Shopware\Core\Framework\Plugin\Context\InstallContext;
use Shopware\Core\Framework\Plugin\Context\UninstallContext;
use Shopware\Storefront\Framework\ThemeInterface;

class FrontendDevIntermediateCampaignStylingTheme extends Plugin implements ThemeInterface
{
    public function install(InstallContext $installContext): void
    {
        parent::install($installContext);

        $this->installCustomFields($installContext->getContext());
    }

    public function uninstall(UninstallContext $uninstallContext): void
    {
        parent::uninstall($uninstallContext);

        if ($uninstallContext->keepUserData()) {
            return;
        }

        $this->uninstallCustomFields($uninstallContext->getContext());
    }

    private function installCustomFields(Context $context): void
    {
        $customFieldsLifecycle = new CustomFieldsLifecycle(
            $this->container->get('custom_field_set.repository')
        );

        $customFieldsLifecycle->install($context);
    }

    private function uninstallCustomFields(Context $context): void
    {
        $customFieldsLifecycle = new CustomFieldsLifecycle(
            $this->container->get('custom_field_set.repository')
        );

        $customFieldsLifecycle->uninstall($context);
    }
}
```

**Explanation**

- This plugin class connects the custom field lifecycle to the Shopware plugin lifecycle.
- When the plugin is installed, Shopware calls the `install()` method.
- Inside `install()`, the plugin calls `installCustomFields()`.
- `installCustomFields()` creates a `CustomFieldsLifecycle` instance and passes the `custom_field_set.repository` to it.
- The repository is needed because the custom field set is stored through Shopware's Data Abstraction Layer.
- Then `installCustomFields()` calls `$customFieldsLifecycle->install($context)`, which creates or updates the custom field set.
- When the plugin is uninstalled, Shopware calls the `uninstall()` method.
- The `keepUserData()` check decides whether plugin-related data should remain in the system.
- If user data should be kept, the method returns early and the custom field set is not deleted.
- If user data should not be kept, `uninstallCustomFields()` removes the custom field set again.

This PHP code is part of the implementation, not just setup code. It makes this practical lab reproducible: The required product flag exists as soon as the plugin is installed, and the storefront customization can rely on it.

<Callout title="Important Note" type="warning">

This cleanup behavior is useful for this lab because it keeps the example reversible.

In real projects, be careful when deleting custom fields that are part of business processes. A campaign flag may be filled by an ERP, PIM, or import process and can influence pricing, promotions, visibility, or other revenue-relevant behavior. Deleting the field can remove important product data and cause unexpected business impact.

</Callout>

### Step 1.2.: Verify in the Administration

Now install and activate your theme:

```bash
bin/console plugin:install FrontendDevIntermediateCampaignStylingTheme --activate --clearCache
```

Then in the administration, go to Content -> Themes -> [YourTheme]. You should see the new tab and the new block.

![Theme Config in the Administration](assets/images/theme-config-in-the-administration.jpg)

And also the new custom field set in the administration's product detail page.

![Custom Field Set in the Administration](assets/images/admin-pdp-specification-custom-fields.jpg)

Now select any product and enable the custom field and set a list price.

![Set List Price in the Administration PDP](assets/images/admin-pdp-general-list-price.jpg)

Finally, everything is ready to start with the customization in the storefront.

## Step 2: Customize Styling in the Listing

Generally, campaign colors should be configurable through theme settings, not hardcoded.

In the listing, the following will be modified:

![Targets to Customize](assets/images/listing-card-targets-for-change.jpg)

- The discount badge
- The price
- The list price
- The buy button

### Step 2.1.: Inspect the Product Card

Start by inspecting the HTML of the product card in the browser.

![Inspect HTML](assets/images/browser-listing-inspect-product-card.jpg)

The `product-box` class is the main wrapper for the product card. You will introduce an additional wrapper element around this element. This ensures to scope the campaign styles safely and avoid side effects on other product cards.

The wrapper with the custom class is used as a clear identifier in the DOM. It marks the product card as a campaign card and gives the SCSS a stable, version-controlled styling hook. Keep this wrapper lightweight and avoid layout-changing styles on the wrapper itself, so the original `.product-box` structure remains stable.

### Step 2.2.: Mirror Templates From the Core-Storefront

In the `vendor` directory, under `storefront/Resources/views`, search for `card product-box` (using your IDE).

In the screenshots below, we use PhpStorm, where you can use "Copy the path from source root".

And in your theme, create under `src` a new file:

```text
Resources/views/storefront/component/product/card/box-standard.html.twig
```

This will create the template we want to customize with the correct path.

![Search Template](assets/images/phpstorm-vendor-product-card.jpg)

![Copy Path](assets/images/phpstorm-vendor-product-card-copy-path.jpg)

![Create File With Copied Path](assets/images/phpstorm-plugin-create-new-file.jpg)

![Paste Path](assets/images/phpstorm-plugin-create-new-file-paste-path.jpg)

![Mirroring Done](assets/images/phpstorm-plugin-file-created-mirroring-done.jpg)

Now in your `box-standard.html.twig` template, add the following:

```twig
{% sw_extends '@Storefront/storefront/component/product/card/box-standard.html.twig' %}

{% block component_product_box %}
    {% if product.translated.customFields.academy_product_black_friday is not same as(true) %}
        {{ parent() }}
    {% else %}
        <div class="academy-product-box">
            {{ parent() }}
        </div>
    {% endif %}
{% endblock %}
```

**Explanation:**

- The custom template extends the original template.
- The dedicated block is overridden.
- The `if-else` check is added.
- If the product is not a Black Friday product, render the original template.
- If the product is a Black Friday product, render the template with the additional wrapper (`academy-product-box`).

<Callout title="Note" type="info">

With `product.translated.customFields`, you can access the custom field values in the storefront.

</Callout>

Now you have the clean base to start styling the product card.

### Step 2.3.: Add Campaign SCSS

In this practical lab, the following will be styled:

- The border color of the product card.
- The background color and text color of the buy button.
- The background color and text color of the discount badge with changing the text content.
- The text color of the discounted price.
- The percentage sign in the discounted price.

In this case, the border color, the product price, and the buy button are easy to style. The rest requires some more work.

But first, let's style the border color, the product price and the buy button.

For that, create a new SCSS partial in `src/Resources/app/storefront/src/scss/campaigns/_black-friday.scss`:

In this partial, add the following:

```scss
/* Card */
.academy-product-box {
  .card {
    &.product-box {
      border: 3px solid $academy-black-friday-background-color;
    }

    .product-price {
      color: $academy-black-friday-text-color;
      margin-top: 5px;
      padding-left: 10px;
    }

    .btn-buy {
      background-color: $academy-black-friday-background-color;
      color: $academy-black-friday-text-color-inverse;
    }
  }
}
```

Because we added the `academy-product-box` wrapper, all styles are scoped and only applied to campaign products.

### Step 2.4.: Adjust the Discount Badge

For the discount badge, you need to change the content from `%` to a text. For that, create a new snippet with the following content:

```json
{
  "academyProductCardStylingTheme": {
    "blackFridayBadge": "Black Friday Sale"
  }
}
```

Next, you need the matching template for the override. The principle is the same: Inspect the element in the browser, copy its HTML class, and search for that class in the Shopware storefront templates under `vendor`.

The HTML class is `badge bg-danger badge-discount` and the template is `storefront/component/product/badge/badge-discount.html.twig`.

In your theme, create this file:

```text
[your_theme]/src/Resources/views/storefront/component/product/card/badges.html.twig
```

And add the following override:

```twig
{% sw_extends '@Storefront/storefront/component/product/card/badges.html.twig' %}

{% block component_product_badges_discount %}
    {% if product.translated.customFields.academy_product_black_friday is not same as(true) %}
        {{ parent() }}
    {% else %}
        {# From SW Core #}
        {% set price = product.calculatedPrice %}
        {% if product.calculatedPrices.count > 0 %}
            {% set price = product.calculatedPrices.last %}
        {% endif %}

        {% set listPrice = price.listPrice.percentage > 0 %}
        {% set hasRange = product.calculatedPrices.count > 1 %}

        {% set displayParent = product.variantListingConfig.displayParent and product.parentId === null %}
        {% if displayParent %}
            {% set displayFromVariants = displayParent and price.unitPrice !== product.calculatedCheapestPrice.unitPrice %}
        {% endif %}

        {% if listPrice and not hasRange and not displayFromVariants %}
            {# Add custom class to the badge #}
            <span class="badge badge-black-friday-sale badge-discount">
                <span class="visually-hidden">{{ 'listing.boxLabelDiscount'|trans|sw_sanitize }}</span>

                {# This is adapted #}
                <span aria-hidden="true">{{ 'academyProductCardStylingTheme.blackFridayBadge'|trans|sw_sanitize }}</span>
            </span>
        {% endif %}
    {% endif %}
{% endblock %}
```

**Explanation:**

Most of the template stays the same as the original. The only differences are:

1. The `product.translated.customFields.academy_product_black_friday is not same as(true)` check.
2. The `badge-black-friday-sale` class is added to the badge element.
3. The `{{ 'academyProductCardStylingTheme.blackFridayBadge'|trans|sw_sanitize }}` snippet is added to the span element that replaces the `%` sign.

Now that the template is adapted, you can style the custom badge in your `_black-friday.scss` partial:

```scss
/* Card */
.academy-product-box {
  .card {
    &.product-box {
      ...
      .badge-black-friday-sale {
        background-color: $academy-black-friday-background-color;
        color: $academy-black-friday-text-color-inverse;
      }
    }
  }
}
```

### Step 2.5.: Adjust the Price Layout

The last step is the percentage element. The following has to be changed.

- The element receives the Bootstrap class `rounded-cirle`.
- Its position is before the product price instead of after the product price.
- The element uses the campaign colors.

Here you use the same principle again: Inspect the element, find the matching core template, and mirror the template in your theme for the override.

In this case the HTML class is `list-price-percentage` and the template is `storefront/component/product/card/price-unit.html.twig`.

In your theme, create this file:

```text
[your_theme]/src/Resources/views/storefront/component/product/card/price-unit.html.twig
```

And add the following content:

```twig
{% sw_extends '@Storefront/storefront/component/product/card/price-unit.html.twig' %}

{% block component_product_box_main_price %}
    {% if product.translated.customFields.academy_product_black_friday is not same as(true) %}
        {{ parent() }}
    {% else %}
        <div class="container">
            <div class="row">
                {# Discount Circle #}
                <div class="col-3 rounded-circle list-price-percentage">
                    {{ price.listPrice.percentage|round(0, 'floor') }}%
                </div>
                <div class="col-9 product-price{% if isListPrice and not displayFrom and not displayFromVariants %} with-list-price{% endif %}">
                    {{ price.unitPrice|currency }}

                    {# From SW-Core #}
                    {% if isListPrice and not displayFrom and not displayFromVariants %}
                        {% set afterListPriceSnippetExists = 'listing.afterListPrice'|trans|length > 0 %}
                        {% set beforeListPriceSnippetExists = 'listing.beforeListPrice'|trans|length > 0 %}
                        {% set hideStrikeTrough = beforeListPriceSnippetExists or afterListPriceSnippetExists %}

                        <span class="list-price{% if hideStrikeTrough %} list-price-no-line-through{% endif %}">

                            {% if beforeListPriceSnippetExists %}{{ 'listing.beforeListPrice'|trans|trim|sw_sanitize }}{% endif %}

                            <span class="visually-hidden list-price-label">{{ 'listing.regularPriceLabel'|trans|sw_sanitize }}</span>

                            <span class="list-price-price">{{ price.listPrice.price|currency }}</span>

                            {% if afterListPriceSnippetExists %}{{ 'listing.afterListPrice'|trans|trim|sw_sanitize }}{% endif %}
                        </span>
                    {% endif %}
                </div>
            </div>
        </div>
    {% endif %}
{% endblock %}
```

**Explanation:**

Most of the template stays the same as the original. The only differences are:

- Extending the original template.
- Add the `if-else` check.
- Add a grid system (bootstrap) to the price element to reposition the percentage element.
- Exchange the position of the percentage element and the product price element.
- Add the `rounded-circle` class to the percentage element.

<Callout title="Note" type="info">

Because you extend the original template block, the variables defined in the core template (e.g., `price`, `isListPrice`) remain available in your override.

</Callout>

And the following SCSS to the `black-friday.scss` partial:

```scss
/* Card */
.academy-product-box {
  .card {
    &.product-box {
      ...
      .rounded-circle {
        background-color: $academy-black-friday-background-color;
        color: $academy-black-friday-text-color-inverse;
        font-size: 14px;
        width: 30px;
        height: 30px;
        display: flex;
        justify-content: center;
        align-items: center;
      }
    }
  }
}
```

## Step 3: Verify the Listing Card

Now import the `_black-friday.scss` partial to the `base.scss` file (otherwise, the styles will not be applied):

```scss
@import "campaigns/black-friday";
```

Assign your theme to your storefront sales channel.

```bash
bin/console theme:change
```

Follow the interactive steps of the command to select your theme and the correct sales channel. After the interactive steps, the theme will be compiled automatically; you don't need to run `bin/console theme:compile`.

Then clear the cache:

```bash
bin/console cache:clear
```

Open your storefront listing. You should now see the Black Friday styling only for products where the custom field is enabled.

## Step 4: Style the Buy Widget on the Product Detail Page (PDP)

In real-world projects, you often need to style not only the product card in the listing, but also other elements, such as the buy widget on the product detail page.

But the principle of the implementation remains the same:

- Inspect in your browser the element you want to style.
- Copy the relevant HTML class.
- Search for the HTML class in the Shopware storefront templates under `vendor`.
- Mirror the template inside your theme.
- Override only the necessary block.
- Customize the template in a maintainable way, for example by scoping styles with a wrapper class.

In this practical lab, we will style the buy widget in the product detail page.

### Step 4.1.: Add a Campaign Wrapper to the Buy Box Block

On the PDP, add a wrapper class to scope the campaign styling.

In this case, the template is `storefront/block/cms-block-gallery-buybox.html.twig`. Create this file:

```text
[your_theme]/src/Resources/views/storefront/block/cms-block-gallery-buybox.html.twig
```

Then wrap the buy box column with `.academy-product-detail-buy` only for campaign products.

```twig
{% sw_extends '@Storefront/storefront/block/cms-block-gallery-buybox.html.twig' %}

{% block block_gallery_buybox_column_right %}
    {% if page.product.translated.customFields.academy_product_black_friday is not same as(true) %}
        {{ parent() }}
    {% else %}
        {% set element = block.slots.getSlot('right') %}

        <div class="academy-product-detail-buy col-lg-5 product-detail-buy" data-cms-element-id="{{ element.id }}">
            {% block block_gallery_buybox_column_right_inner %}
                {% sw_include '@Storefront/storefront/element/cms-element-' ~ element.type ~ '.html.twig' ignore missing %}
            {% endblock %}
        </div>
    {% endif %}
{% endblock %}
```

Again, most of the template stays the same as the original. The differences are the `if-else` check and the wrapper with the custom class `academy-product-detail-buy`.

### Step 4.2.: Adapt the Buy Widget Price Structure

Next, the structure of the buy widget price should slightly be adapted:

- The discounted percentage should be displayed before the list price and not after it.
- The discounted percentage should be visually highlighted.
- The default badge should not be displayed.

To do so, you need to override the following template: `storefront/component/buy-widget/buy-widget-price.html.twig`.

Create this file:

```text
[your_theme]/src/Resources/views/storefront/component/buy-widget/buy-widget-price.html.twig
```

And add the following code:

```twig
{% sw_extends '@Storefront/storefront/component/buy-widget/buy-widget-price.html.twig' %}

{% block buy_widget_price_content %}
    {% if page.product.translated.customFields.academy_product_black_friday is not same as(true) %}
        {{ parent() }}
    {% else %}
        {# From SW Core, but adapted where necessary #}
        {% set listPrice = price.listPrice %}
        {% set isListPrice = price.listPrice.percentage > 0 %}
        {% set isRegulationPrice = price.regulationPrice != null %}

        <div class="container">
            <div class="row">
                <div class="col-12 p-0 product-detail-price{% if isListPrice %} with-list-price{% endif %}{% if isRegulationPrice %} with-regulation-price{% endif %}">
                    {{ price.unitPrice|currency }}
                </div>

                {% if isListPrice %}
                    {% block buy_widget_was_price %}
                        {% block buy_widget_was_price_badge %}
                            {% if product.translated.customFields.academy_product_black_friday is not same as(true) %}
                                {{ parent() }}
                            {% endif %}
                        {% endblock %}

                        {% set afterListPriceSnippetExists = 'listing.afterListPrice'|trans|length > 0 %}
                        {% set beforeListPriceSnippetExists = 'listing.beforeListPrice'|trans|length > 0 %}

                        {% block buy_widget_was_price_wrapper %}
                            {% if product.translated.customFields.academy_product_black_friday is not same as(true) %}
                                {{ parent() }}
                            {% else %}
                                {# Adapted #}
                                <div class="col-12 p-0 product-detail-list-price-wrapper">
                                    <span class="list-price-percentage fw-bold">-{{ listPrice.percentage|round(0, 'floor') }}%</span>

                                    {% if beforeListPriceSnippetExists %}{{ 'listing.beforeListPrice'|trans|trim }}{% endif %}

                                    <span{% if not (afterListPriceSnippetExists or beforeListPriceSnippetExists) %} class="list-price-price"{% endif %}>{{ listPrice.price|currency }}</span>

                                    {% if afterListPriceSnippetExists %}
                                        {{ 'listing.afterListPrice'|trans|trim }}
                                    {% endif %}
                                </div>
                            {% endif %}
                        {% endblock %}
                    {% endblock %}
                {% endif %}
                {% if isRegulationPrice %}
                    <span class="product-detail-list-price-wrapper">
                        <span class="regulation-price">
                            {{ 'general.listPricePreviously'|trans({'%price%': price.regulationPrice.price|currency }) }}
                        </span>
                    </span>
                {% endif %}
            </div>
        </div>
    {% endif %}
{% endblock %}
```

**Explanation:**

Most of the template stays the same as the original. The differences are:

- The `if-else` check.
- The standard "badge" is not displayed for products where the custom field is not enabled (see `{% block buy_widget_was_price_badge %}`).
- The block `{% block buy_widget_was_price_wrapper %}` is slightly adapted (first the adapted discounted percentage, then the list price).
- Bootstrap's grid system is used to reposition the discounted percentage.

### Step 4.3.: Style the Discounted Price

Now add the PDP styling to your `black-friday.scss` partial:

```scss
/* PDP */
.academy-product-detail-buy {
  .product-detail-price {
    &.with-list-price {
      color: $academy-black-friday-text-color;
    }
  }

  .list-price-percentage {
    background-color: $academy-black-friday-background-color;
    color: $academy-black-friday-text-color-inverse;
    border-radius: 7px;
    padding: 1px 5px;
  }

  .btn-buy {
    background-color: $academy-black-friday-background-color;
    color: $academy-black-friday-text-color-inverse;
  }
}
```

## Step 5: Verify the PDP

Now recompile the theme and verify the PDP in the storefront:

```bash
bin/console theme:compile && bin/console cache:clear
```

---

Now go back to the administration and deactivate the custom field toggle and check the PDP and the listing.

Refresh the storefront pages. The entire customization should now fully revert without any missing styling, broken layout, or template issues.

Only when this step is successfully completed, your theme is ready for production.

## Double-Check With the Plugin

If you want to compare your implementation with the reference solution, you can use our Academy [repository](https://github.com/ShopwareAcademy/FrontendDevIntermediateCampaignStylingTheme/tree/frontend-dev-intermediate-campaign-style).

Or clone it locally:

```bash
git clone git@github.com:ShopwareAcademy/FrontendDevIntermediateCampaignStylingTheme.git
```

And then checkout the related Git tag:

```bash
cd FrontendDevIntermediateCampaignStylingTheme
git checkout tags/frontend-dev-intermediate-campaign-style
```

## Troubleshooting

Always keep in mind that the following points are important to consider when implementing your theme:

- Ensure you clean the cache after each change.
- Ensure your theme is assigned to the correct sales channel.
- Ensure your theme is compiled after you made changes in the SCSS files.
- Ensure your theme is not missing any template or SCSS files.
- Use project-specific prefixes for your own SCSS variables and theme configuration fields, so you do not accidentally override Bootstrap or Shopware core variables.

<Callout title="Note" type="info">

In this practical lab, we kept most of the original templates.

Keep an eye on your customization when you perform or plan to perform a Shopware upgrade.

</Callout>

## Summary

Congratulations!

In this practical lab you implemented campaign-based storefront styling in an update-safe and maintainable way. You learned how to:

- Use a product custom field (bool) to toggle campaign behavior per product.
- Use theme configuration to define campaign colors and expose them as SCSS variables.
- Override selected storefront templates with `sw_extends` while keeping core logic intact.
- Guard your overrides with strict conditions to ensure clean fallback behavior.
- Apply scoped SCSS styling through wrapper classes so that only campaign products are affected.

With this knowledge, you can implement seasonal campaigns such as Black Friday, Christmas, Summer Sale, or other campaigns in your storefront.

This practical lab is the implementation part of the campaign customization. In the next lab, you will add a small Playwright test suite to verify that the campaign elements are still rendered in the listing and on the PDP.
