> For the complete documentation index, see [llms.txt](https://docs.rafflex.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.rafflex.io/advanced/twig-templating/custom-twig-slider.md).

# Custom Twig Slider

Build a completely custom hero or slider section with the Sliders block's "Custom Twig (Advanced)" style, using your featured competitions, your own text and image, and live countdowns.

The **Sliders** block normally uses one of ten built-in designs. Choose **Custom Twig (Advanced)** instead and you can write the whole section yourself: a single big hero, a carousel, a grid of featured prizes, or anything else you can build with HTML and CSS.

Your template receives every competition you have switched **Add To Slider** on for, plus four text fields and an optional image that you fill in on the block.

{% hint style="info" %}
**Is this for you?** This option is for people comfortable writing HTML and CSS. If that is not you, the built-in slider styles look great out of the box. You can also copy this page into an AI assistant (such as ChatGPT or Claude), describe the slider you want, and ask it to write the template for you.
{% endhint %}

***

### What you get

When you choose **Custom Twig (Advanced)** as the slider style:

* A **Featured Image** upload appears, for an optional banner or background image of your own.
* A **Content** section appears with four text fields: **Title**, **Value**, **Subtitle** and **Subvalue**.
* A **Custom Twig Code** panel appears with an **HTML + Twig Code** box for your template.
* Your template receives a `products` list of your slider competitions (see [Available variables](#available-variables)), plus your site's colours and branding through `settings`.
* The live countdown, progress bar and quick add components are available (see [Dynamic components](#dynamic-components)), and so is the Splide carousel library (see [Making a carousel with Splide](#making-a-carousel-with-splide)).

Unlike the [Custom Twig Product Grid](/advanced/twig-templating/custom-twig-product-grid.md), where you write one card and the block repeats it, here you write the **whole section**. You loop through the competitions yourself and decide how many to show and how.

***

### Set up a custom slider

{% stepper %}
{% step %}

#### Open the page

In the **admin panel**, look in the left hand menu under **Content** and click **Pages**. Click **Edit** next to the page you want. This is usually **Home**, but the Sliders block works on any page.
{% endstep %}

{% step %}

#### Add or find a Sliders block

Scroll to the bottom of the blocks and click **Add to blocks**, then choose **Sliders**. If your page already has one, you can use that instead.
{% endstep %}

{% step %}

#### Choose Custom Twig (Advanced)

Open the **Slider Style** dropdown and choose **Custom Twig (Advanced)**. A short description appears underneath: "Build your own slider with full HTML/Twig control."
{% endstep %}

{% step %}

#### Add an image (optional)

If your design uses a banner or background image, upload it in **Featured Image**. It must be a JPG or PNG of up to 800KB. In your template it is available as `featured_image_url`.
{% endstep %}

{% step %}

#### Fill in the Content fields (optional)

Type any text you want to use into **Title**, **Value**, **Subtitle** and **Subvalue**. They appear in your template as `title`, `value`, `subtitle` and `subvalue`. They only show where your template uses them, so you can leave them blank if you do not need them.
{% endstep %}

{% step %}

#### Open the Custom Twig Code panel

The **Custom Twig Code** panel starts closed. Click it (it says "Click to expand and edit your custom template") to open it. Inside is the **HTML + Twig Code** box and a **Help** list of the most used variables, with a **View Full Documentation** link back to these docs.
{% endstep %}

{% step %}

#### Add your code

Paste your template into **HTML + Twig Code**. There is only one box here, so put your CSS inside a `<style>` tag at the top of the template. If you are just starting, copy [Example 1](#example-1-single-hero) below to see it working.
{% endstep %}

{% step %}

#### Save and check

Click **Save** at the top right of the page. Then open the page on your live site in a new tab and refresh it.
{% endstep %}
{% endstepper %}

{% hint style="warning" %}
**Check your slider while logged in as an admin.** If your template has a mistake, admins see a red **Template Error** box explaining what went wrong. Customers do not see that message, so to them the section simply looks broken. Always check the live page after saving and fix any error straight away.
{% endhint %}

{% hint style="info" %}
The **Help** list in the panel mentions `site_name` and `currency_icon`. In your template, write these as `settings.site_name` and `settings.currency_icon`, as described in [Settings Variables](/advanced/twig-templating/settings-variables.md).
{% endhint %}

***

### Which competitions are included

Your template's `products` list contains every competition that:

1. has **Add To Slider** switched on, and
2. is live and publicly listed. That means it is **Active**, has reached its **Start Date (Scheduler)** if it has one, has not ended, has not sold out, and does not have **Hidden / VIP Competition** switched on.

The list is always in your **Manual Sort Order** (the order you set in **Competitions** using **Enable reordering**). That is why the **Collection Order** dropdown, which you see with the built-in styles, does not appear for **Custom Twig (Advanced)**. If you want a different order or only some of the competitions, you can do that in your template (see [Common patterns](#common-patterns)).

{% hint style="info" %}
For more on how competitions get into the slider and why one might drop out, see [How the Sliders Block Chooses Competitions](/customise-your-site/set-up-your-homepage/how-the-slider-chooses-competitions.md). Competitions in the slider can take a minute or two to appear or update after you change them.
{% endhint %}

{% hint style="warning" %}
**The slider image is not filled in automatically.** If a competition has no **Slider Image**, `product.slider_image_url` is empty. Use the featured image as a fallback so nothing goes blank: `{{ product.slider_image_url|default(product.image_url) }}`.
{% endhint %}

***

### Available variables

#### Block content

These come from the fields on the block itself.

| Variable             | Type | What it holds                                           | Example               |
| -------------------- | ---- | ------------------------------------------------------- | --------------------- |
| `title`              | text | The **Title** field                                     | "Win Big"             |
| `value`              | text | The **Value** field                                     | "£50,000"             |
| `subtitle`           | text | The **Subtitle** field                                  | "This Week's Prize"   |
| `subvalue`           | text | The **Subvalue** field                                  | "Cash Alternative"    |
| `featured_image_url` | text | The address of the **Featured Image**, or empty if none | Full address or empty |

{% hint style="info" %}
The **Featured Image** belongs to the page rather than the block. If you put two Sliders blocks that use it on the same page, they share the same image.
{% endhint %}

#### Products

Your slider competitions, as a list. Loop through them like this:

```twig
{% for product in products %}
  <h3>{{ product.title }}</h3>
{% endfor %}
```

Or pick just the first one for a single hero:

```twig
{% set hero = products|first %}
<h1>{{ hero.title }}</h1>
```

Each `product` has the following details.

**The basics**

| Variable           | Type   | What it holds                                    | Example                                           |
| ------------------ | ------ | ------------------------------------------------ | ------------------------------------------------- |
| `product.id`       | number | The competition's ID                             | 123                                               |
| `product.title`    | text   | The competition's **Title**                      | "Win a BMW M3"                                    |
| `product.subtitle` | text   | The competition's **Subtitle**, or empty if none | "Limited Edition"                                 |
| `product.slug`     | text   | The last part of the competition's web address   | "win-a-bmw-m3"                                    |
| `product.url`      | text   | The full web address of the competition page     | "<https://yoursite.com/competition/win-a-bmw-m3>" |

**Images and video**

| Variable                          | Type | What it holds                                                             | Example                                |
| --------------------------------- | ---- | ------------------------------------------------------------------------- | -------------------------------------- |
| `product.slider_image_url`        | text | The **Slider Image** address, or empty if none was uploaded               | Full address or empty                  |
| `product.image_url`               | text | The **Featured Image** address                                            | Full address                           |
| `product.portrait_image_url`      | text | The **Portrait Image** address, or empty if none. Handy for phone layouts | Full address or empty                  |
| `product.gallery`                 | list | The addresses of the **Gallery** images, in order                         | \["https\://...", "https\://..."]      |
| `product.featured_video_url`      | text | The **Featured Video** (short looping clip) address, or empty if none     | Full address or empty                  |
| `product.video_embed_url`         | text | The player address for the competition's **Video URL**, or empty if none  | "<https://player.vimeo.com/video/>..." |
| `product.video_embed_type`        | text | "iframe" for YouTube or Vimeo, "video" for a direct video file, or empty  | "iframe"                               |
| `product.video_embed_mime_type`   | text | The file type for a direct video file, otherwise empty                    | "video/mp4"                            |
| `product.video_embed_orientation` | text | "portrait" for YouTube Shorts, otherwise "landscape", or empty            | "landscape"                            |

**Price**

| Variable                     | Type   | What it holds                                                                                                                                | Example |
| ---------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `product.price`              | text   | The current ticket price with the currency symbol. Uses the sale price when there is one                                                     | "£1.99" |
| `product.price_raw`          | number | The current ticket price as a plain number                                                                                                   | 1.99    |
| `product.price_display`      | text   | A friendlier price: "Free", pence for prices under £1 (such as "50p"), otherwise the same as `price`. Add `trim` to remove any leading space | "50p"   |
| `product.original_price`     | text   | The normal (non sale) price with the currency symbol. It is filled in even when there is no sale, so only show it when `is_on_sale` is true  | "£2.99" |
| `product.original_price_raw` | number | The normal (non sale) price as a number. Filled in even when there is no sale                                                                | 2.99    |
| `product.is_on_sale`         | yes/no | Whether a sale price is set                                                                                                                  | true    |
| `product.is_free`            | yes/no | Whether the competition is free to enter                                                                                                     | false   |

**Dates and countdowns**

| Variable                     | Type   | What it holds                                                       | Example                      |
| ---------------------------- | ------ | ------------------------------------------------------------------- | ---------------------------- |
| `product.end_date`           | text   | When the competition closes, written for people                     | "Today 7:00pm"               |
| `product.end_date_raw`       | text   | The closing date and time                                           | "2026-01-05 19:00:00"        |
| `product.end_date_seconds`   | number | Seconds until the competition closes. Use this for countdowns       | 3600                         |
| `product.draw_date`          | text   | The draw date written for people, or empty if none                  | "Draw On Mon Jan 5th 7:00pm" |
| `product.draw_date_raw`      | text   | The draw date and time                                              | "2026-01-05 19:00:00"        |
| `product.draw_date_seconds`  | number | Seconds until the draw                                              | 86400                        |
| `product.start_date`         | text   | The **Start Date (Scheduler)** written for people, or empty if none | "Today 9:00am"               |
| `product.start_date_raw`     | text   | The start date and time, or empty if none                           | "2026-01-01 09:00:00"        |
| `product.start_date_seconds` | number | Seconds until the competition opens (always 0 here)                 | 0                            |
| `product.has_started`        | yes/no | Whether the competition has opened (always true here)               | true                         |

**Tickets and progress**

| Variable                    | Type   | What it holds                                                                                                 | Example |
| --------------------------- | ------ | ------------------------------------------------------------------------------------------------------------- | ------- |
| `product.max_tickets`       | number | The total number of tickets                                                                                   | 5000    |
| `product.sold_tickets`      | number | Tickets sold so far                                                                                           | 3421    |
| `product.tickets_remaining` | number | Tickets still available                                                                                       | 1579    |
| `product.percent_sold`      | number | The percentage sold, from 0 to 100, to one decimal place                                                      | 68.4    |
| `product.max_per_user`      | number | The most tickets one customer can buy (0 means no limit is set)                                               | 50      |
| `product.simple_max_ticket` | yes/no | Whether **Percentage Only** is on, meaning you prefer to show the percentage rather than exact ticket numbers | false   |

**Status**

| Variable                      | Type   | What it holds                                                                                                      | Example                   |
| ----------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------ | ------------------------- |
| `product.has_ended`           | yes/no | Whether the competition has closed or sold out                                                                     | false                     |
| `product.is_sold_out`         | yes/no | Whether the competition is marked as sold out                                                                      | false                     |
| `product.is_active`           | yes/no | Whether the competition's **Active** toggle is on                                                                  | true                      |
| `product.is_featured`         | yes/no | Whether **Add To Slider** is on (always true here)                                                                 | true                      |
| `product.has_question`        | yes/no | Whether the competition has an [entry question](/create-your-first-raffle/create-a-competition/entry-questions.md) | false                     |
| `product.question`            | text   | The entry question, or empty if none                                                                               | "What colour is the sky?" |
| `product.auto_draw`           | yes/no | Whether the competition is drawn automatically                                                                     | true                      |
| `product.leaderboard_enabled` | yes/no | Whether the competition has a [leaderboard](/additional-settings/leaderboards.md)                                  | false                     |
| `product.is_leaderboard_only` | yes/no | Whether it is a leaderboard competition with no main draw (its **Draw Winner Quantity** is 0)                      | false                     |

**Prizes, game and categories**

| Variable                         | Type   | What it holds                                                                                       | Example               |
| -------------------------------- | ------ | --------------------------------------------------------------------------------------------------- | --------------------- |
| `product.cash_alt`               | text   | The **Cash Alternative** with the currency symbol. Shows "£0" when none is set                      | "£50,000"             |
| `product.cash_alt_raw`           | number | The **Cash Alternative** as a number (0 when none is set)                                           | 50000                 |
| `product.winner_qty`             | number | The **Draw Winner Quantity** (main draw winners, not instant winners)                               | 1                     |
| `product.instant_wins`           | number | The number of instant win prizes                                                                    | 5                     |
| `product.instant_wins_remaining` | number | Instant win prizes not yet won                                                                      | 3                     |
| `product.game_type`              | text   | The competition's **Game Type**, such as "standard", "spins" or "scratch"                           | "standard"            |
| `product.category`               | text   | The name of the competition's first category, or empty                                              | "Cars"                |
| `product.categories`             | list   | The names of all its categories                                                                     | \["Cars", "Featured"] |
| `product.description`            | text   | Reserved for a short description. It is currently empty for most competitions, so do not rely on it | Empty                 |

For the full list of game type values, see [Custom Twig Product Grid](/advanced/twig-templating/custom-twig-product-grid.md#prizes-and-game).

{% hint style="warning" %}
`product.cash_alt` is never empty, so a plain check on it is always true. To show a cash alternative only when one is set, check `product.cash_alt_raw > 0` instead.
{% endhint %}

#### Site settings

Your site's colours, logo, name and social links are available through `settings`, for example `{{ settings.primary_color }}` or `{{ settings.site_name }}`. See [Settings Variables](/advanced/twig-templating/settings-variables.md) for the full list.

***

### Dynamic components

These ready made components add live behaviour without writing any JavaScript. They work exactly as described on the [Custom Twig Product Grid](/advanced/twig-templating/custom-twig-product-grid.md#dynamic-components) page.

#### Countdown timer

```twig
<div x-data="raffleCountdown({{ product.end_date_seconds }})">
  <template x-if="!hasEnded">
    <span>
      <span x-text="time.days"></span>d
      <span x-text="pad(time.hours)"></span>h
      <span x-text="pad(time.minutes)"></span>m
      <span x-text="pad(time.seconds)"></span>s
    </span>
  </template>
  <template x-if="hasEnded">
    <span>Closed</span>
  </template>
</div>
```

You can use `time.days`, `time.hours`, `time.minutes`, `time.seconds`, `hasEnded` and `pad(number)` (which adds a leading zero). Pass `draw_date_seconds` instead to count down to the draw.

#### Progress bar

```twig
<div x-data="raffleProgress({{ product.percent_sold }})" class="progress-bar">
  <div class="progress-fill" :style="'width:' + width + '%'"></div>
</div>
```

`percent` is the percentage sold. `width` is the width to draw the bar at (very small amounts show as 3% so the bar is still visible). Style `.progress-bar` and `.progress-fill` with your own CSS.

#### Quick add to basket

```twig
{% if not product.has_question %}
<div x-data="raffleQuickAdd({
    productId: {{ product.id }},
    price: {{ product.price_raw }},
    maxTickets: {{ product.max_per_user > 0 ? product.max_per_user : 100 }},
    hasQuestion: false,
    hasEnded: {{ product.has_ended ? 'true' : 'false' }}
})">
  <button type="button" @click="decrement" :disabled="tickets <= 1">-</button>
  <span x-text="tickets"></span>
  <button type="button" @click="increment" :disabled="tickets >= maxTickets">+</button>
  <button type="button" @click="addToCart" :disabled="!canAdd">
    <span x-show="!loading">Add to basket</span>
    <span x-show="loading">Adding...</span>
  </button>
</div>
{% else %}
  <a href="{{ product.url }}">Answer the question to enter</a>
{% endif %}
```

Unlike the product grid, slides are not wrapped in a link, so you can add normal links and buttons freely. Competitions with an [entry question](/create-your-first-raffle/create-a-competition/entry-questions.md) cannot use Quick Add, so link those to the competition page instead.

***

### Making a carousel with Splide

The Splide carousel library and its basic styles are already loaded on your site, so you can turn a list of slides into a swipeable carousel without adding anything else.

Splide needs this structure: an outer `div` with the class `splide`, then `splide__track`, then a `ul` with `splide__list`, and one `li` with `splide__slide` per slide. Start it with Alpine, using `x-data` and `x-init`:

```twig
<div class="splide" x-data x-init="new Splide($el, {
  type: 'loop',
  perPage: 1,
  autoplay: true,
  interval: 5000,
  pauseOnHover: true,
  arrows: true,
  pagination: true
}).mount()">
  <div class="splide__track">
    <ul class="splide__list">
      {% for product in products %}
        <li class="splide__slide">
          <a href="{{ product.url }}">
            <img src="{{ product.slider_image_url|default(product.image_url) }}" alt="{{ product.title }}" style="width: 100%; display: block;">
          </a>
        </li>
      {% endfor %}
    </ul>
  </div>
</div>
```

{% hint style="warning" %}
Always start Splide with `x-init="new Splide($el, {...}).mount()"` as shown. Do not use the `data-splide` attribute method from Splide's own documentation, because the carousel would not start.
{% endhint %}

**Common Splide options:**

| Option         | What it does                                                    | Example                       |
| -------------- | --------------------------------------------------------------- | ----------------------------- |
| `type`         | How slides move. `'fade'` only works with `perPage: 1`          | `'loop'`, `'slide'`, `'fade'` |
| `perPage`      | How many slides are visible at once                             | `3`                           |
| `gap`          | Space between slides                                            | `'1rem'`, `'20px'`            |
| `autoplay`     | Move to the next slide automatically                            | `true`                        |
| `interval`     | Time between automatic moves, in milliseconds (1000 = 1 second) | `5000`                        |
| `pauseOnHover` | Pause while the mouse is over the slider                        | `true`                        |
| `arrows`       | Show previous and next arrows                                   | `true`                        |
| `pagination`   | Show the row of dots underneath                                 | `true`                        |
| `speed`        | How long a slide change takes, in milliseconds                  | `800`                         |
| `padding`      | Space at the sides, so the next slide peeks in                  | `{ left: 20, right: 20 }`     |
| `breakpoints`  | Different options at smaller screen widths                      | See below                     |

**Showing three slides on desktop and one on phones:**

```twig
<div class="splide" x-data x-init="new Splide($el, {
  type: 'loop',
  perPage: 3,
  gap: '20px',
  breakpoints: {
    1024: { perPage: 2 },
    640: { perPage: 1, gap: '10px' }
  }
}).mount()">
  <!-- splide__track, splide__list and your slides go here -->
</div>
```

Each number under `breakpoints` is a screen width in pixels. The options inside it apply at that width and below.

***

### Code examples

#### Example 1: Single hero

One large banner for your first slider competition, using the **Title** and **Value** fields as a headline. If no competition is in the slider, it shows your **Featured Image** instead.

```twig
<style>
  .hero {
    position: relative;
    min-height: 420px;
    display: flex;
    align-items: flex-end;
    background-size: cover;
    background-position: center;
    border-radius: 16px;
    overflow: hidden;
    margin: 16px;
  }
  .hero::before {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(transparent 30%, rgba(0, 0, 0, 0.85));
  }
  .hero-content {
    position: relative;
    padding: 32px;
    color: white;
    width: 100%;
  }
  .hero-kicker {
    color: var(--hero-primary);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-size: 13px;
  }
  .hero-title {
    font-size: 36px;
    font-weight: 800;
    margin: 6px 0 12px;
    line-height: 1.1;
  }
  .hero-btn {
    display: inline-block;
    background: var(--hero-primary);
    color: var(--hero-btn-text);
    padding: 14px 28px;
    border-radius: 10px;
    font-weight: 700;
    text-decoration: none;
  }
  @media (max-width: 640px) {
    .hero { min-height: 300px; margin: 8px; border-radius: 12px; }
    .hero-content { padding: 16px; }
    .hero-title { font-size: 24px; }
  }
</style>

{% if products|length > 0 %}
  {% set hero = products|first %}
  <section class="hero"
    style="--hero-primary: {{ settings.primary_color|default('#4ade80') }}; --hero-btn-text: {{ settings.button_text_color|default('#000000') }}; background-image: url('{{ hero.slider_image_url|default(hero.image_url) }}');">
    <div class="hero-content">
      {% if title %}<div class="hero-kicker">{{ title }} {{ value }}</div>{% endif %}
      <h2 class="hero-title">{{ hero.title }}</h2>
      <p>Tickets {{ hero.price_display|trim }}. Closes {{ hero.end_date }}.</p>
      <a href="{{ hero.url }}" class="hero-btn">Enter Now</a>
    </div>
  </section>
{% elseif featured_image_url %}
  <img src="{{ featured_image_url }}" alt="{{ settings.site_name }}" style="width: 100%; display: block;">
{% endif %}
```

#### Example 2: Hero with a trust bar

A carousel of your slider competitions with a bar underneath showing the four **Content** fields as two pairs, in the same way the built-in Big Slider does. For example, set **Title** to "Guaranteed Draws", **Value** to "Every Week", **Subtitle** to "No Extensions" and **Subvalue** to "Ever".

```twig
<style>
  .tb-wrap { --tb-primary: {{ settings.primary_color|default('#4ade80') }}; }
  .tb-slide img { width: 100%; height: 420px; object-fit: cover; display: block; }
  .tb-bar {
    display: flex;
    justify-content: center;
    gap: 40px;
    background: #0b0f1a;
    color: white;
    padding: 14px 16px;
    flex-wrap: wrap;
  }
  .tb-bar strong { color: var(--tb-primary); }
  @media (max-width: 640px) {
    .tb-slide img { height: 220px; }
    .tb-bar { gap: 16px; font-size: 13px; }
  }
</style>

<div class="tb-wrap">
  {% if products|length > 0 %}
    <div class="splide" x-data x-init="new Splide($el, { type: 'loop', perPage: 1, autoplay: true, interval: 6000, pauseOnHover: true }).mount()">
      <div class="splide__track">
        <ul class="splide__list">
          {% for product in products %}
            <li class="splide__slide tb-slide">
              <a href="{{ product.url }}">
                <img src="{{ product.slider_image_url|default(product.image_url) }}" alt="{{ product.title }}">
              </a>
            </li>
          {% endfor %}
        </ul>
      </div>
    </div>
  {% endif %}

  {% if title or subtitle %}
    <div class="tb-bar">
      {% if title %}<span><strong>{{ title }}</strong> {{ value }}</span>{% endif %}
      {% if subtitle %}<span><strong>{{ subtitle }}</strong> {{ subvalue }}</span>{% endif %}
    </div>
  {% endif %}
</div>
```

#### Example 3: Full featured showcase

A modern single slide carousel with a split image and details layout, sale and instant win badges, a progress bar, the cash alternative and an enter button. It uses your site colours and has a separate phone layout with the price over the image.

<details>

<summary>Show the full template</summary>

```html
<style>
  .ultimate-slider {
    --slider-primary: {{ settings.primary_color|default('#4ade80') }};
    --slider-bg: #0f0f23;
    --slider-btn-text: {{ settings.button_text_color|default('#000000') }};
    background: var(--slider-bg);
    padding: 40px 0;
    position: relative;
    overflow: hidden;
  }

  .ultimate-slider::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 200px;
    background: linear-gradient(180deg, var(--slider-primary), transparent);
    opacity: 0.1;
    pointer-events: none;
  }

  .ultimate-header {
    text-align: center;
    margin-bottom: 30px;
    padding: 0 20px;
  }

  .ultimate-header h2 {
    color: white;
    font-size: 32px;
    font-weight: 700;
    margin-bottom: 8px;
  }

  .ultimate-header p {
    color: #9ca3af;
    font-size: 16px;
  }

  .ultimate-slide {
    padding: 0 20px;
  }

  .ultimate-card {
    display: grid;
    grid-template-columns: 1fr 1fr;
    max-width: 1200px;
    margin: 0 auto;
    background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
    border-radius: 24px;
    overflow: hidden;
    border: 1px solid rgba(255, 255, 255, 0.1);
  }

  .ultimate-image-wrapper {
    position: relative;
    min-height: 450px;
  }

  .ultimate-image {
    width: 100%;
    height: 100%;
    object-fit: cover;
    position: absolute;
    inset: 0;
  }

  .ultimate-badges {
    position: absolute;
    top: 16px;
    left: 16px;
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    z-index: 10;
  }

  .ultimate-badge {
    padding: 6px 12px;
    border-radius: 20px;
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
  }

  .ultimate-badge--sale {
    background: #ef4444;
    color: white;
  }

  .ultimate-badge--instant {
    background: var(--slider-primary);
    color: var(--slider-btn-text);
  }

  .ultimate-mobile-price-overlay {
    display: none;
  }

  .ultimate-content {
    padding: 32px;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  .ultimate-category {
    color: var(--slider-primary);
    font-size: 14px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 1px;
    margin-bottom: 8px;
  }

  .ultimate-title {
    color: white;
    font-size: 28px;
    font-weight: 700;
    margin-bottom: 8px;
    line-height: 1.2;
  }

  .ultimate-subtitle {
    color: #9ca3af;
    font-size: 16px;
    margin-bottom: 20px;
  }

  .ultimate-stat {
    margin-bottom: 20px;
    background: rgba(0, 0, 0, 0.3);
    border-radius: 12px;
    padding: 16px;
    text-align: center;
  }

  .ultimate-stat-label {
    color: #9ca3af;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin-bottom: 4px;
  }

  .ultimate-stat-value {
    color: white;
    font-size: 20px;
    font-weight: 700;
  }

  .ultimate-stat-value small {
    color: #6b7280;
    font-size: 14px;
  }

  .ultimate-progress-wrapper {
    margin-bottom: 20px;
  }

  .ultimate-progress-header {
    display: flex;
    justify-content: space-between;
    margin-bottom: 8px;
  }

  .ultimate-progress-text {
    color: #9ca3af;
    font-size: 13px;
  }

  .ultimate-progress-percent {
    color: var(--slider-primary);
    font-weight: 600;
    font-size: 13px;
  }

  .ultimate-progress-bar {
    height: 8px;
    background: rgba(255, 255, 255, 0.1);
    border-radius: 4px;
    overflow: hidden;
  }

  .ultimate-progress-fill {
    height: 100%;
    background: linear-gradient(90deg, var(--slider-primary), #22d3ee);
    border-radius: 4px;
    transition: width 0.5s ease;
  }

  .ultimate-pricing {
    display: flex;
    align-items: baseline;
    gap: 12px;
    margin-bottom: 16px;
  }

  .ultimate-price {
    color: var(--slider-primary);
    font-size: 36px;
    font-weight: 800;
  }

  .ultimate-original-price {
    color: #6b7280;
    font-size: 18px;
    text-decoration: line-through;
  }

  .ultimate-cash-alt {
    color: #9ca3af;
    font-size: 14px;
    margin-bottom: 20px;
  }

  .ultimate-cash-alt span {
    color: white;
    font-weight: 600;
  }

  .ultimate-cta {
    display: block;
    width: 100%;
    padding: 16px 32px;
    background: var(--slider-primary);
    color: var(--slider-btn-text);
    font-size: 16px;
    font-weight: 700;
    text-align: center;
    text-decoration: none;
    border-radius: 12px;
    transition: transform 0.3s ease;
  }

  .ultimate-cta:hover {
    transform: translateY(-2px);
  }

  .ultimate-slider .splide__pagination {
    bottom: -30px;
  }

  .ultimate-slider .splide__pagination__page {
    width: 10px;
    height: 10px;
    background: rgba(255, 255, 255, 0.3);
    opacity: 1;
  }

  .ultimate-slider .splide__pagination__page.is-active {
    background: var(--slider-primary);
    transform: scale(1.2);
  }

  .ultimate-slider .splide__arrow {
    background: rgba(255, 255, 255, 0.1);
    width: 48px;
    height: 48px;
    border-radius: 50%;
    opacity: 1;
  }

  .ultimate-slider .splide__arrow:hover {
    background: var(--slider-primary);
  }

  .ultimate-slider .splide__arrow svg {
    fill: white;
    width: 20px;
    height: 20px;
  }

  /* Tablets: stack the image above the details */
  @media (max-width: 900px) {
    .ultimate-card {
      grid-template-columns: 1fr;
    }
  }

  /* Phones: smaller spacing, price shown over the image */
  @media (max-width: 640px) {
    .ultimate-slider { padding: 16px 0 40px; }
    .ultimate-header { margin-bottom: 16px; padding: 0 12px; }
    .ultimate-header h2 { font-size: 22px; }
    .ultimate-header p { font-size: 13px; }
    .ultimate-slide { padding: 0 8px; }
    .ultimate-card { border-radius: 12px; }
    .ultimate-image-wrapper { min-height: 220px; height: 220px; }
    .ultimate-badges { top: 10px; left: 10px; gap: 6px; }
    .ultimate-badge { padding: 4px 10px; font-size: 10px; }
    .ultimate-mobile-price-overlay {
      display: flex;
      position: absolute;
      bottom: 0;
      left: 0;
      right: 0;
      padding: 12px;
      background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
      align-items: baseline;
      gap: 8px;
    }
    .ultimate-mobile-price-overlay .ultimate-price { font-size: 28px; }
    .ultimate-mobile-price-overlay .ultimate-original-price { font-size: 16px; }
    .ultimate-content { padding: 14px; }
    .ultimate-title { font-size: 18px; margin-bottom: 4px; }
    .ultimate-subtitle { font-size: 13px; margin-bottom: 12px; }
    .ultimate-stat { margin-bottom: 12px; padding: 10px; border-radius: 8px; }
    .ultimate-stat-value { font-size: 16px; }
    .ultimate-progress-wrapper { margin-bottom: 12px; }
    .ultimate-pricing { display: none; }
    .ultimate-cash-alt { font-size: 12px; margin-bottom: 12px; }
    .ultimate-cta { padding: 14px 20px; font-size: 15px; border-radius: 10px; }
    .ultimate-slider .splide__arrow { width: 36px; height: 36px; }
    .ultimate-slider .splide__arrow svg { width: 16px; height: 16px; }
    .ultimate-slider .splide__pagination { bottom: -24px; }
  }
</style>

{% if products|length > 0 %}
<section class="ultimate-slider">
  <div class="ultimate-header">
    <h2>{{ title|default('This Week\'s Top Prizes') }}</h2>
    <p>{{ subtitle|default('Don\'t miss your chance to win big') }}</p>
  </div>

  <div class="splide" x-data x-init="new Splide($el, {
    type: 'loop',
    perPage: 1,
    autoplay: true,
    interval: 6000,
    pauseOnHover: true,
    pagination: true,
    arrows: true,
    speed: 800,
    gap: '20px',
    padding: { left: 20, right: 20 }
  }).mount()">
    <div class="splide__track">
      <ul class="splide__list">
        {% for product in products %}
          <li class="splide__slide">
            <div class="ultimate-slide">
              <div class="ultimate-card">
                <div class="ultimate-image-wrapper">
                  <img src="{{ product.slider_image_url|default(product.image_url) }}" alt="{{ product.title }}" class="ultimate-image" loading="lazy">
                  <div class="ultimate-badges">
                    {% if product.is_on_sale %}
                      <span class="ultimate-badge ultimate-badge--sale">Sale</span>
                    {% endif %}
                    {% if product.instant_wins > 0 %}
                      <span class="ultimate-badge ultimate-badge--instant">{{ product.instant_wins }} Instant Wins</span>
                    {% endif %}
                  </div>
                  <div class="ultimate-mobile-price-overlay">
                    <span class="ultimate-price">{{ product.price }}</span>
                    {% if product.is_on_sale %}
                      <span class="ultimate-original-price">{{ product.original_price }}</span>
                    {% endif %}
                  </div>
                </div>

                <div class="ultimate-content">
                  {% if product.category %}
                    <span class="ultimate-category">{{ product.category }}</span>
                  {% endif %}

                  <h3 class="ultimate-title">{{ product.title }}</h3>

                  {% if product.subtitle %}
                    <p class="ultimate-subtitle">{{ product.subtitle }}</p>
                  {% endif %}

                  {% if not product.simple_max_ticket %}
                    <div class="ultimate-stat">
                      <div class="ultimate-stat-label">Tickets Sold</div>
                      <div class="ultimate-stat-value">{{ product.sold_tickets|number_format }} <small>/ {{ product.max_tickets|number_format }}</small></div>
                    </div>
                  {% endif %}

                  <div class="ultimate-progress-wrapper" x-data="raffleProgress({{ product.percent_sold }})">
                    <div class="ultimate-progress-header">
                      <span class="ultimate-progress-text">Closes {{ product.end_date }}</span>
                      <span class="ultimate-progress-percent" x-text="percent.toFixed(1) + '% Sold'"></span>
                    </div>
                    <div class="ultimate-progress-bar">
                      <div class="ultimate-progress-fill" :style="'width:' + width + '%'"></div>
                    </div>
                  </div>

                  <div class="ultimate-pricing">
                    <span class="ultimate-price">{{ product.price }}</span>
                    {% if product.is_on_sale %}
                      <span class="ultimate-original-price">{{ product.original_price }}</span>
                    {% endif %}
                  </div>

                  {% if product.cash_alt_raw > 0 %}
                    <p class="ultimate-cash-alt">Cash Alternative: <span>{{ product.cash_alt }}</span></p>
                  {% endif %}

                  <a href="{{ product.url }}" class="ultimate-cta">Enter Now for {{ product.price_display|trim }}</a>
                </div>
              </div>
            </div>
          </li>
        {% endfor %}
      </ul>
    </div>
  </div>
</section>
{% endif %}
```

</details>

***

### Common patterns

#### Show nothing when the slider is empty

```twig
{% if products|length > 0 %}
  <!-- your slider goes here -->
{% endif %}
```

#### Show only the first few competitions

```twig
{% for product in products|slice(0, 5) %}
  <!-- your slide goes here -->
{% endfor %}
```

#### Show a message when there is nothing to show

```twig
{% for product in products %}
  <!-- your slide goes here -->
{% else %}
  <p>New competitions coming soon. Check back shortly!</p>
{% endfor %}
```

#### Cash alternative (only when one is set)

```twig
{% if product.cash_alt_raw > 0 %}
  <span>Or take {{ product.cash_alt }} cash</span>
{% endif %}
```

#### Use the portrait image on phones

```twig
<picture>
  {% if product.portrait_image_url %}
    <source media="(max-width: 640px)" srcset="{{ product.portrait_image_url }}">
  {% endif %}
  <img src="{{ product.slider_image_url|default(product.image_url) }}" alt="{{ product.title }}" style="width: 100%; display: block;">
</picture>
```

***

### Tips and troubleshooting

* **Use your brand colours.** Put `settings` values into CSS custom properties, for example `--brand: {{ settings.primary_color|default('#4ade80') }};`, then use `var(--brand)` in your CSS. Your slider then follows any colour changes you make in your settings. Be careful with `settings.background`, which is only a colour code for some background choices (see [Settings Variables](/advanced/twig-templating/settings-variables.md)). For a section background, a fixed colour is safer.
* **Design for phones first.** Most customers visit on a phone. Add `@media (max-width: 640px)` rules that reduce padding, font sizes and image heights so the competition and the enter button fit on screen without scrolling.
* **Give your classes unique names** (for example `mysite-hero` rather than `hero`) so they do not clash with the rest of your site.
* **Add `loading="lazy"`** to images further down the slider so the page loads faster.
* **Use the filters that are available.** Templates support a set list of Twig filters. See [Twig Templating](/advanced/twig-templating.md) for the list.

| Problem                                 | What to check                                                                                                                                 |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| A red **Template Error** box appears    | Read the message. It usually points to a missing `endif` or `endfor`, or a filter that is not available.                                      |
| The slider is empty                     | No competition currently has **Add To Slider** on and is live. See [Which competitions are included](#which-competitions-are-included).       |
| A competition image is blank            | It has no **Slider Image**. Fall back to the featured image, as shown in [Which competitions are included](#which-competitions-are-included). |
| The carousel does not move              | Check the `splide`, `splide__track`, `splide__list` and `splide__slide` classes, and that you start it with `x-init` as shown above.          |
| The cash alternative shows "£0"         | Only show it when `product.cash_alt_raw > 0`.                                                                                                 |
| `site_name` or `currency_icon` is blank | Use `settings.site_name` and `settings.currency_icon`.                                                                                        |
| A change to a competition has not shown | Wait a minute or two and refresh the page.                                                                                                    |

***

### AI Context

**For AI Assistants:** This documents the Rafflex **Sliders** block's "Custom Twig (Advanced)" style for building a custom hero or slider section.

**Block placement:** the Sliders block can be used on any page built with the page builder, most often the homepage. The whole section is one template (HTML, a `<style>` tag and Twig in a single box).

**Context variables:**

* Block content: `title`, `value`, `subtitle`, `subvalue` (text fields, empty strings when blank), `featured_image_url` (empty string when no image).
* `products`: list of competitions with Add To Slider switched on that are live (active, started, not ended, not sold out, not hidden), always in manual sort order. Can be empty.
* `settings`: `settings.primary_color`, `settings.button_text_color`, `settings.background` (a hex code only for some themes; it can be a palette name such as "slate", so do not rely on it as a CSS colour), `settings.site_name`, `settings.currency_icon`, `settings.logo_url` and the others in Settings Variables. There are no top level `site_name` or `currency_icon` variables.

**Product fields:** id, title, subtitle, slug, url (absolute), slider\_image\_url (empty string if none, so use `|default(product.image_url)`), image\_url, portrait\_image\_url, gallery, featured\_video\_url, video\_embed\_url, video\_embed\_type, video\_embed\_mime\_type, video\_embed\_orientation, price, price\_raw, price\_display (may have a leading space, use `|trim`), original\_price (formatted normal price, always filled in, so only show it when is\_on\_sale is true), original\_price\_raw, is\_on\_sale, is\_free, has\_question, question, cash\_alt (never empty, "£0" when none), cash\_alt\_raw, end\_date, end\_date\_raw, end\_date\_seconds, draw\_date, draw\_date\_raw, draw\_date\_seconds, start\_date, start\_date\_raw, start\_date\_seconds, has\_started, max\_tickets, sold\_tickets, tickets\_remaining, percent\_sold, max\_per\_user, simple\_max\_ticket, has\_ended, is\_sold\_out, is\_active, is\_featured, auto\_draw, leaderboard\_enabled, is\_leaderboard\_only, winner\_qty, instant\_wins, instant\_wins\_remaining, game\_type, category, categories, description (usually empty, do not rely on it).

**Twig:** tags `if`, `for`, `set`. Filters `upper`, `lower`, `date`, `number_format`, `default`, `length`, `first`, `last`, `join`, `slice`, `round`, `trim`, `escape`.

**SplideJS (important):** Splide and its base CSS are already loaded. Initialise with Alpine, never with the `data-splide` attribute:

```html
<div class="splide" x-data x-init="new Splide($el, { type: 'loop', perPage: 1, autoplay: true }).mount()">
  <div class="splide__track"><ul class="splide__list"><li class="splide__slide">...</li></ul></div>
</div>
```

Common options: type (loop, slide, fade), perPage, gap, autoplay, interval, pauseOnHover, arrows, pagination, speed, padding, breakpoints.

**Dynamic Alpine components:**

* `raffleCountdown(seconds)`: pass `product.end_date_seconds`. Properties `time.days`, `time.hours`, `time.minutes`, `time.seconds`, `hasEnded`, method `pad(num)`.
* `raffleProgress(percent)`: pass `product.percent_sold`. Properties `percent`, `width`.
* `raffleQuickAdd(config)`: config `productId`, `price` (use `price_raw`), `maxTickets`, `hasQuestion`, `hasEnded`. Properties `tickets`, `loading`, `added`, `error`, `errorMessage`, `canAdd`. Methods `increment()`, `decrement()`, `addToCart()`. Do not use for competitions with an entry question.

**Best practices:**

* Always handle an empty `products` list.
* Use settings colours through CSS custom properties.
* Every template must be mobile responsive with `@media` queries. Reduce padding (around 24px vertical and 12px horizontal), font sizes and image heights on phones so key content sits above the fold.
* Use `loading="lazy"` on images and include hover states for links and buttons.
* Examples are for context only. Always follow the site owner's design directions.

***

**See also:** [Settings Variables](/advanced/twig-templating/settings-variables.md) for the site wide values available in every template, [How the Sliders Block Chooses Competitions](/customise-your-site/set-up-your-homepage/how-the-slider-chooses-competitions.md) for how competitions are picked, and [Custom Twig Product Grid](/advanced/twig-templating/custom-twig-product-grid.md) for custom competition cards.
