> 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-block.md).

# Custom Twig Block

The Custom Twig Block is the most flexible block in the page builder. It lets you create completely custom layouts by defining your own variables and writing HTML with Twig templating.

The **Custom Twig Block** lets you design a section of a page completely from scratch. You create the pieces of information you need (called **variables**), such as a headline, an image, a colour, a list of your competitions or your latest winners, and then write an HTML template that decides exactly how they look.

Because the competitions, winners and customer details are pulled live from your site, the section keeps itself up to date. You set it up once and it changes automatically as competitions open, sell and close.

{% hint style="info" %}
**New to Twig?** Read the [Twig Templating](/advanced/twig-templating.md) overview first. It explains the basic syntax (printing values, if statements and loops) in a few minutes. You can also hand this page to an AI assistant and ask it to write the template for you (see [Need Help?](#need-help)).
{% endhint %}

***

### How It Works <a href="#how-it-works" id="how-it-works"></a>

1. **Add the block** to any page in the page builder.
2. **Create your variables** in the **Custom Variables** list. Each one has a name, a type and a value.
3. **Write your template** in the **HTML + Twig Code** editor, using your variable names to show their values.
4. **Save and check** the page on your live site.

***

### Add the Block <a href="#add-the-block" id="add-the-block"></a>

{% stepper %}
{% step %}

#### Open the page

In the **admin panel**, look in the left hand menu under **Content** and click **Pages**. Click the page you want to add the section to.
{% endstep %}

{% step %}

#### Add a Custom Twig Block

Scroll to the bottom of the **Blocks** area and click **Add to blocks**. Choose **Custom Twig Block** from the list. The new block appears at the bottom of the page. You can drag it up or down to where you want it.
{% endstep %}

{% step %}

#### Create your variables

In the **Custom Variables** area, click **Add to custom Variables** to add a variable. For each one:

1. Type a **Variable Name**, for example `headline`.
2. Choose its **Type**, for example **Text**.
3. Fill in its value. The fields you see change depending on the type you chose.

Repeat for every piece of information you need. Each variable shows its name as a heading, and you can click it to collapse it and keep the list tidy.
{% endstep %}

{% step %}

#### Write your template

Click the **Twig Template** section (it starts collapsed) to expand it. Type or paste your HTML and Twig into the **HTML + Twig Code** editor.

Underneath the editor, **Your Variables** lists every variable you have created with a ready made example of how to use it, so you can copy it straight into your template.
{% endstep %}

{% step %}

#### Save and check

Click **Save**. Open the page on your live site in a new tab and refresh it to see your section.
{% endstep %}
{% endstepper %}

{% hint style="warning" %}
**Check the page while logged in as an admin.** If something in your template is wrong, admins see a red **Template Error** box in place of the section, with a short explanation of the problem. Customers do not see that message. Instead, they see the section with your Twig code showing as plain text, so fix any error straight away. See [Troubleshooting](#troubleshooting) for the most common errors.
{% endhint %}

***

### Variable Names <a href="#variable-names" id="variable-names"></a>

The **Variable Name** is what you type in your template to use the variable. For example, a variable named `headline` is shown with `{{ headline }}`.

* Use letters, numbers and underscores (`_`) only. No spaces, dashes or other symbols.
* The name must start with a letter or an underscore, not a number.
* Every variable in the same block needs a different name.
* Names are case sensitive, so `Title` and `title` are two different variables. Sticking to lower case avoids mistakes.
* Choose short, descriptive names. `hero_headline` is much easier to work with later than `text1`.

**Good examples:** `headline`, `button_text`, `featured_products`, `show_prices`

{% hint style="warning" %}
**Do not name a variable `settings`.** That name is already used for your site's colours, logos and social links (see [Settings Variables](/advanced/twig-templating/settings-variables.md)), and your own variable would replace it.
{% endhint %}

***

### Variable Types <a href="#variable-types" id="variable-types"></a>

The **Type** dropdown has the following options. The first eleven hold information you type or upload yourself. The rest pull live information from your site.

| Type                          | What you fill in                                | What your template receives                                                                                                        |
| ----------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| **Text**                      | A single line of text                           | That text                                                                                                                          |
| **Text Area**                 | Several lines of text                           | That text                                                                                                                          |
| **Number**                    | A number                                        | That number (0 if left empty)                                                                                                      |
| **Boolean (Yes/No)**          | An on or off switch                             | Yes (true) or no (false)                                                                                                           |
| **Color**                     | A colour, picked from a colour picker           | A colour code such as `#4f46e5`                                                                                                    |
| **Date & Time**               | A date and time                                 | The date and time, ready to format                                                                                                 |
| **URL**                       | A full web address                              | That address                                                                                                                       |
| **Image**                     | One uploaded image                              | The full address of the image                                                                                                      |
| **Gallery (up to 20 images)** | Up to 20 uploaded images                        | A list of images                                                                                                                   |
| **Custom Fields**             | A table of named values                         | Each value, by its name                                                                                                            |
| **JSON (for loops)**          | A list written in JSON                          | A list you can loop through                                                                                                        |
| **Products (from database)**  | Filters, or a hand picked set of competitions   | A list of competitions                                                                                                             |
| **Recent Winners**            | How many winners to show                        | A list of your latest draw winners                                                                                                 |
| **Latest Instant Winners**    | How many to show, and an optional minimum value | A list of your latest instant winners                                                                                              |
| **Current User**              | Nothing                                         | The customer viewing the page                                                                                                      |
| **Club (membership tiers)**   | Nothing                                         | Your Club tiers and the visitor's membership. Only listed while your [Club](/club-memberships/club-memberships.md) is switched on. |

Each type is explained below.

#### Text <a href="#text" id="text"></a>

A single line of text, typed into the **Value** field.

**Use for:** Headlines, button wording, short labels.

```twig
<h1>{{ headline }}</h1>
<a href="{{ settings.trustpilot_url }}">{{ button_text }}</a>
```

#### Text Area <a href="#text-area" id="text-area"></a>

Several lines of text, typed into the **Value** field.

**Use for:** Descriptions and longer paragraphs.

```twig
<p>{{ description }}</p>
```

{% hint style="info" %}
Line breaks you type into a Text Area are not shown as separate lines on the page, because web pages ignore them. To keep them, add `style="white-space: pre-line;"` to the tag that shows the text, for example `<p style="white-space: pre-line;">{{ description }}</p>`. Alternatively, create one Text Area variable per paragraph, or use a **JSON (for loops)** list.
{% endhint %}

#### Number <a href="#number" id="number"></a>

A number, typed into the **Value** field. If you leave it empty, your template receives 0.

**Use for:** Counts, percentages, sizes.

```twig
<p>Over {{ winners_count|number_format }} winners and counting!</p>
<div style="width: {{ progress }}%;"></div>
```

#### Boolean (Yes/No) <a href="#boolean-yesno" id="boolean-yesno"></a>

A **Value** switch that is either on (yes) or off (no).

**Use for:** Showing or hiding part of your section without deleting the code. For example, switch a promotional banner on for a weekend sale and off again afterwards.

```twig
{% if show_banner %}
  <div>Special Offer!</div>
{% endif %}
```

#### Color <a href="#color" id="color"></a>

A colour picker. Your template receives a colour code such as `#1a1a2e`. If you do not pick a colour, it receives black (`#000000`).

**Use for:** Backgrounds, text colours and borders you want to change without editing the code.

```twig
<style>
  .custom-section { background-color: {{ background_color }}; }
  .custom-title { color: {{ text_color }}; }
</style>

<div class="custom-section">
  <h2 class="custom-title">{{ title }}</h2>
</div>
```

{% hint style="info" %}
If you simply want your site's own brand colours, you do not need a Color variable. Use `{{ settings.primary_color }}` instead, and the section will update by itself if you ever change your theme. See [Settings Variables](/advanced/twig-templating/settings-variables.md).
{% endhint %}

#### Date & Time <a href="#date--time" id="date--time"></a>

A **Date & Time** picker. Use the `date` filter to choose how it looks on the page.

**Use for:** Event dates, offer deadlines, announcements.

```twig
<p>Offer ends {{ end_date|date('l j F') }}</p>
<p>Live draw at {{ draw_time|date('g:ia') }}</p>
<p>{{ event_date|date('j M Y \\a\\t g:ia') }}</p>
```

These print, for example, "Offer ends Monday 5 January", "Live draw at 7:00pm" and "5 Jan 2026 at 7:00pm". Without a filter, the date prints as `2026-01-05 19:00:00`.

{% hint style="warning" %}
**Always pick a date.** If the **Date & Time** field is left empty, the `date` filter shows today's date and time instead. If the date is optional, wrap it in an `if`, for example `{% if end_date %}Offer ends {{ end_date|date('l j F') }}{% endif %}`.
{% endhint %}

| Code   | Shows                             | Example |
| ------ | --------------------------------- | ------- |
| `j`    | Day of the month                  | 5       |
| `jS`   | Day with "st", "nd", "rd" or "th" | 5th     |
| `l`    | Day of the week                   | Monday  |
| `D`    | Short day of the week             | Mon     |
| `F`    | Month                             | January |
| `M`    | Short month                       | Jan     |
| `Y`    | Year                              | 2026    |
| `g:ia` | Time, 12 hour clock               | 7:00pm  |
| `H:i`  | Time, 24 hour clock               | 19:00   |

#### URL <a href="#url" id="url"></a>

A web address, typed into the **URL** field.

**Use for:** Links and buttons.

```twig
<a href="{{ learn_more_url }}">Learn More</a>
```

{% hint style="warning" %}
**The URL field needs a full web address,** starting with `https://`, for example `https://yoursite.co.uk/results`. A short link such as `/results` will not save. If you prefer short links, use a **Text** variable instead.
{% endhint %}

#### Image <a href="#image" id="image"></a>

Upload one image into the **Image** field. Your template receives the full address of the image, or nothing if no image has been uploaded.

**Use for:** Hero images, banners, backgrounds, badges.

```twig
{% if hero_image %}
  <img src="{{ hero_image }}" alt="{{ headline }}">
{% endif %}

<div style="background-image: url('{{ background_image }}');"></div>
```

#### Gallery (up to 20 images) <a href="#gallery" id="gallery"></a>

Upload up to 20 images into the **Gallery Images** field. Drag them into the order you want them to appear.

**Use for:** Photo galleries, logo strips, image carousels.

| Field  | What it contains              |
| ------ | ----------------------------- |
| `url`  | The full address of the image |
| `name` | The image's stored file name  |

```twig
{% if my_gallery|length > 0 %}
<div class="my-gallery">
  {% for image in my_gallery %}
    <img src="{{ image.url }}" alt="Photo {{ loop.index }}">
  {% endfor %}
</div>
{% endif %}
```

{% hint style="info" %}
The stored file name is generated by the system when you upload, so it is not the same as the name of the file on your computer. For image descriptions (alt text), write your own, as in the example above.
{% endhint %}

#### Custom Fields <a href="#custom-fields" id="custom-fields"></a>

A small table of named values. Click **Add row**, type a name in the **Key** column and its value in the **Value** column. Rows can be dragged into a different order.

**Use for:** Groups of related details, such as contact information or opening times.

For example, a variable named `contact` with the keys `email`, `phone` and `hours`:

```twig
{# Show specific fields by their key #}
<p>Email: {{ contact.email }}</p>
<p>Phone: {{ contact.phone }}</p>

{# Or loop through every row #}
<dl>
  {% for key, value in contact %}
    <dt>{{ key }}</dt>
    <dd>{{ value }}</dd>
  {% endfor %}
</dl>
```

{% hint style="info" %}
To use `contact.email`, the key must follow the same rules as variable names (letters, numbers and underscores). If you only ever loop through the rows, the keys can contain spaces.
{% endhint %}

#### JSON (for loops) <a href="#json" id="json"></a>

A list of items written in JSON, typed into the **JSON Data** editor. JSON is a simple way of writing structured information. Each item sits between curly brackets `{ }`, items are separated by commas, and the whole list sits between square brackets `[ ]`.

**Use for:** Any repeating content you type yourself, such as feature lists, FAQs, steps or links.

**Example JSON:**

```json
[
  {"title": "Pick a prize", "text": "Choose from our live competitions."},
  {"title": "Answer the question", "text": "Enter your tickets in seconds."},
  {"title": "Watch the draw", "text": "Winners are announced live."}
]
```

**In your template:**

```twig
{% for step in how_it_works %}
  <div>
    <h3>{{ loop.index }}. {{ step.title }}</h3>
    <p>{{ step.text }}</p>
  </div>
{% endfor %}
```

{% hint style="warning" %}
**The page will not save if the JSON has a mistake.** The most common causes are a missing comma between items, a comma after the last item, or using curly quotes (“ ”) instead of straight quotes (" "). If you are stuck, paste your JSON into a free online JSON checker to find the problem.
{% endhint %}

***

### Products (from database) <a href="#products" id="products"></a>

Pulls your competitions straight from your site, so the list keeps itself up to date.

#### Choosing which competitions appear

The **Product Selection** dropdown has two options.

{% tabs %}
{% tab title="Use Filters" %}
Your template receives every competition that matches the filters you set. As competitions open and close, the list changes by itself. This is the best choice for most sections.

| Option           | What it does                                                                                                                                                                                                         |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Status**       | **Active Only** (the default) shows competitions that are live right now: switched to **Active**, past their start date, not yet ended and not sold out. **Inactive Only** shows competitions that are switched off. |
| **Categories**   | Only show competitions in the categories you pick. Leave it empty (it shows **All Categories**) to include every category. See [Product Categories](/create-your-first-raffle/product-categories.md).                |
| **Sliders Only** | When switched on, only competitions with **Add To Slider** switched on are included. Handy for a "featured prizes" section.                                                                                          |
| **Sort By**      | The order of the list (see below).                                                                                                                                                                                   |
| **Limit**        | The most competitions to include. Leave it empty for no limit.                                                                                                                                                       |

**Sort By options:**

* **Ending Soon First** (the default)
* **Ending Last First**
* **Draw Date (Soonest)**
* **Draw Date (Latest)**
* **Price (Low to High)**
* **Price (High to Low)**
* **Title (A-Z)**
* **Title (Z-A)**
* **Newest First**
* **Manual Sort Order** (the drag and drop order you set on your **Competitions** list, see [Managing Your Competitions](/create-your-first-raffle/managing-competitions.md#set-the-order-competitions-appear-in))

Competitions set as a **Hidden / VIP Competition** never appear in a filtered list. See [Hidden VIP Competitions](/create-your-first-raffle/hidden-vip-competitions.md).
{% endtab %}

{% tab title="Select Specific Products" %}
Choose exactly which competitions to include in the **Select Products** field. Start typing a competition's name to find it.

Use this when you want to spotlight particular competitions, for example a "Prize of the Month" section.

Hand picked competitions are always included, whatever their status. That means they stay in your section after they end or sell out, so check the section regularly and swap them out when needed. It also means you can show a competition that has not opened yet as a "coming soon" teaser (see [Scheduled Competitions](#scheduled-competitions-coming-soon) below).

{% hint style="warning" %}
**Be careful with Hidden / VIP competitions.** If you hand pick a Hidden / VIP Competition here, anyone who views the page can see it and click through to it. Only do this on purpose.
{% endhint %}

Hand picked competitions are not guaranteed to appear in the order you selected them. If the order matters, use **Use Filters** with a **Sort By** option instead.
{% endtab %}
{% endtabs %}

{% hint style="info" %}
Competition details in Twig sections are refreshed automatically, but a change you make to a competition (such as a new price or image) can take a minute or two to appear.
{% endhint %}

#### Competition fields

Loop through your Products variable and use these fields on each competition. Where you see `product.` below, it is the name you give each item in your loop, as in `{% for product in featured_products %}`.

| Field                     | What it contains                                                                                                         | Example                                             |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------- |
| `id`                      | The competition's ID number                                                                                              | 123                                                 |
| `title`                   | Competition name                                                                                                         | "Win a BMW M3"                                      |
| `subtitle`                | The **Subtitle**, if set                                                                                                 | "Competition of the Month"                          |
| `slug`                    | The end part of the competition's web address                                                                            | "win-a-bmw-m3"                                      |
| `url`                     | Full link to the competition page                                                                                        | "<https://yoursite.co.uk/competition/win-a-bmw-m3>" |
| `image_url`               | **Featured Image** address                                                                                               | Full image address                                  |
| `slider_image_url`        | **Slider Image** address                                                                                                 | Full image address                                  |
| `portrait_image_url`      | **Portrait Image** address (empty if none)                                                                               | Full image address                                  |
| `featured_video_url`      | **Featured Video** address (empty if none)                                                                               | Full video address                                  |
| `video_embed_url`         | Player address for the competition's **Video URL** (empty if none)                                                       | "<https://www.youtube-nocookie.com/embed/>..."      |
| `video_embed_type`        | "iframe" for YouTube or Vimeo, "video" for a video file                                                                  | "iframe"                                            |
| `video_embed_mime_type`   | File type of a video file (empty for YouTube or Vimeo)                                                                   | "video/mp4"                                         |
| `video_embed_orientation` | "portrait" for YouTube Shorts, otherwise "landscape"                                                                     | "landscape"                                         |
| `price`                   | Current ticket price, with currency symbol                                                                               | "£1.99"                                             |
| `price_display`           | Friendly price: "Free", pence (or cents on a euro site) for prices under 1, otherwise the full price                     | "50p"                                               |
| `price_raw`               | Current ticket price as a number                                                                                         | 1.99                                                |
| `original_price`          | The full price, only when the competition is on sale (empty otherwise)                                                   | "£2.99"                                             |
| `original_price_raw`      | The full price as a number                                                                                               | 2.99                                                |
| `is_on_sale`              | Whether a sale price is set                                                                                              | true / false                                        |
| `is_free`                 | Whether the competition is free to enter                                                                                 | true / false                                        |
| `cash_alt`                | **Cash Alternative**, with currency symbol                                                                               | "£50,000"                                           |
| `cash_alt_raw`            | Cash alternative as a number                                                                                             | 50000                                               |
| `draw_date`               | Friendly draw date                                                                                                       | "Draw On Mon Jan 5th 7:00pm"                        |
| `end_date`                | Friendly closing date. Shows "Sold Out" once every ticket has sold, and "Ended" followed by the date once it has closed. | "Today 7:00pm"                                      |
| `start_date`              | Friendly start date                                                                                                      | "Tomorrow 9:00am"                                   |
| `draw_date_raw`           | Draw date and time                                                                                                       | "2026-01-05 19:00:00"                               |
| `end_date_raw`            | Closing date and time                                                                                                    | "2026-01-05 19:00:00"                               |
| `start_date_raw`          | Start date and time (empty if none set)                                                                                  | "2026-01-01 09:00:00"                               |
| `end_date_seconds`        | Seconds until the competition closes (for countdowns)                                                                    | 3600                                                |
| `draw_date_seconds`       | Seconds until the draw                                                                                                   | 86400                                               |
| `start_date_seconds`      | Seconds until the competition opens (0 once open)                                                                        | 7200                                                |
| `has_started`             | Whether the competition has opened                                                                                       | true / false                                        |
| `has_ended`               | Whether the competition has closed or sold out                                                                           | true / false                                        |
| `is_sold_out`             | Whether every ticket has been sold                                                                                       | true / false                                        |
| `is_active`               | Whether the competition is switched to **Active**                                                                        | true / false                                        |
| `is_featured`             | Whether **Add To Slider** is switched on                                                                                 | true / false                                        |
| `max_tickets`             | Total number of tickets                                                                                                  | 5000                                                |
| `sold_tickets`            | Tickets sold so far                                                                                                      | 3421                                                |
| `tickets_remaining`       | Tickets left                                                                                                             | 1579                                                |
| `percent_sold`            | Percentage sold, to one decimal place                                                                                    | 68.4                                                |
| `max_per_user`            | Most tickets one customer can buy (0 means no limit)                                                                     | 50                                                  |
| `simple_max_ticket`       | Whether **Percentage Only** is switched on, so you can hide ticket numbers and show the percentage instead               | true / false                                        |
| `instant_wins`            | Number of instant win prizes                                                                                             | 5                                                   |
| `instant_wins_remaining`  | Instant win prizes not yet won                                                                                           | 3                                                   |
| `game_type`               | The competition's **Game Type** ("standard" for a normal competition)                                                    | "standard"                                          |
| `winner_qty`              | **Draw Winner Quantity**                                                                                                 | 1                                                   |
| `auto_draw`               | Whether the winner is picked automatically                                                                               | true / false                                        |
| `leaderboard_enabled`     | Whether the competition has a leaderboard                                                                                | true / false                                        |
| `is_leaderboard_only`     | Whether the competition has a leaderboard and no main draw winners                                                       | true / false                                        |
| `has_question`            | Whether the competition has an entry question                                                                            | true / false                                        |
| `question`                | The entry question (empty if none)                                                                                       | "What colour is the sky?"                           |
| `category`                | Name of the first category                                                                                               | "Cars"                                              |
| `categories`              | All category names                                                                                                       | \["Cars", "Featured"]                               |
| `description`             | The competition description                                                                                              | "Win this amazing..."                               |
| `gallery`                 | List of the competition's **Gallery** image addresses                                                                    | \["https\://...", "https\://..."]                   |

**A simple competition list:**

```twig
{% for product in featured_products %}
  <div>
    <img src="{{ product.image_url }}" alt="{{ product.title }}">
    <h3>{{ product.title }}</h3>
    <p>{{ product.subtitle|default('') }}</p>

    {% if product.is_on_sale %}
      <span style="text-decoration: line-through;">{{ product.original_price }}</span>
      <span style="color: red;">{{ product.price }}</span>
    {% else %}
      <span>{{ product.price_display }}</span>
    {% endif %}

    <p>{{ product.draw_date }}</p>
    {% if product.cash_alt_raw > 0 %}
      <p>Cash Alternative: {{ product.cash_alt }}</p>
    {% endif %}

    {% if product.simple_max_ticket %}
      <p>{{ product.percent_sold|round }}% sold</p>
    {% else %}
      <p>{{ product.sold_tickets }} / {{ product.max_tickets }} sold</p>
    {% endif %}

    <a href="{{ product.url }}">Enter Now</a>
  </div>
{% else %}
  <p>New competitions are coming soon!</p>
{% endfor %}
```

#### Video with Image Fallback

Show the competition's **Featured Video** if it has one, otherwise its image:

```twig
{% if product.featured_video_url %}
  <video autoplay loop muted playsinline poster="{{ product.image_url }}" style="width: 100%;">
    <source src="{{ product.featured_video_url }}" type="video/mp4">
  </video>
{% else %}
  <img src="{{ product.image_url }}" alt="{{ product.title }}" style="width: 100%;">
{% endif %}
```

#### Video URL Embed

When a competition has a **Video URL** set (in its **Product Page Display** options), the `video_embed_` fields give you everything needed to show it. YouTube and Vimeo links play in an `iframe`, while a direct video file plays in a `video` tag.

```twig
{% if product.video_embed_url %}
  {% if product.video_embed_type == 'iframe' %}
    {% if product.video_embed_orientation == 'portrait' %}
      {% set ratio = '9 / 16' %}
    {% else %}
      {% set ratio = '16 / 9' %}
    {% endif %}
    <div style="aspect-ratio: {{ ratio }}; width: 100%;">
      <iframe src="{{ product.video_embed_url }}" style="width: 100%; height: 100%; border: 0;" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen loading="lazy"></iframe>
    </div>
  {% else %}
    <video controls playsinline preload="metadata" style="width: 100%;">
      <source src="{{ product.video_embed_url }}" type="{{ product.video_embed_mime_type }}">
    </video>
  {% endif %}
{% endif %}
```

#### Scheduled Competitions (Coming Soon) <a href="#scheduled-competitions-coming-soon" id="scheduled-competitions-coming-soon"></a>

A competition with a future start date (see [Schedule a Competition](/create-your-first-raffle/create-a-competition/schedule-a-competition.md)) has `has_started` set to false. Filtered lists only include competitions that have started, so this is useful when you hand pick competitions with **Select Specific Products**. Use `start_date_seconds` to count down to the opening time.

```twig
{% if not product.has_started %}
  <p>Entries open {{ product.start_date }}</p>
  <div x-data="raffleCountdown({{ product.start_date_seconds }})">
    <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
  </div>
{% else %}
  <a href="{{ product.url }}">Enter Now</a>
{% endif %}
```

***

### Recent Winners <a href="#winners" id="winners"></a>

Shows your latest draw winners, newest first.

| Option                       | What it does                              |
| ---------------------------- | ----------------------------------------- |
| **Number of Recent Winners** | How many winners to include. Starts at 5. |

Winners of **Hidden / VIP** competitions are never included.

| Field           | What it contains                                                                                                                                                    | Example                                             |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| `name`          | The winner's name. This is the name entered on the winner, if there is one, otherwise the customer's name shown the way your **Winners Name Display** setting says. | "John S"                                            |
| `prize`         | What they won (the competition name if no prize is set)                                                                                                             | "BMW M3"                                            |
| `product_title` | Competition name                                                                                                                                                    | "Win a BMW M3"                                      |
| `product_url`   | Full link to the competition                                                                                                                                        | "<https://yoursite.co.uk/competition/win-a-bmw-m3>" |
| `product_image` | The competition's **Featured Image** address                                                                                                                        | Full image address                                  |
| `date`          | Date of the win                                                                                                                                                     | "Jan 5, 2026"                                       |
| `date_ago`      | How long ago they won                                                                                                                                               | "2 days ago"                                        |

This example assumes you named the variable `recent_winners`:

```twig
{% for winner in recent_winners %}
  <div>
    <img src="{{ winner.product_image }}" alt="{{ winner.product_title }}">
    <h3>{{ winner.name }}</h3>
    <p>Won: {{ winner.prize }}</p>
    <p>{{ winner.date_ago }}</p>
    <a href="{{ winner.product_url }}">View Competition</a>
  </div>
{% endfor %}
```

{% hint style="info" %}
**Winners Name Display** is in **Settings**, then **User Experience**. It controls how customer names appear across your site, including here. See [User Experience Settings](/additional-settings/settings/user-experience-settings.md).
{% endhint %}

***

### Latest Instant Winners <a href="#instant-winners" id="instant-winners"></a>

Shows your latest [instant win](/create-your-first-raffle/add-instant-wins.md) winners, newest first.

| Option                        | What it does                                                                                                                                                                                   |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Number of Instant Winners** | How many to include. Starts at 5, and the most you can show is 20.                                                                                                                             |
| **Minimum Payout Value**      | Only show instant wins with a payout value over this amount, so small prizes do not crowd out the big ones. Leave it empty to show all. Prizes that have no payout value set are always shown. |

Only instant wins from the last 30 days are included. Wins on **Hidden / VIP** competitions, and on competitions that have not opened yet, are never included.

| Field             | What it contains                                                                | Example                                             |
| ----------------- | ------------------------------------------------------------------------------- | --------------------------------------------------- |
| `winner_name`     | The winner's name, shown the way your **Winners Name Display** setting says     | "John S"                                            |
| `prize`           | The prize name                                                                  | "£10 Site Credit"                                   |
| `prize_value`     | Payout value with two decimal places, without a currency symbol (empty if none) | "10.00"                                             |
| `prize_value_raw` | Payout value as a number                                                        | 10                                                  |
| `is_site_credit`  | Whether the prize is site credit                                                | true / false                                        |
| `product_title`   | Competition name                                                                | "Win a BMW M3"                                      |
| `product_url`     | Full link to the competition                                                    | "<https://yoursite.co.uk/competition/win-a-bmw-m3>" |
| `product_image`   | The competition's **Featured Image** address                                    | Full image address                                  |
| `date`            | Date of the win                                                                 | "Jan 5, 2026"                                       |
| `date_ago`        | How long ago they won                                                           | "2 hours ago"                                       |

This example assumes you named the variable `instant_winners`:

```twig
{% for winner in instant_winners %}
  <div>
    <img src="{{ winner.product_image }}" alt="{{ winner.product_title }}">
    <h3>{{ winner.winner_name }} won!</h3>
    <p>{{ winner.prize }}</p>
    {% if winner.prize_value %}
      <p>Worth {{ settings.currency_icon }}{{ winner.prize_value }}</p>
    {% endif %}
    <p>{{ winner.date_ago }}</p>
  </div>
{% else %}
  <p>Instant winners will appear here.</p>
{% endfor %}
```

***

### Current User <a href="#user-current-logged-in" id="user-current-logged-in"></a>

The customer who is viewing the page. There is nothing to fill in apart from the **Variable Name**. The examples below assume you named it `user`. If nobody is logged in, the variable still works and `is_logged_in` is false, so you can show a log in prompt instead.

| Field          | What it contains                                             | Example              |
| -------------- | ------------------------------------------------------------ | -------------------- |
| `id`           | The customer's account number (empty for guests)             | 123                  |
| `name`         | First name (empty for guests)                                | "John"               |
| `last_name`    | Last name (empty for guests)                                 | "Smith"              |
| `full_name`    | First and last name (empty for guests)                       | "John Smith"         |
| `display_name` | The customer's display name, or "Guest"                      | "JohnS"              |
| `email`        | Email address (empty for guests)                             | "<john@example.com>" |
| `is_logged_in` | Whether someone is logged in                                 | true / false         |
| `balance`      | Site credit balance, with currency symbol (empty for guests) | "£25.00"             |
| `balance_raw`  | Site credit balance as a number (0 for guests)               | 25                   |

```twig
{% if user.is_logged_in %}
  <div>
    <p>Welcome back, {{ user.name }}!</p>
    {% if user.balance_raw > 0 %}
      <p>You have {{ user.balance }} site credit to spend.</p>
    {% endif %}
  </div>
{% else %}
  <div>
    <p>Log in to see your site credit balance.</p>
  </div>
{% endif %}
```

{% hint style="warning" %}
Each customer only ever sees their own details. Even so, avoid putting personal details such as an email address anywhere prominent on a page, in case someone is looking over the customer's shoulder.
{% endhint %}

***

### Club (membership tiers) <a href="#club-membership-tiers" id="club-membership-tiers"></a>

Your Club tiers, prices and benefits, plus the visitor's own membership. There is nothing to fill in apart from the **Variable Name**. This type is only listed while your Club is switched on, so always wrap your template in `{% if club.enabled %}` (assuming you named it `club`).

See [Club Variables](/advanced/twig-templating/club-variables.md) for every field and examples.

***

### Settings Variables <a href="#settings-variables" id="settings-variables"></a>

As well as your own variables, every Custom Twig Block can use `settings`, which holds your site name, brand colours, logos, social links and review scores. You do not need to create it.

```twig
<h2 style="color: {{ settings.primary_color }};">Welcome to {{ settings.site_name }}</h2>
```

See [Settings Variables](/advanced/twig-templating/settings-variables.md) for the full list.

***

### Filters Reference <a href="#filters-reference" id="filters-reference"></a>

Filters change a value before it is shown. These are the filters you can use:

| Filter          | Example                                      | Result                                              |
| --------------- | -------------------------------------------- | --------------------------------------------------- |
| `upper`         | `{{ name\|upper }}`                          | JOHN                                                |
| `lower`         | `{{ name\|lower }}`                          | john                                                |
| `default`       | `{{ subtitle\|default('None') }}`            | A fallback if the value is empty                    |
| `length`        | `{{ products\|length }}`                     | Number of items                                     |
| `first`         | `{{ (products\|first).title }}`              | The first item (here, the first competition's name) |
| `last`          | `{{ (products\|last).title }}`               | The last item (here, the last competition's name)   |
| `join`          | `{{ product.categories\|join(', ') }}`       | Cars, Featured                                      |
| `slice`         | `{% for product in products\|slice(0, 3) %}` | Only the first three items                          |
| `trim`          | `{{ text\|trim }}`                           | Removes spaces from both ends                       |
| `round`         | `{{ 3.7\|round }}`                           | 4                                                   |
| `number_format` | `{{ 1234.5\|number_format(2) }}`             | 1,234.50                                            |
| `date`          | `{{ my_date\|date('j M Y') }}`               | 5 Jan 2026                                          |
| `escape`        | `{{ text\|escape }}`                         | Text made safe for HTML                             |

`slice` is especially handy for splitting one list into two parts, for example a large card for the first competition and smaller cards for the rest:

```twig
{% set top = products|first %}
<h2>{{ top.title }}</h2>

{% for product in products|slice(1, 4) %}
  <p>{{ product.title }}</p>
{% endfor %}
```

***

### Dynamic Components <a href="#dynamic-components" id="dynamic-components"></a>

Your templates can use a few ready made interactive components for live countdowns, progress bars and quick add buttons. You add them with an `x-data` attribute, as shown below.

#### Countdown Timer <a href="#countdown-timer" id="countdown-timer"></a>

A live countdown to when a competition closes.

```twig
<div x-data="raffleCountdown({{ product.end_date_seconds }})" class="countdown">
  <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
</div>
```

{% hint style="info" %}
Always use `end_date_seconds` (not `end_date_raw`) for countdowns. The seconds are worked out by your site, so the countdown is correct whatever time zone the customer's device is set to. Use `draw_date_seconds` to count down to the draw instead.
{% endhint %}

**Showing a message when it reaches zero:**

```twig
<div x-data="raffleCountdown({{ product.end_date_seconds }})">
  <template x-if="!hasEnded">
    <div class="countdown">
      <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
    </div>
  </template>
  <template x-if="hasEnded">
    <span>Competition Ended</span>
  </template>
</div>
```

| Property       | What it gives you                                        |
| -------------- | -------------------------------------------------------- |
| `time.days`    | Days remaining                                           |
| `time.hours`   | Hours remaining (0 to 23)                                |
| `time.minutes` | Minutes remaining (0 to 59)                              |
| `time.seconds` | Seconds remaining (0 to 59)                              |
| `hasEnded`     | True when the countdown reaches zero                     |
| `pad(num)`     | Adds a leading zero to single digits, so 5 shows as "05" |

#### Progress Bar <a href="#progress-bar" id="progress-bar"></a>

A bar showing how many tickets have sold.

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

| Property  | What it gives you                                                                                      |
| --------- | ------------------------------------------------------------------------------------------------------ |
| `percent` | The percentage sold (0 to 100)                                                                         |
| `width`   | The width to draw the bar. If a few tickets have sold, it is at least 3% so the bar is always visible. |

You style the bar yourself in your `<style>` block (see the complete example below).

#### Quick Add to Cart <a href="#quick-add-to-cart" id="quick-add-to-cart"></a>

A working "add to basket" control for a competition card, with a ticket quantity selector. It adds the tickets, updates the basket count and shows a small confirmation message, without the customer leaving the page.

```twig
<div x-data="raffleQuickAdd({
  productId: {{ product.id }},
  price: {{ product.price_raw }},
  maxTickets: {{ product.max_per_user > 0 ? product.max_per_user : 100 }},
  hasQuestion: {{ product.has_question ? 'true' : 'false' }},
  hasEnded: {{ product.has_ended ? 'true' : 'false' }}
})">
  <button @click="decrement" :disabled="tickets <= 1">-</button>
  <span x-text="tickets"></span>
  <button @click="increment" :disabled="tickets >= maxTickets">+</button>

  <button @click="addToCart" :disabled="!canAdd">
    <span x-show="!loading">Quick Add</span>
    <span x-show="loading">Adding...</span>
  </button>

  <span x-show="added" x-transition>Added!</span>
  <span x-show="error" x-text="errorMessage"></span>
</div>
```

**Settings you pass in:**

| Option        | What it does                                                                                                                                |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `productId`   | The competition to add (`product.id`)                                                                                                       |
| `price`       | The ticket price as a number (`product.price_raw`). Used to pick a sensible starting quantity, so cheaper tickets start with more selected. |
| `maxTickets`  | The most tickets the + button allows (100 if not set)                                                                                       |
| `hasQuestion` | If the competition has an entry question, quick add is switched off and the customer must use the full competition page                     |
| `hasEnded`    | If the competition has ended, quick add is switched off                                                                                     |

**What you can use in your HTML:**

| Property or action | What it does                                                                                          |
| ------------------ | ----------------------------------------------------------------------------------------------------- |
| `tickets`          | The quantity currently selected                                                                       |
| `increment()`      | Adds one ticket (up to `maxTickets`)                                                                  |
| `decrement()`      | Removes one ticket (down to 1)                                                                        |
| `addToCart()`      | Adds the selected tickets to the basket                                                               |
| `loading`          | True while the tickets are being added                                                                |
| `added`            | True for a few seconds after a successful add                                                         |
| `error`            | True if the tickets could not be added                                                                |
| `errorMessage`     | The reason, ready to show to the customer                                                             |
| `requiresLogin`    | True if the customer needs to log in before they can add these tickets. Use it to show a log in link. |
| `canAdd`           | True when the control is ready to add (not busy, not ended, no entry question and a valid quantity)   |

{% hint style="info" %}
Quick add follows exactly the same rules as your normal competition pages, so things like ticket limits are still applied. Competitions with an entry question, or that have ended, cannot be added this way. For those, link the customer to `product.url` instead.
{% endhint %}

#### Cart Count (Headless) <a href="#cart-count-headless" id="cart-count-headless"></a>

`raffleBasket` shows a live basket count that updates whenever something is added. It is designed for custom headers and footers, which have a `cart_count` and `urls` variable. The Custom Twig Block does not have these, so use `raffleQuickAdd` here and see [Header and Footer Variables](/advanced/twig-templating/header-and-footer-variables.md) for `raffleBasket`.

***

### Complete Examples <a href="#complete-examples" id="complete-examples"></a>

Copy any of these as a starting point, create the variables listed, then adjust the wording and styles to suit your site.

#### Hero Section with Products <a href="#hero-section-with-products" id="hero-section-with-products"></a>

**Variables needed:**

* `headline` (Text): "This Week's Top Prizes"
* `subheadline` (Text): "Don't miss out on these amazing competitions"
* `background_color` (Color): #1a1a2e
* `text_color` (Color): #ffffff
* `featured_products` (Products from database): **Use Filters**, **Sliders Only** switched on, **Limit** 3

**Template:**

```html
<style>
  .hero-section {
    background-color: {{ background_color }};
    color: {{ text_color }};
    padding: 60px 20px;
  }
  .hero-container {
    max-width: 1200px;
    margin: 0 auto;
    text-align: center;
  }
  .hero-title {
    font-size: 36px;
    margin-bottom: 10px;
  }
  .hero-subtitle {
    font-size: 18px;
    margin-bottom: 40px;
  }
  .hero-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 30px;
  }
  .hero-card {
    background: #16213e;
    border-radius: 12px;
    overflow: hidden;
  }
  .hero-card img {
    width: 100%;
    height: 200px;
    object-fit: cover;
  }
  .hero-card-content {
    padding: 20px;
  }
  .hero-card h3 {
    font-size: 20px;
    margin-bottom: 10px;
  }
  .hero-card .price {
    color: #4ade80;
    font-size: 24px;
    font-weight: bold;
  }
  .hero-card .date {
    color: #9ca3af;
    font-size: 14px;
  }
  .hero-card .btn {
    display: block;
    margin-top: 15px;
    background: {{ settings.primary_color }};
    color: {{ settings.button_text_color }};
    padding: 12px;
    border-radius: 8px;
    text-decoration: none;
    font-weight: bold;
  }
</style>

<section class="hero-section">
  <div class="hero-container">
    <h1 class="hero-title">{{ headline }}</h1>
    <p class="hero-subtitle">{{ subheadline }}</p>

    <div class="hero-grid">
      {% for product in featured_products %}
        <div class="hero-card">
          <img src="{{ product.image_url }}" alt="{{ product.title }}">
          <div class="hero-card-content">
            <h3>{{ product.title }}</h3>
            <p class="price">{{ product.price_display }}</p>
            <p class="date">{{ product.end_date }}</p>
            <a href="{{ product.url }}" class="btn">Enter Now</a>
          </div>
        </div>
      {% endfor %}
    </div>
  </div>
</section>
```

{% hint style="info" %}
`repeat(auto-fit, minmax(260px, 1fr))` makes the cards sit side by side on a computer and stack neatly on a phone, without any extra code. It is a good habit for any grid you build.
{% endhint %}

#### Winners Strip <a href="#winners-carousel" id="winners-carousel"></a>

**Variables needed:**

* `title` (Text): "Recent Winners"
* `show_title` (Boolean): switched on
* `recent_winners` (Recent Winners): 6

**Template:**

```html
<style>
  .winners-section {
    background: #0f0f23;
    padding: 50px 20px;
  }
  .winners-container {
    max-width: 1200px;
    margin: 0 auto;
  }
  .winners-title {
    color: white;
    font-size: 28px;
    text-align: center;
    margin-bottom: 30px;
  }
  .winners-scroll {
    display: flex;
    gap: 20px;
    overflow-x: auto;
    padding-bottom: 20px;
  }
  .winner-card {
    flex-shrink: 0;
    width: 220px;
    background: #1a1a3e;
    border-radius: 12px;
    padding: 20px;
    text-align: center;
  }
  .winner-card img {
    width: 100px;
    height: 100px;
    border-radius: 50%;
    object-fit: cover;
    margin: 0 auto 15px;
  }
  .winner-card h3 {
    color: white;
    font-size: 18px;
    margin-bottom: 5px;
  }
  .winner-card .prize {
    color: #4ade80;
    font-size: 16px;
    margin-bottom: 5px;
  }
  .winner-card .date {
    color: #6b7280;
    font-size: 13px;
  }
</style>

<section class="winners-section">
  <div class="winners-container">
    {% if show_title %}
      <h2 class="winners-title">{{ title }}</h2>
    {% endif %}

    <div class="winners-scroll">
      {% for winner in recent_winners %}
        <div class="winner-card">
          <img src="{{ winner.product_image }}" alt="{{ winner.product_title }}">
          <h3>{{ winner.name }}</h3>
          <p class="prize">{{ winner.prize }}</p>
          <p class="date">{{ winner.date_ago }}</p>
        </div>
      {% endfor %}
    </div>
  </div>
</section>
```

#### Promotional Banner <a href="#promotional-banner" id="promotional-banner"></a>

**Variables needed:**

* `show_banner` (Boolean): switched on
* `banner_headline` (Text): "Flash Sale!"
* `banner_text` (Text): "Extra tickets on every competition this weekend"
* `banner_color` (Color): #dc2626
* `banner_link` (URL): the full address of the page you want to send customers to, for example your homepage, <https://yoursite.co.uk/>

**Template:**

```html
<style>
  .promo-banner {
    background: {{ banner_color }};
    padding: 20px;
    text-align: center;
  }
  .promo-banner h2 {
    color: white;
    font-size: 24px;
    margin: 0 0 10px 0;
  }
  .promo-banner p {
    color: white;
    font-size: 16px;
    margin: 0 0 15px 0;
  }
  .promo-banner .btn {
    display: inline-block;
    background: white;
    color: {{ banner_color }};
    padding: 12px 30px;
    border-radius: 6px;
    text-decoration: none;
    font-weight: bold;
  }
</style>

{% if show_banner %}
<div class="promo-banner">
  <h2>{{ banner_headline }}</h2>
  <p>{{ banner_text }}</p>
  <a href="{{ banner_link }}" class="btn">Enter Now</a>
</div>
{% endif %}
```

When the sale ends, edit the block, switch `show_banner` off and click **Save**. The banner disappears, but your design is kept for next time.

#### Social Links Bar <a href="#social-links-bar" id="social-links-bar"></a>

This example uses the social links you have already saved in **Settings**, so there is nothing to retype. See [Connect Your Social Accounts](/essential-settings/connect-your-social-accounts.md).

**Variables needed:** none.

**Template:**

```html
<style>
  .social-bar {
    background: #111;
    padding: 20px;
    text-align: center;
  }
  .social-bar p {
    color: #9ca3af;
    margin-bottom: 15px;
  }
  .social-links {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 12px;
  }
  .social-link {
    padding: 10px 18px;
    background: #222;
    border-radius: 999px;
    color: white;
    text-decoration: none;
  }
  .social-link:hover {
    background: {{ settings.primary_color }};
    color: {{ settings.button_text_color }};
  }
</style>

<div class="social-bar">
  <p>Follow {{ settings.site_name }}</p>
  <div class="social-links">
    {% if settings.facebook_url %}<a href="{{ settings.facebook_url }}" class="social-link" target="_blank">Facebook</a>{% endif %}
    {% if settings.instagram_url %}<a href="{{ settings.instagram_url }}" class="social-link" target="_blank">Instagram</a>{% endif %}
    {% if settings.tiktok_url %}<a href="{{ settings.tiktok_url }}" class="social-link" target="_blank">TikTok</a>{% endif %}
    {% if settings.twitter_url %}<a href="{{ settings.twitter_url }}" class="social-link" target="_blank">X</a>{% endif %}
    {% if settings.youtube_url %}<a href="{{ settings.youtube_url }}" class="social-link" target="_blank">YouTube</a>{% endif %}
  </div>
</div>
```

#### Competition Cards with Live Countdowns <a href="#complete-card-with-dynamic-components" id="complete-card-with-dynamic-components"></a>

**Variables needed:**

* `products` (Products from database): **Use Filters**, **Status** Active Only, **Limit** 6

**Template:**

```html
<style>
  .dynamic-card {
    background: #1a1a2e;
    border-radius: 12px;
    overflow: hidden;
  }
  .dynamic-card img {
    width: 100%;
    height: 200px;
    object-fit: cover;
  }
  .dynamic-card-body {
    padding: 20px;
  }
  .dynamic-card h3 {
    color: white;
    margin: 0 0 10px;
    font-size: 18px;
  }
  .dynamic-card .price {
    color: #4ade80;
    font-size: 24px;
    font-weight: bold;
  }
  .dynamic-card .countdown {
    display: flex;
    gap: 8px;
    color: #9ca3af;
    font-size: 14px;
    margin: 15px 0;
  }
  .dynamic-card .countdown span {
    background: #16213e;
    padding: 4px 8px;
    border-radius: 4px;
  }
  .dynamic-card .progress-bar {
    height: 8px;
    background: #374151;
    border-radius: 4px;
    overflow: hidden;
    margin-top: 15px;
  }
  .dynamic-card .progress-fill {
    height: 100%;
    background: #4ade80;
    border-radius: 4px;
    transition: width 0.3s ease;
  }
  .dynamic-card .stats {
    color: #9ca3af;
    font-size: 12px;
    margin-top: 8px;
  }
  .dynamic-card .badge {
    display: inline-block;
    background: #fbbf24;
    color: #000;
    padding: 4px 8px;
    border-radius: 4px;
    font-size: 12px;
    font-weight: bold;
    margin-top: 10px;
  }
  .dynamic-card .btn {
    display: block;
    margin-top: 15px;
    background: {{ settings.primary_color }};
    color: {{ settings.button_text_color }};
    padding: 12px;
    border-radius: 8px;
    text-decoration: none;
    font-weight: bold;
    text-align: center;
  }
  .dynamic-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 20px;
    max-width: 1200px;
    margin: 0 auto;
    padding: 40px 20px;
  }
</style>

<div class="dynamic-grid">
  {% for product in products %}
    <div class="dynamic-card">
      <img src="{{ product.image_url }}" alt="{{ product.title }}">
      <div class="dynamic-card-body">
        <h3>{{ product.title }}</h3>
        <p class="price">{{ product.price_display }}</p>

        <!-- Live Countdown Timer -->
        <div x-data="raffleCountdown({{ product.end_date_seconds }})" class="countdown">
          <span x-text="time.days + 'd'"></span>
          <span x-text="pad(time.hours) + 'h'"></span>
          <span x-text="pad(time.minutes) + 'm'"></span>
          <span x-text="pad(time.seconds) + 's'"></span>
        </div>

        <!-- Progress Bar -->
        <div x-data="raffleProgress({{ product.percent_sold }})" class="progress-bar">
          <div class="progress-fill" :style="'width:' + width + '%'"></div>
        </div>
        {% if product.simple_max_ticket %}
          <p class="stats">{{ product.percent_sold|round }}% sold</p>
        {% else %}
          <p class="stats">{{ product.sold_tickets }}/{{ product.max_tickets }} tickets sold</p>
        {% endif %}

        <!-- Instant Wins Badge -->
        {% if product.instant_wins > 0 %}
          <span class="badge">{{ product.instant_wins }} Instant Wins ({{ product.instant_wins_remaining }} left)</span>
        {% endif %}

        <a href="{{ product.url }}" class="btn">Enter Now</a>
      </div>
    </div>
  {% endfor %}
</div>
```

***

### Troubleshooting <a href="#troubleshooting" id="troubleshooting"></a>

<details>

<summary>I see a red "Template Error" box on my page</summary>

Only admins see this box. It contains a short description of what went wrong. The most common causes are:

* **A tag that was opened but never closed.** Every `{% if %}` needs an `{% endif %}`, and every `{% for %}` needs an `{% endfor %}`.
* **A missing bracket.** Check that every `{{` has a matching `}}`, and every `{%` has a matching `%}`.
* **A filter or feature that is not available.** Only the tags and filters listed in [Twig Templating](/advanced/twig-templating.md#what-twig-can-do-here) can be used.
* **Printing a whole list.** `{{ featured_products }}` or `{{ featured_products|first }}` cannot be printed on their own. Print a single field such as `{{ product.title }}` inside a `for` loop instead.
* **Three dots in your wording.** If the error mentions "Range operator", look for two or more full stops followed by more text, such as `Wait... there's more`. Replace them with the single ellipsis character (…) or rephrase the sentence.

Fix the template, click **Save** and refresh the page.

</details>

<details>

<summary>My variable shows nothing</summary>

* Check the spelling and capitals. The name in your template must match the **Variable Name** exactly.
* Make sure the variable has a value. An empty **Text** variable prints nothing.
* For **Products**, **Recent Winners** and **Latest Instant Winners**, you need a `for` loop. `{{ featured_products }}` on its own does not show a list.
* For **Current User**, print a field such as `{{ user.name }}`, not the variable on its own.

</details>

<details>

<summary>A competition is missing from my Products list</summary>

With **Use Filters** and **Active Only**, a competition only appears while it is switched to **Active**, has started, has not ended and has not sold out. It also must not be a **Hidden / VIP Competition**. Check your **Categories**, **Sliders Only** and **Limit** options too. If you have just changed the competition, wait a minute or two and refresh. See [Why a Competition Is Not on My Homepage](/customise-your-site/set-up-your-homepage/why-a-competition-is-not-on-my-homepage.md) for more checks.

</details>

<details>

<summary>My HTML shows as text on the page</summary>

Anything printed with `{{ ... }}` is shown as plain text, for safety. Put HTML tags in the template itself, not inside a Text or Text Area value.

</details>

<details>

<summary>The page will not save</summary>

Scroll through the block and look for a field outlined in red. Common causes are a **Variable Name** with a space or dash in it, two variables with the same name, a **URL** that does not start with `https://`, or a mistake in your **JSON Data**.

</details>

<details>

<summary>My section looks wrong on a phone</summary>

Use flexible layouts such as `grid-template-columns: repeat(auto-fit, minmax(260px, 1fr))` instead of a fixed number of columns, and avoid fixed widths in pixels on large elements. Always check the page on your own phone before you share it.

</details>

***

### Tips <a href="#tips" id="tips"></a>

* **Start simple.** Begin with one or two variables, check they show, then build up.
* **Use meaningful names.** `hero_headline` is clearer than `text1` when you come back in six months.
* **Use your brand colours.** `settings.primary_color` and `settings.button_text_color` keep your section matching the rest of your site.
* **Plan for empty lists.** Add `{% else %}` to your loops so the section still looks good when there are no competitions or winners to show.
* **Handle missing values** with the default filter, for example `{{ subtitle|default('') }}`.
* **Keep a copy.** Paste your finished template into a document on your computer, so you can restore it if it is ever changed by mistake.

### Need Help? <a href="#need-help" id="need-help"></a>

Copy this page (including the **AI Context** section below) and share it with an AI assistant, such as ChatGPT or Claude, along with a description or screenshot of what you want. It can write the template, list the variables you need to create, and help fix any **Template Error** messages. If you are still stuck, contact RaffleX support.

***

### AI Context <a href="#ai-context" id="ai-context"></a>

**For AI Assistants:** This documents the RaffleX Custom Twig Block for the page builder. Help users create custom HTML layouts. When you write a template, also tell the user exactly which variables to create (name, type and options).

**Block Capabilities:**

* User defines variables in the "Custom Variables" repeater (Variable Name, Type, value fields)
* User writes HTML + Twig in the "HTML + Twig Code" editor (in the collapsible "Twig Template" section)
* Output renders on the frontend. Template errors show only to admins.

**Variable names:** must match `^[a-zA-Z_][a-zA-Z0-9_]*$`, unique within the block, case sensitive. Do not use `settings`.

**Variable Types (admin label → template value):**

* Text → string
* Text Area → string (newlines are not converted to line breaks)
* Number → number (0 if empty)
* Boolean (Yes/No) → true/false
* Color → hex string (`#000000` if empty)
* Date & Time → string `Y-m-d H:i:s`, format with `|date()`
* URL → string (must be a full `https://` address)
* Image → full image URL string (empty if none)
* Gallery (up to 20 images) → array of `{url, name}` (name is a system generated file name)
* Custom Fields → associative array (`var.key` or `{% for key, value in var %}`)
* JSON (for loops) → decoded JSON array
* Products (from database) → array of products
* Recent Winners → array of winners
* Latest Instant Winners → array of instant winners
* Current User → single user object
* Club (membership tiers) → single club object (`enabled`, `name`, `url`, `tiers`, `main_competition`, `daily_draw`, `member`, `user`); only offered while the Club is on, empty otherwise. See [Club Variables](/advanced/twig-templating/club-variables.md)

**Products Variable:**

* Product Selection "Use Filters": Status (Active Only = active, started, not ended, not sold out; Inactive Only = switched off), Categories (multi, empty = all), Sliders Only toggle, Sort By (Ending Soon First, Ending Last First, Draw Date (Soonest), Draw Date (Latest), Price (Low to High), Price (High to Low), Title (A-Z), Title (Z-A), Newest First, Manual Sort Order), Limit (empty = no limit). Hidden / VIP competitions are excluded.
* Product Selection "Select Specific Products": "Select Products" multi select. Included regardless of status. Order not guaranteed.
* Fields: id, title, subtitle, slug, url (absolute, `/competition/{slug}`), image\_url, featured\_video\_url, video\_embed\_url, video\_embed\_type, video\_embed\_mime\_type, video\_embed\_orientation, slider\_image\_url, portrait\_image\_url, price, price\_raw, price\_display, original\_price, original\_price\_raw, is\_on\_sale, is\_free, has\_question, question, cash\_alt, cash\_alt\_raw, draw\_date, end\_date, start\_date, draw\_date\_raw, end\_date\_raw, start\_date\_raw, draw\_date\_seconds, end\_date\_seconds, start\_date\_seconds, has\_started, has\_ended (true when ended or sold out), is\_sold\_out, is\_active, is\_featured, max\_tickets, sold\_tickets, tickets\_remaining, percent\_sold, max\_per\_user (0 = no limit), simple\_max\_ticket (Percentage Only display), instant\_wins, instant\_wins\_remaining, game\_type, winner\_qty, auto\_draw, leaderboard\_enabled, is\_leaderboard\_only, category, categories, description, gallery (array of URLs)

**Recent Winners Variable:**

* Option: Number of Recent Winners (default 5)
* Fields: name, prize, product\_title, product\_url, product\_image, date (`M j, Y`), date\_ago

**Latest Instant Winners Variable:**

* Options: Number of Instant Winners (default 5, max 20), Minimum Payout Value (optional; prizes with no payout value always included)
* Only wins from the last 30 days
* Fields: winner\_name, prize, prize\_value (string with 2 decimals, no currency symbol, or empty), prize\_value\_raw, is\_site\_credit, product\_title, product\_url, product\_image, date, date\_ago

**Current User Variable:**

* Fields: id, name, last\_name, full\_name, display\_name ("Guest" when logged out), email, is\_logged\_in, balance (site credit, formatted), balance\_raw (site credit number, 0 for guests)

**Global:** `settings` (see [Settings Variables](/advanced/twig-templating/settings-variables.md))

**Available Twig Tags:** `if` (with `elseif`, `else`), `for` (with `else` and `loop`), `set`

**Available Filters:** `upper`, `lower`, `date`, `number_format`, `default`, `length`, `first`, `last`, `join`, `slice`, `round`, `trim`, `escape`. No functions, no `raw` filter.

**Dynamic Alpine Components:**

* `raffleCountdown(seconds)`: live countdown. Pass `end_date_seconds`, `draw_date_seconds` or `start_date_seconds`. Properties: `time.days`, `time.hours`, `time.minutes`, `time.seconds`, `hasEnded`, `pad(num)`
* `raffleProgress(percent)`: progress bar. Properties: `percent`, `width` (minimum 3% when above 0)
* `raffleQuickAdd(config)`: add to basket with quantity selector. Config: `productId`, `price` (use `price_raw`), `maxTickets` (default 100), `hasQuestion`, `hasEnded`. Properties/actions: `tickets`, `increment()`, `decrement()`, `addToCart()`, `loading`, `added`, `error`, `errorMessage`, `requiresLogin`, `canAdd`. Disabled when the competition has an entry question or has ended.
* `raffleBasket(config)`: header/footer templates only (needs `cart_count`, not available in this block)

**Key notes:**

1. `price` is always formatted with two decimals (e.g. "£1.99"). `price_display` is friendlier ("Free", "50p").
2. draw\_date, end\_date and start\_date are human readable strings.
3. Use `|default()` for optional fields. An empty Date & Time variable passed to `|date()` renders the current date, so wrap optional dates in `{% if %}`.
4. Output is HTML autoescaped. Never print a whole array (such as a Products or Current User variable on its own); print a field or loop instead.
5. Put CSS in a `<style>` block with unique class names. Twig variables work inside it. Do not rely on site utility classes or icon fonts (no FontAwesome); use inline SVG or emoji for icons.
6. Use `end_date_seconds` for countdowns and `end_date` for display.
7. Filtered product lists only include competitions that have started. Use `has_started` and `start_date_seconds` for a coming soon countdown on hand picked competitions.
8. Use responsive grids such as `repeat(auto-fit, minmax(260px, 1fr))`.

**Copyable Template Starter:**

```html
<style>
  .custom-section {
    padding: 40px 20px;
    background: #1a1a2e;
  }
  .custom-container {
    max-width: 1200px;
    margin: 0 auto;
  }
  .custom-title {
    color: white;
    font-size: 28px;
    text-align: center;
    margin-bottom: 30px;
  }
  .custom-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 20px;
  }
  .custom-card {
    background: #16213e;
    border-radius: 8px;
    padding: 20px;
    text-align: center;
  }
  .custom-card img {
    width: 100%;
    border-radius: 8px;
    margin-bottom: 15px;
  }
  .custom-card h3 {
    color: white;
    font-size: 18px;
    margin-bottom: 10px;
  }
  .custom-card .price {
    color: #4ade80;
    font-size: 20px;
    font-weight: bold;
    margin-bottom: 10px;
  }
  .custom-card .date {
    color: #9ca3af;
    font-size: 14px;
    margin-bottom: 15px;
  }
  .custom-card .btn {
    display: inline-block;
    background: {{ settings.primary_color }};
    color: {{ settings.button_text_color }};
    padding: 10px 20px;
    border-radius: 6px;
    text-decoration: none;
    font-weight: bold;
  }
</style>

<section class="custom-section">
  <div class="custom-container">
    <h2 class="custom-title">{{ title }}</h2>

    <div class="custom-grid">
      {% for product in products %}
        <div class="custom-card">
          <img src="{{ product.image_url }}" alt="{{ product.title }}">
          <h3>{{ product.title }}</h3>
          <p class="price">{{ product.price_display }}</p>
          <p class="date">{{ product.end_date }}</p>
          <a href="{{ product.url }}" class="btn">Enter Now</a>
        </div>
      {% else %}
        <p>New competitions are coming soon!</p>
      {% endfor %}
    </div>
  </div>
</section>
```

***

**See also:**

* [Settings Variables](/advanced/twig-templating/settings-variables.md) for the site settings available in all Twig templates
* [Custom Twig Product Grid](/advanced/twig-templating/custom-twig-product-grid.md) for building custom competition cards
* [Custom Twig Slider](/advanced/twig-templating/custom-twig-slider.md) for building a custom hero section
