Skip to content

Template Examples

sv-template is for pages written in plain HTML, where there is no framework around to repeat markup or fill it with data. If your page already runs one, use its templating instead.

The examples below assume that you have imported the component and set up the theme. They also import sv-expression-parser, which runs the expressions in a sandbox. Without it sv-template falls back to new Function.

Basic

A <sv-template> with an id holds the markup inside a <template> element. Anywhere else on the page, <sv-template use="id"> renders a copy of it with the data passed through prop-* attributes.

<!-- Definition -->
<sv-template id="basic-user">
<template>
<p class="basic-user">
<strong>[[ name ]]</strong>
<span>[[ role ]]</span>
</p>
</template>
</sv-template>
<!-- Instances -->
<div class="basic-users">
<sv-template prop-name="Alice" prop-role="Admin" use="basic-user"></sv-template>
<sv-template prop-name="Bob" prop-role="Member" use="basic-user"></sv-template>
</div>
<style>
.basic-users {
display: grid;
gap: 0.5em;
justify-items: center;
padding-block: 1em;
.basic-user {
display: flex;
gap: 0.6em;
align-items: baseline;
margin: 0;
span {
font-size: 0.85em;
color: var(--sv-gray-3);
}
}
}
</style>

Placeholders

[[ ... ]] holds an expression, evaluated against the data. It works in text and in attribute values, so the plan name and the disabled state below come from the same pro property. An expression that resolves to null removes the attribute.

prop-pro:json="true"
prop-pro:json="false"
<sv-template id="placeholder-seat">
<template>
<div class="seat">
<span class="seat-name">[[ name ]]</span>
<span class="seat-plan">[[ pro ? "Pro" : "Free" ]]</span>
<button type="button" class="seat-invite" disabled="[[ pro ? null : '' ]]">Invite</button>
</div>
</template>
</sv-template>
<div class="placeholder-seats">
<div class="variant">
<sv-template prop-name="Alice" prop-pro:json="true" use="placeholder-seat"></sv-template>
<span>prop-pro:json="true"</span>
</div>
<div class="variant">
<sv-template prop-name="Bob" prop-pro:json="false" use="placeholder-seat"></sv-template>
<span>prop-pro:json="false"</span>
</div>
</div>
<style>
.placeholder-seats {
display: grid;
gap: 1.5em;
justify-items: center;
padding-block: 1em;
.variant {
display: grid;
gap: 0.4em;
justify-items: center;
> span {
font-size: 0.8em;
color: var(--sv-gray-3);
}
}
.seat {
display: flex;
gap: 0.75em;
align-items: center;
padding: 0.5em 0.9em;
background-color: var(--sv-gray-6);
border-radius: var(--sv-bdr-rad-md);
}
.seat-plan {
font-size: 0.75em;
color: var(--sv-gray-3);
}
.seat-invite {
padding: 0.25em 0.9em;
font: inherit;
font-size: 0.85em;
color: var(--sv-accent-content);
cursor: pointer;
background-color: var(--sv-accent);
border: none;
border-radius: var(--sv-bdr-rad-sm);
&:disabled {
color: var(--sv-gray-3);
cursor: not-allowed;
background-color: var(--sv-gray-5);
}
}
}
</style>

Passing Data

prop-name passes a string, prop-name:json parses the value as JSON, and prop-name:js evaluates it as an expression against the properties read so far. A dot in the name sets a nested key, and json-data passes the whole object at once.

prop-* attributes
json-data
<sv-template id="props-plan">
<template>
<ul class="props-plan">
<li>
Plan:
<strong>[[ plan ]]</strong>
</li>
<li>
Seats:
<strong>[[ seats ]]</strong>
</li>
<li>
Billed yearly:
<strong>$[[ yearly ]]</strong>
</li>
<li>
Owner:
<strong>[[ owner.name ]]</strong>
</li>
</ul>
</template>
</sv-template>
<div class="props-plans">
<div class="variant">
<sv-template
prop-owner.name="Alice"
prop-plan="Team"
prop-seats:json="8"
prop-yearly:js="seats * 120"
use="props-plan"
></sv-template>
<span>prop-* attributes</span>
</div>
<div class="variant">
<sv-template json-data='{"plan":"Business","seats":24,"yearly":2880,"owner":{"name":"Bob"}}' use="props-plan"></sv-template>
<span>json-data</span>
</div>
</div>
<style>
.props-plans {
display: grid;
gap: 1.5em;
justify-items: center;
padding-block: 1em;
.variant {
display: grid;
gap: 0.4em;
justify-items: center;
> span {
font-size: 0.8em;
color: var(--sv-gray-3);
}
}
.props-plan {
display: grid;
gap: 0.3em;
min-inline-size: 14rem;
padding: 0.8em 1.2em;
margin: 0;
font-size: 0.9em;
list-style: none;
background-color: var(--sv-gray-6);
border-radius: var(--sv-bdr-rad-md);
}
}
</style>

Scoped Styles

A <style> next to the <template> is written once and scoped to instances of that template, so every copy gets it without duplicating the rules.

prop-tone="solid"
prop-tone="soft"
prop-tone="outline"
<sv-template id="styled-chip">
<template>
<span class="chip [[ tone ]]">[[ label ]]</span>
</template>
<!-- Written once, applied to every instance -->
<style>
.chip {
padding: 0.25em 0.9em;
font-size: 0.85em;
border-radius: 999px;
}
.chip.solid {
color: var(--sv-accent-content);
background-color: var(--sv-accent);
}
.chip.soft {
color: var(--sv-gray-1);
background-color: var(--sv-gray-5);
}
.chip.outline {
color: var(--sv-gray-1);
border: var(--sv-bdr-sz-sm) solid var(--sv-gray-4);
}
</style>
</sv-template>
<div class="styled-chips">
<div class="variant">
<sv-template prop-label="Shipped" prop-tone="solid" use="styled-chip"></sv-template>
<span>prop-tone="solid"</span>
</div>
<div class="variant">
<sv-template prop-label="In review" prop-tone="soft" use="styled-chip"></sv-template>
<span>prop-tone="soft"</span>
</div>
<div class="variant">
<sv-template prop-label="Draft" prop-tone="outline" use="styled-chip"></sv-template>
<span>prop-tone="outline"</span>
</div>
</div>
<style>
.styled-chips {
display: grid;
gap: 1.5em;
justify-items: center;
padding-block: 1em;
.variant {
display: grid;
gap: 0.4em;
justify-items: center;
> span {
font-size: 0.8em;
color: var(--sv-gray-3);
}
}
}
</style>

Repeating

template-for clones its element once per entry of the collection. The same template handles every list below: arrays, numbers, strings, and objects are all iterable sources.

an array
a number
a string
an object, values shown
<sv-template id="repeat-list">
<template>
<ul class="repeat-list">
<li template-for=",item of items">[[ item ]]</li>
</ul>
</template>
</sv-template>
<div class="repeat-lists">
<div class="variant">
<sv-template prop-items:json='["Draft", "In review", "Shipped"]' use="repeat-list"></sv-template>
<span>an array</span>
</div>
<div class="variant">
<sv-template prop-items:json="3" use="repeat-list"></sv-template>
<span>a number</span>
</div>
<div class="variant">
<sv-template prop-items="HTML" use="repeat-list"></sv-template>
<span>a string</span>
</div>
<div class="variant">
<sv-template prop-items:json='{"theme": "dark", "size": "lg"}' use="repeat-list"></sv-template>
<span>an object, values shown</span>
</div>
</div>
<style>
.repeat-lists {
display: grid;
gap: 1.5em;
justify-items: center;
padding-block: 1em;
.variant {
display: grid;
gap: 0.4em;
justify-items: center;
> span {
font-size: 0.8em;
color: var(--sv-gray-3);
}
}
.repeat-list {
display: flex;
gap: 0.4em;
padding: 0;
margin: 0;
list-style: none;
li {
padding: 0.25em 0.8em;
font-size: 0.85em;
background-color: var(--sv-gray-6);
border-radius: var(--sv-bdr-rad-sm);
}
}
}
</style>

Removing Elements

template-remove drops its element when the expression is true. Use it for badges, notes, and anything else that only some instances have.

prop-beta:json="true"
no beta, with a note
<sv-template id="remove-feature">
<template>
<div class="feature">
<span class="feature-name">[[ name ]]</span>
<span class="feature-badge" template-remove="[[ !beta ]]">Beta</span>
<span class="feature-note" template-remove="[[ note == undefined ]]">[[ note ]]</span>
</div>
</template>
</sv-template>
<div class="remove-features">
<div class="variant">
<sv-template prop-beta:json="true" prop-name="Webhooks" use="remove-feature"></sv-template>
<span>prop-beta:json="true"</span>
</div>
<div class="variant">
<sv-template prop-name="Exports" prop-note="Team plans only" use="remove-feature"></sv-template>
<span>no beta, with a note</span>
</div>
</div>
<style>
.remove-features {
display: grid;
gap: 1.5em;
justify-items: center;
padding-block: 1em;
.variant {
display: grid;
gap: 0.4em;
justify-items: center;
> span {
font-size: 0.8em;
color: var(--sv-gray-3);
}
}
.feature {
display: flex;
gap: 0.6em;
align-items: center;
min-inline-size: 14rem;
padding: 0.5em 0.9em;
background-color: var(--sv-gray-6);
border-radius: var(--sv-bdr-rad-md);
}
.feature-badge {
padding-inline: 0.5em;
font-size: 0.7em;
color: var(--sv-accent-content);
background-color: var(--sv-accent);
border-radius: 999px;
}
.feature-note {
margin-inline-start: auto;
font-size: 0.75em;
color: var(--sv-gray-3);
}
}
</style>

Refs and Scripts

A <script> inside the <template> runs once per instance, with TemplateHost pointing at the <sv-template> that rendered it. template-ref names the elements it needs, and onDisconnect cleans up when the instance leaves the DOM. Each counter below keeps its own state.

<sv-template id="refs-counter">
<template>
<!-- Runs once per instance -->
<script type="module">
const { props, refs, onDisconnect } = TemplateHost;
let count = props.start;
const increment = () => {
count++;
refs.output.textContent = count;
};
refs.button.addEventListener("click", increment);
onDisconnect(() => {
refs.button.removeEventListener("click", increment);
});
</script>
<div class="counter">
<span class="counter-label">[[ label ]]</span>
<output class="counter-output" template-ref="output">[[ start ]]</output>
<button type="button" class="counter-button" template-ref="button">Add</button>
</div>
</template>
</sv-template>
<div class="refs-counters">
<sv-template prop-label="Cart" prop-start:json="0" use="refs-counter"></sv-template>
<sv-template prop-label="Wishlist" prop-start:json="2" use="refs-counter"></sv-template>
</div>
<style>
.refs-counters {
display: grid;
gap: 0.75em;
justify-items: center;
padding-block: 1em;
.counter {
display: flex;
gap: 0.75em;
align-items: center;
inline-size: 15rem;
padding: 0.5em 0.9em;
background-color: var(--sv-gray-6);
border-radius: var(--sv-bdr-rad-md);
}
.counter-output {
margin-inline-start: auto;
font-variant-numeric: tabular-nums;
color: var(--sv-gray-3);
}
.counter-button {
padding: 0.25em 0.9em;
font: inherit;
font-size: 0.85em;
color: var(--sv-accent-content);
cursor: pointer;
background-color: var(--sv-accent);
border: none;
border-radius: var(--sv-bdr-rad-sm);
}
}
</style>

Rendering in JavaScript

create(data) returns an instance element that renders once it is in the DOM, which is the way to go for data you already have in JavaScript. render(props) re-renders an instance, merging the given properties into the current ones.

<sv-template id="js-service">
<template>
<div class="service">
<span class="service-name">[[ name ]]</span>
<span class="service-status">[[ status ]]</span>
</div>
</template>
</sv-template>
<div class="js-services">
<div class="js-services-list"></div>
<button type="button" class="js-services-reset">Mark all operational</button>
</div>
<script type="application/x-typescript" module>
const template = document.querySelector<Template>("#js-service")!;
const list = document.querySelector(".js-services-list")!;
const reset = document.querySelector(".js-services-reset")!;
const services = [
{ name: "API", status: "operational" },
{ name: "Database", status: "degraded" },
{ name: "CDN", status: "maintenance" },
];
const instances = services.map(service => template.create(service));
list.append(...instances);
reset.addEventListener("click", () => {
for (const instance of instances) {
instance.render({ status: "operational" });
}
});
</script>
<style>
.js-services {
display: grid;
gap: 0.75em;
justify-items: center;
padding-block: 1em;
.js-services-list {
display: grid;
gap: 0.4em;
inline-size: 16rem;
}
.service {
display: flex;
gap: 0.75em;
align-items: center;
padding: 0.5em 0.9em;
background-color: var(--sv-gray-6);
border-radius: var(--sv-bdr-rad-md);
}
.service-status {
margin-inline-start: auto;
font-size: 0.8em;
color: var(--sv-gray-3);
}
.js-services-reset {
padding: 0.3em 1em;
font: inherit;
font-size: 0.85em;
color: var(--sv-gray-1);
cursor: pointer;
background-color: var(--sv-gray-5);
border: none;
border-radius: var(--sv-bdr-rad-sm);
}
}
</style>

Updating Props

Changing use, json-data, or any prop-* attribute on a rendered instance replaces its content with a fresh render.

<sv-template id="update-build">
<template>
<p class="build">
Build
<strong>#[[ id ]]</strong>
<span class="build-status">[[ status ]]</span>
</p>
</template>
</sv-template>
<div class="update-panel">
<sv-template class="update-instance" prop-id="482" prop-status="queued" use="update-build"></sv-template>
<div class="update-actions">
<button type="button" data-status="queued">queued</button>
<button type="button" data-status="running">running</button>
<button type="button" data-status="passed">passed</button>
</div>
</div>
<script type="application/x-typescript" module>
const instance = document.querySelector<Template>(".update-instance")!;
for (const button of document.querySelectorAll<HTMLButtonElement>(".update-actions button")) {
button.addEventListener("click", () => {
const status = button.dataset.status;
if (!status) return;
instance.setAttribute("prop-status", status);
});
}
</script>
<style>
.update-panel {
display: grid;
gap: 0.75em;
justify-items: center;
padding-block: 1em;
.build {
display: flex;
gap: 0.6em;
align-items: center;
margin: 0;
}
.build-status {
padding: 0.15em 0.7em;
font-size: 0.8em;
color: var(--sv-accent-content);
background-color: var(--sv-accent);
border-radius: 999px;
}
.update-actions {
display: flex;
gap: 0.4em;
button {
padding: 0.3em 0.9em;
font: inherit;
font-size: 0.85em;
color: var(--sv-gray-1);
cursor: pointer;
background-color: var(--sv-gray-5);
border: none;
border-radius: var(--sv-bdr-rad-sm);
}
}
}
</style>