---
title: Render an experience with tag helpers
slug: cms-13/docs/render-an-experience-with-tag-helpers
docTags: 
createdAt: 2026-07-30T20:22:08.256Z
---

An experience is a page-level composition created in the Visual Builder. It organizes content into a tree of sections, rows, columns, and components. Optimizely CMS provides a set of tag helpers for rendering experiences declaratively in Razor views.

:::hint{type="info"}
**Note**
Microsoft is no longer investing in the Html Helpers technology for building templates, and they have removed the related documentation from recent versions. You should also be aware that Microsoft may eventually discontinue support for Html Helper technology altogether.
:::

## Prerequisites

1. Install the `EPiServer.CMS.AspNetCore.TagHelpers` package.
2. Register tag helper dependencies in `Startup.cs`:

```csharp
services.AddCmsTagHelpers();
```

3. Add the tag helper directive to `Views/_ViewImports.cshtml`:

```csharp
@addTagHelper *, EPiServer.Cms.AspNetCore.TagHelpers
```

:::hint{type="info"}
**Note**
For working with Visual Builder pages and on-page editing (OPE), see [Render properties with tag helpers](docId:5M4kNNWFrgXJyeHAE7BKD).
:::

## Experience composition structure

An experience consists of a tree of composition nodes:

```javascript
Experience (root)
  +-- Section (grid layout)
  |     +-- Row
  |     |     +-- Column
  |     |           +-- Component (a block/content item)
  |     +-- Row
  |           +-- Column
  |                 +-- Component
  +-- Component (standalone, outside a grid)
```

The following tag helpers correspond to each node type:

| Tag Helper        | Default HTML | Description                                              |
| ----------------- | ------------ | -------------------------------------------------------- |
| `<epi-outline>`   | `<div>`      | Root wrapper. Renders the entire experience composition. |
| `<epi-grid>`      | `<div>`      | Renders a section node with grid layout.                 |
| `<epi-row>`       | `<div>`      | Renders a row inside a grid.                             |
| `<epi-column>`    | `<div>`      | Renders a column inside a row.                           |
| `<epi-component>` | `<div>`      | Renders a component (block) within the composition.      |
| `<epi-styles>`    | None         | Declares CSS class mappings for display templates.       |
| `<epi-style>`     | None         | Maps a display setting name/value pair to CSS classes.   |
| `<epi-style-map>` | None         | Alternative mapping syntax inside `<epi-style>`.         |

## Render an experience with tag helpers

The simplest way to render an experience is to use `<epi-outline>` with the grid tag helpers nested inside:

```csharp
@model IPageViewModel<ExperienceData>

<epi-outline class="experience" experience="@Model.CurrentPage">
    <epi-grid>
        <epi-row>
            <epi-column>
                <epi-component />
            </epi-column>
        </epi-row>
    </epi-grid>
    <epi-component />
</epi-outline>
```

The `experience` attribute accepts a model expression pointing to your `ExperienceData`. If the model is `null`, the tag helper renders nothing.

**How the rendering works:**

1. `<epi-outline>` converts the `ExperienceData` into a `Composition` tree and iterates over top-level nodes.
2. For each **section** node, it tries to resolve a Razor partial view (display template) for the section.
   - If the framework finds none, the renderer falls back to the tag helper template declared inside `<epi-outline>` (the `<epi-grid>` block).
   - If no inline template exists, the framework renders the default grid layout.
3. For each **component** node, it renders the component using the standard Optimizely template resolution, honoring the `DisplayTemplateKey` from the composition node.
4. In edit mode, `data-epi-block-id` attributes are automatically added to enable Visual Builder editing. No additional markup is needed.

### Customize the HTML tag

All composition node tag helpers render as `<div>` by default. Use the `tag-name` attribute to change the output element:

```html
<epi-outline tag-name="section" experience="@Model.CurrentPage">
    <epi-grid tag-name="main">
        <epi-row tag-name="section">
            <epi-column tag-name="article">
                <epi-component tag-name="aside" />
            </epi-column>
        </epi-row>
    </epi-grid>
</epi-outline>
```

## Display templates

The rendering engine uses the standard Optimizely template resolution to find views for sections and components. When a node has a `DisplayTemplateKey`, the framework tries to find an MVC partial view tagged with that key. For a detailed explanation of how the `TemplateResolver` selects templates based on content type, tags, and inheritance, see [Select templates](docId\:X6jWKJqMhlD1rFhW_y6u6).

### Section templates

1. The renderer checks if the section's `DisplayTemplateKey` is set.
2. If set, it resolves a partial view registered with that tag (for example, via `[TemplateDescriptor]`).
3. If a matching view is found, it renders that view with the section's `SectionData` as the model.
4. If no matching view is found, it falls back to the inline tag helper template (`<epi-grid>` block inside `<epi-outline>`).
5. If no inline template is declared, the framework renders a built-in default grid layout..

To create a section display template, add a Razor partial view for your `SectionData` type.

```csharp
[ContentType]
public class MySection : SectionData
{
    [Display(GroupName = SystemTabNames.Content)]
    [CultureSpecific]
    public virtual string Title { get; set; }

    [Display(Name = "Short description", GroupName = SystemTabNames.Content)]
    public virtual string Description { get; set; }
}
```

Then create the corresponding Razor view:

```csharp
@* Views/Shared/MySection.cshtml *@
@model MySection

<epi-grid class="container section">
    <h4 epi-property="@Model.Title">@Model.Title</h4>
    <p epi-property="@Model.Description">@Model.Description</p>

    <epi-styles>
        <epi-style name="margin">
            <epi-style-map value="top" class="mt-3"/>
            <epi-style-map value="bottom" class="mb-5"/>
            <epi-style-map value="both" class="mt-3 mb5"/>
        </epi-style>
    </epi-styles>
    <epi-row class="row">
        <epi-column class="col-sm-12 col-md mb-4 mb-md-0">
            <epi-component/>
        </epi-column>
    </epi-row>
</epi-grid>
```

The framework resolves the view automatically by convention (matching the content type name). You can also register additional section templates explicitly using `IViewTemplateModelRegistrator` (see [Component templates](docId:2rCTqetHmi7zDZvAJqLwj) below).

### View file naming convention for display tags

In addition to the default view (`{ContentTypeName}.cshtml`), you can create tag-specific views by appending the display tag name to the content type name with a dot separator:

```javascript
{ContentTypeName}.{DisplayTag}.cshtml
```

When the framework renders a component or section, it checks for a display tag (from the `DisplayTemplateKey` or the view context). If a tag is present, the renderer first looks for a view named `{ContentTypeName}.{DisplayTag}.cshtml`. If no matching view is found, it falls back to the default `{ContentTypeName}.cshtml` view.

For example, given a `BlankSection` content type and a display tag `CardSection`, create a view file named `BlankSection.CardSection.cshtml`:

```csharp
@* Views/Shared/BlankSection.CardSection.cshtml *@
@model BlankSection

<epi-grid class="container section card">
    <epi-styles>
        <epi-style name="margin">
            <epi-style-map value="top" class="mt-3"/>
            <epi-style-map value="bottom" class="mb-5"/>
            <epi-style-map value="both" class="mt-3 mb5"/>
        </epi-style>
        <epi-style name="color">
            <epi-style-map value="primary" class="primary"/>
            <epi-style-map value="secondary" class="secondary"/>
        </epi-style>
    </epi-styles>
    <epi-row class="row">
        <div class="col-sm-12 col-md mb-4 mb-md-3">
            <epi-column class="card">
                <epi-styles>
                    <epi-style name="color" value="primary" class="primary"/>
                    <epi-style name="color" value="secondary" class="secondary"/>
                </epi-styles>
                <epi-component/>
            </epi-column>
        </div>
    </epi-row>
</epi-grid>
```

This view renders when a `BlankSection` is assigned the display template key `"CardSection"` in the Visual Builder, while the default `BlankSection.cshtml` renders when no display tag is set.

You can create multiple tagged views for the same content type without any code registration:

```javascript
Views/Shared/
├── BlankSection.cshtml              ← default (no tag)
├── BlankSection.CardSection.cshtml  ← tag "CardSection"
└── BlankSection.FullWidth.cshtml    ← tag "FullWidth"
```

:::hint{type="info"}
**Note**
This naming convention works alongside `IViewTemplateModelRegistrator`. If you need additional control (such as setting `AvailableWithoutTag` or specifying multiple tags for the same view), use the programmatic registration approach described in the [Component templates](docId:2rCTqetHmi7zDZvAJqLwj) section.
:::

### Component templates

Components follow the same template resolution pattern. Each `ComponentNode` has a `Component` property (the actual `IContentData` block) and an optional `DisplayTemplateKey`.

1. If `DisplayTemplateKey` is set, the framework resolves a partial view tagged with that key for the component's content type.
2. The matched view is rendered with the block as the model.
3. If no tagged view is found, the framework uses the standard default view.

The default view is resolved by convention from its filename matching the content type name:

```csharp
@* Views/Shared/Blocks/TeaserBlock.cshtml (default template) *@
@model TeaserBlock

<div>
    <div class="img-wrapper mb-3" epi-property="@Model.Image">
        <img epi-property="@Model.Image" />
    </div>
    <h3 epi-property="@Model.Heading">@Model.Heading</h3>
    <p epi-property="@Model.Text">@Model.Text</p>
</div>
```

A wide variant is defined in a separate view file:

```csharp
@* Views/Shared/Blocks/TeaserBlockWide.cshtml (wide template) *@
@model TeaserBlock

<div class="row align-items-center">
    <div class="col-sm-12 col-lg-6" epi-property="@Model.Image">
        <img class="pb-3 pb-lg-0" epi-property="@Model.Image" />
    </div>
    <div class="col-sm-12 col-lg-6 text-lg-start">
        <h2 epi-property="@Model.Heading">@Model.Heading</h2>
        <p epi-property="@Model.Text">@Model.Text</p>
    </div>
</div>
```

Register the wide template with tags using `IViewTemplateModelRegistrator` in a `TemplateCoordinator`:

```csharp
[ServiceConfiguration(typeof(IViewTemplateModelRegistrator))]
public class TemplateCoordinator : IViewTemplateModelRegistrator
{
    public void Register(TemplateModelCollection viewTemplateModelRegistrator)
    {
        viewTemplateModelRegistrator.Add(typeof(TeaserBlock), new TemplateModel
        {
            Name = "TeaserBlockWide",
            Tags = ["wide", "full"],
            AvailableWithoutTag = false,
        });
    }
}
```

When a component in the Visual Builder is assigned the display template key `"wide"`, the `TeaserBlockWide` partial is rendered instead of the default.

## Style display settings with CSS classes

Display settings are key-value pairs configured in the Visual Builder (for example, `margin=top`, `padding=large`). The style tag helpers let you map these values to CSS classes declaratively.

### Declare a style section with `<epi-styles>`

Use `<epi-styles>` inside `<epi-outline>` (or any composition tag helper) to declare CSS classes and display setting mappings. It produces no HTML output. It only configures styling for the composition nodes.

| Attribute | Required | Description                                                                                          |
| --------- | -------- | ---------------------------------------------------------------------------------------------------- |
| `name`    | No       | The `DisplayTemplateKey` this style section applies to. If omitted, applies as the default fallback. |
| `class`   | No       | CSS classes to always add to nodes matching this template.                                           |

```csharp
<epi-outline experience="@Model.CurrentPage">
    <!-- Default styles (fallback when no named match is found) -->
    <epi-styles class="experience" />

    <!-- Styles for the "dark" display template -->
    <epi-styles name="dark" class="experience dark-theme" />

    <epi-grid>
        <epi-row>
            <epi-column>
                <epi-component />
            </epi-column>
        </epi-row>
    </epi-grid>
</epi-outline>
```

**Fallback behavior:** When a composition node has a `DisplayTemplateKey` of `"dark"`, the `<epi-styles name="dark">` section is used. If a node has no template key, or its key does not match any named `<epi-styles>`, the unnamed (default) section is used as a fallback.

### Map display settings with `<epi-style>`

Use `<epi-style>` inside `<epi-styles>` to map specific display setting values to CSS classes. You can use self-closing syntax for single mappings:

```csharp
<epi-styles class="experience">
    <epi-style name="padding" value="none" class="p-0" />
    <epi-style name="padding" value="small" class="p-3" />
    <epi-style name="padding" value="large" class="p-9" />
    <epi-style name="margin" value="top" class="mt-3" />
    <epi-style name="margin" value="bottom" class="mb-3" />
    <epi-style name="margin" value="both" class="mt-3 mb-3" />
</epi-styles>
```

| Attribute | Required                | Description                                                   |
| --------- | ----------------------- | ------------------------------------------------------------- |
| `name`    | Yes                     | The display setting name to match (for example, `"padding"`). |
| `value`   | Yes (self-closing mode) | The display setting value to match (for example, `"small"`).  |
| `class`   | Yes (self-closing mode) | Space-separated CSS classes to apply when matched.            |

### Group mappings with `<epi-style-map>`

For grouping multiple value mappings under a single display setting name, use `<epi-style>` in normal (non-self-closing) mode with `<epi-style-map>` children:

```csharp
<epi-styles class="experience">
    <epi-style name="margin">
        <epi-style-map value="top" class="mt-3" />
        <epi-style-map value="bottom" class="mb-3" />
        <epi-style-map value="both" class="mt-3 mb-3" />
    </epi-style>
    <epi-style name="padding">
        <epi-style-map value="none" class="p-0" />
        <epi-style-map value="small" class="p-3" />
        <epi-style-map value="large" class="p-9" />
    </epi-style>
</epi-styles>
```

Both syntaxes produce identical results.

### Per-template style mappings

You can define different CSS mappings for different display templates:

```csharp
<epi-outline experience="@Model.CurrentPage">
    <!-- Default template: standard spacing -->
    <epi-styles class="experience">
        <epi-style name="margin" value="top" class="mt-3" />
        <epi-style name="margin" value="bottom" class="mb-3" />
    </epi-styles>

    <!-- "dark" template: larger spacing -->
    <epi-styles name="dark" class="experience dark-theme">
        <epi-style name="margin" value="top" class="mt-5" />
        <epi-style name="margin" value="bottom" class="mb-5" />
    </epi-styles>

    <epi-grid>
        <epi-row>
            <epi-column>
                <epi-component />
            </epi-column>
        </epi-row>
    </epi-grid>
</epi-outline>
```

When a node has `DisplayTemplateKey = "dark"`, the framework applies the CSS classes from the `name="dark"` styles section. For nodes without a matching template key, the unnamed default section is used. If the same `name` appears in multiple `<epi-styles>` blocks, the framework merges their mapping rules.

## Render an experience with HTML helpers

For full control over the markup, you can bypass tag helpers and render the composition tree manually. This approach uses `ICompositionMapper` to convert `ExperienceData` to `Composition` model to access the composition tree.

:::hint{type="info"}
**Note**
You should use the [tag helper](docId:2rCTqetHmi7zDZvAJqLwj) approach for most scenarios because it handles edit mode attributes, template resolution, and style mapping automatically. Use the manual approach when you need complete control over the HTML structure.
:::

### Manual rendering example

The following example demonstrates manual rendering:

```csharp
@using EPiServer.Cms.VisualBuilder.Rendering
@using EPiServer.VisualBuilder.Compositions
@model ExperienceViewModel<HtmlHelperExperience>

@{
    var marginMap = new Dictionary<string, string>
    {
        { "top", "mt-3" },
        { "bottom", "mb-3" },
        { "both", "mt-3 mb-3" }
    };
}

@if (Model.Composition != null)
{
    <div class="experience @Model.Composition.GetTheme()">
        @foreach (var section in Model.Composition.Nodes)
        {
            <div class="container section @(section.GetCss("margin", marginMap))"
                 @Html.EditAttributes(section)>
                @if (section is SectionNode sectionNode)
                {
                    foreach (var row in sectionNode.Nodes)
                    {
                        <div class="row @row.GetCss("margin", marginMap)"
                             @Html.EditAttributes(row)>
                            @if (row is StructureNode rowNode)
                            {
                                foreach (var column in rowNode.Nodes)
                                {
                                    <div class="col-sm-12 col-md mb-4 mb-md-0
                                         @column.GetCss("margin", marginMap)"
                                         @Html.EditAttributes(column)>
                                        @if (column is StructureNode columnNode)
                                        {
                                            foreach (var component in columnNode.Nodes)
                                            {
                                                <div @Html.EditAttributes(component)>
                                                    @if (component is ComponentNode componentNode)
                                                    {
                                                        await Html.RenderContentDataAsync(
                                                            componentNode.Component,
                                                            false,
                                                            [componentNode.DisplayTemplateKey],
                                                            new { Node = componentNode });
                                                    }
                                                </div>
                                            }
                                        }
                                    </div>
                                }
                            }
                        </div>
                    }
                }
                else if (section is ComponentNode componentNode)
                {
                    await Html.RenderContentDataAsync(
                        componentNode.Component,
                        false,
                        [componentNode.DisplayTemplateKey],
                        new { Node = componentNode });
                }
            </div>
        }
    </div>
}
```

```csharp
    public static string GetTheme(this CompositionNode model)
    {
        if (model.DisplayTemplateKey == "MyExperience")
        {
            return "cool-experience";
        }

        return "";
    }

    public static string? GetCss(this CompositionNode node, string key, Dictionary<string, string> map, string? defaultCss = null)
    {
        if (node.DisplaySettings.TryGetValue(key, out var setting))
        {
            if (map.TryGetValue(setting, out var css))
            {
                return css;
            }
        }
        return defaultCss;
    }
```

### Key points for manual rendering

- Use `@Html.EditAttributes(node)` on every container element to support Visual Builder editing.
- Use `Html.RenderContentDataAsync()` to render individual components, passing the `DisplayTemplateKey` as a tag.
