Skip to content

Pages and templates

Pages are the building blocks of uproot experiments. Each page represents a screen that participants see—instructions, questions, feedback, or results. Pages are defined as Python classes and rendered using HTML templates.

Defining a page

A page is a class that inherits from Page:

class Welcome(Page):
    pass

This minimal page displays the template Welcome.html from your app's templates folder. Every page needs a corresponding template file.

The page_order list

The page_order list defines which pages participants see and in what sequence:

page_order = [
    Welcome,
    Instructions,
    Task,
    Results,
]

Participants progress through pages in order. You can use SmoothOperators to randomize, repeat, or conditionally select pages.

Templates

Templates are HTML files that define what participants see. uproot uses Jinja2 for templating.

Template naming and location

By default, uproot looks for a template matching the page class name:

my_app/
├── __init__.py      # Contains class Welcome(Page)
└── Welcome.html     # Template for Welcome page

To use a custom template path:

class Welcome(Page):
    template = "shared/welcome.html"

Basic template structure

A typical template extends the base layout and defines content:

{% extends "_uproot/Page.html" %}

{% block content %}
<h1>Welcome to the experiment</h1>
<p>Thank you for participating.</p>
{% endblock %}

The _uproot/Page.html base template provides the form wrapper, navigation buttons, and styling. Your content goes in the content block.

Adding a form

To collect data, include form fields in your template:

{% extends "_uproot/Page.html" %}

{% block content %}
<h1>Your decision</h1>

{{ form.amount.label }}
{{ form.amount }}

{% endblock %}

See Collecting data with forms for details on defining form fields.

The context method

Use the context method to pass data from Python to your template:

class Results(Page):
    @classmethod
    def context(page, player):
        return dict(
            earnings=player.payoff,
            partner_choice=other_in_group(player).choice,
        )

Then use these variables in your template:

{% extends "_uproot/Page.html" %}

{% block content %}
<h1>Results</h1>
<p>You earned {{ earnings }} points.</p>
<p>Your partner chose: {{ partner_choice }}</p>
{% endblock %}

The context method receives page (the page class) and player (the current participant's data).

Built-in template variables

Every template has access to these variables:

Variable Description
player The current participant's data
form The form instance (if the page has fields)
page The page class
C Constants defined in your app's C class
session The current session
_("text") Translation function for internationalization

Accessing player data

<p>Your name: {{ player.name }}</p>
<p>Current round: {{ player.round }}</p>

Using constants

Define constants in your app:

class C:
    ENDOWMENT = 100
    EXCHANGE_RATE = 0.10

Use them in templates:

<p>You start with {{ C.ENDOWMENT }} points.</p>
<p>Each point is worth ${{ C.EXCHANGE_RATE }}.</p>

Static files

App-specific static files

Place static files (images, CSS, JavaScript) in a static/ folder within your app:

my_app/
├── __init__.py
├── Welcome.html
└── static/
    ├── diagram.png
    └── custom.css

Reference them using appstatic():

<img src="{{ appstatic('diagram.png') }}" alt="Diagram">
<link rel="stylesheet" href="{{ appstatic('custom.css') }}">

Project-wide static files

For files shared across apps, use a project-level static folder and projectstatic().

Conditional page display

Use the show method to conditionally display pages:

class BonusRound(Page):
    @classmethod
    def show(page, player):
        return player.score >= 80  # Only show if score is high enough

Pages where show returns False are skipped automatically.

Role-based pages

A common pattern for multiplayer experiments:

class ProposerDecision(Page):
    @classmethod
    def show(page, player):
        return player.role == "proposer"

class ResponderDecision(Page):
    @classmethod
    def show(page, player):
        return player.role == "responder"

Allowing back navigation

By default, participants can only move forward. To allow going back:

class Survey(Page):
    allow_back = True

This adds a "Back" button that lets participants revisit and change previous answers.

Note

Back navigation only re-displays pages—it doesn't undo any data changes or re-run page logic.

NoshowPage for logic-only pages

Sometimes you need to run code without displaying anything to participants. Use NoshowPage:

class CalculatePayoffs(NoshowPage):
    @classmethod
    def after_always_once(page, player):
        player.payoff = player.correct_answers * 10

NoshowPage runs its lifecycle methods but never renders a template. Use it for:

  • Calculating scores or payoffs
  • Initializing player data
  • Setting up randomization

See NoshowPage in the big5_short example

Page lifecycle methods

Pages have several methods that run at different points:

Method When it runs
show Before displaying—return False to skip the page
context Before rendering—return template variables
before_once Once per player, before first display
before_always_once Before each display
after_once Once per player, after first submission
after_always_once After each submission
before_next Just before advancing to the next page

Example: one-time initialization

class Task(Page):
    @classmethod
    def before_once(page, player):
        # Runs once when the player first sees this page
        player.start_time = time()

Example: cleanup after submission

class Task(Page):
    @classmethod
    def after_always_once(page, player):
        # Runs after each submission
        player.attempts += 1

See Page methods reference for the complete list.

JavaScript variables

To pass data to JavaScript, use the jsvars method:

class TradingGame(Page):
    @classmethod
    def jsvars(page, player):
        return dict(
            initial_price=player.price,
            max_trades=C.MAX_TRADES,
        )

Access these in your template's JavaScript:

<script>
const price = _uproot_js.initial_price;
const maxTrades = _uproot_js.max_trades;
</script>

Complete example

Here's a complete page with context, conditional display, and form handling:

class Offer(Page):
    allow_back = True

    fields = dict(
        amount=IntegerField(
            label="How much do you offer?",
            min=0,
            max=100,
        ),
    )

    @classmethod
    def show(page, player):
        return player.role == "proposer"

    @classmethod
    def context(page, player):
        return dict(
            endowment=C.ENDOWMENT,
            partner=other_in_group(player).name,
        )

    @classmethod
    def before_next(page, player):
        player.offer_made = True
{% extends "_uproot/Page.html" %}

{% block content %}
<h1>Make an offer</h1>

<p>You have {{ endowment }} points to split with {{ partner }}.</p>

{{ form.amount.label }}
{{ form.amount }}

{% endblock %}

See complete examples in uproot-examples