Skip to content

Template Cards

Use template scripts, TemplateHost, and onDisconnect for instance behavior.

Code

The following example assumes that you have imported the component and set up the theme.

Game Cards
<sv-template id="card-template">
<template>
<!-- Template script: runs on every instance -->
<script type="module">
const { refs, props, onDisconnect } = TemplateHost;
const detailsBtnHandler = () => {
alert(
`📜 ${props.name}` +
`\nClass: ${props.class ?? "Adventurer"}` +
`\nElement: ${props.element ?? "neutral"}` +
`\nHP: ${props.hp} | ATK: ${props.attack} | DEF: ${props.defense ?? 50}` +
`\nSkills: ${props.skills.join(", ")}` +
(props.lore ? `\n\n"${props.lore}"` : "")
);
};
refs.detailsBtn.addEventListener("click", detailsBtnHandler);
onDisconnect(() => {
refs.detailsBtn.removeEventListener("click", detailsBtnHandler);
});
</script>
<!-- Template markup -->
<article class="card" data-element="[[ element ?? 'neutral' ]]">
<!-- ternary: remove legendary badge if not legendary -->
<div class="legendary-badge" template-remove="[[ legendary ? false : true ]]">[[ legendary && "✦ LEGENDARY" ]]</div>
<div class="card-inner">
<header class="card-header">
<h2 class="card-name">[[ name ]]</h2>
<span class="card-class">[[ class ?? "Adventurer" ]]</span>
</header>
<div class="card-avatar">
<div class="avatar-ring"></div>
<div class="avatar-icon" title="[[ name ]]">[[ icon ]]</div>
</div>
<!-- ternary: legendary chars show their named origin, others show their element -->
<p class="card-origin">[[ legendary ? origin : element ?? "unknown" ]]</p>
<div class="card-stats">
<div class="stat">
<span class="stat-label">HP</span>
<span class="stat-bar-wrap">
<span class="stat-bar" style="--val: [[hp]]%"></span>
</span>
<span class="stat-value">[[ hp ]]</span>
</div>
<div class="stat">
<span class="stat-label">ATK</span>
<span class="stat-bar-wrap">
<span class="stat-bar atk" style="--val: [[attack]]%"></span>
</span>
<span class="stat-value">[[ attack ]]</span>
</div>
<div class="stat">
<span class="stat-label">DEF</span>
<span class="stat-bar-wrap">
<span class="stat-bar def" style="--val: [[defense ?? 50]]%"></span>
</span>
<span class="stat-value">[[ defense ?? 50 ]]</span>
</div>
</div>
<!-- repeat: one <li> cloned per entry in the skills array -->
<ul class="card-skills">
<li class="skill-tag" template-for=",skill of skills">[[ skill ]]</li>
</ul>
<p class="card-lore">[[ lore ?? "" ]]</p>
<!-- ternary: button label differs for legendary characters -->
<button type="button" class="card-btn" template-ref="detailsBtn">[[ legendary ? "View Legend" : "View Details" ]]</button>
</div>
</article>
</template>
<!-- Scoped style -->
248 collapsed lines
<style>
.card {
position: relative;
padding: 1em;
overflow: visible;
background: var(--sv-gray-6);
border: 1px solid var(--sv-gray-5);
border-radius: 16px;
transition:
transform 0.3s ease,
box-shadow 0.3s ease;
&::before {
position: absolute;
inset-block-start: -1px;
inset-inline: 16px;
block-size: 4px;
content: "";
background: linear-gradient(90deg, #0000, var(--sv-accent), #0000);
}
&:hover {
box-shadow:
0 0 0 1px var(--sv-accent),
0 0 32px var(--sv-accent-glow);
transform: translateY(-6px) scale(1.001);
}
&[data-element="fire"] {
--sv-accent: #ff6b35;
--sv-accent-glow: rgb(255 107 53 / 25%);
}
&[data-element="earth"] {
--sv-accent: #7dab57;
--sv-accent-glow: rgb(125 171 87 / 25%);
}
&[data-element="void"] {
--sv-accent: #b56eff;
--sv-accent-glow: rgb(181 110 255 / 25%);
}
&[data-element="lightning"] {
--sv-accent: #9f9036;
--sv-accent-glow: rgb(255 224 51 / 25%);
}
&[data-element="nature"] {
--sv-accent: #52d68a;
--sv-accent-glow: rgb(82 214 138 / 25%);
}
&[data-element="ice"] {
--sv-accent: #4eacc4;
--sv-accent-glow: rgb(99 216 245 / 25%);
}
}
.legendary-badge {
position: absolute;
inset-block-start: -12px;
inset-inline-start: 50%;
z-index: 2;
padding: 3px 14px;
font-family: Cinzel, serif;
font-size: 0.62rem;
font-weight: 700;
color: #1a1000;
letter-spacing: 0.15em;
white-space: nowrap;
background: linear-gradient(90deg, #7a5a1a, var(--gold), #7a5a1a);
border-radius: 20px;
box-shadow: 0 0 14px var(--gold-glow);
transform: translateX(-50%);
}
.card-inner {
display: flex;
flex-direction: column;
gap: 1em;
justify-content: space-between;
block-size: 100%;
}
.card-header {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.card-name {
font-family: Cinzel, serif;
font-size: 1.3rem;
font-weight: 700;
letter-spacing: 0.06em;
}
.card-class {
font-size: 0.82rem;
color: var(--sv-accent);
text-transform: uppercase;
letter-spacing: 0.12em;
opacity: 0.85;
}
.card-avatar {
position: relative;
align-self: center;
inline-size: 72px;
block-size: 72px;
}
.avatar-ring {
position: absolute;
inset: 0;
border: 2px solid var(--sv-accent);
border-radius: 50%;
opacity: 0.6;
animation: pulse 3s ease-in-out infinite;
}
.avatar-icon {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 2.2rem;
background: var(--sv-gray-7);
border-radius: 50%;
}
.card-origin {
font-size: 0.78rem;
font-style: italic;
color: var(--gold);
text-align: center;
letter-spacing: 0.06em;
opacity: 0.8;
}
.card-stats {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.stat {
display: grid;
grid-template-columns: 34px 1fr 36px;
gap: 0.5rem;
align-items: center;
}
.stat-label {
font-family: Cinzel, serif;
font-size: 0.65rem;
color: var(--text-dim);
letter-spacing: 0.12em;
}
.stat-bar-wrap {
block-size: 5px;
overflow: hidden;
background: var(--sv-gray-5);
border-radius: 999px;
}
.stat-bar {
display: block;
inline-size: var(--val, 0%);
block-size: 100%;
background: var(--sv-accent);
border-radius: 999px;
transition: width 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
.stat-bar.atk {
background: linear-gradient(90deg, var(--sv-accent), #ff9a6c);
}
.stat-bar.def {
background: linear-gradient(90deg, var(--sv-accent), #7ed4ff);
}
.stat-value {
font-size: 0.8rem;
font-variant-numeric: tabular-nums;
color: var(--text-dim);
text-align: end;
}
.card-skills {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
padding: 0;
list-style: none;
}
.skill-tag {
padding: 0.2rem 0.6rem;
font-size: 0.7rem;
color: var(--sv-accent);
letter-spacing: 0.08em;
background: color-mix(in srgb, var(--sv-accent) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--sv-accent) 35%, transparent);
border-radius: 999px;
}
.card-lore {
min-block-size: 1.4rem;
padding-inline-start: 0.65rem;
font-size: 0.9rem;
font-style: italic;
line-height: 1.55;
color: var(--text-dim);
border-inline-start: 2px solid var(--sv-accent);
opacity: 0.85;
&:empty {
border-inline-start: none;
}
}
.card-btn {
align-self: flex-start;
padding: 0.4rem 1rem;
font-family: Cinzel, serif;
font-size: 0.72rem;
color: var(--sv-accent);
letter-spacing: 0.12em;
cursor: pointer;
background: transparent;
border: 1px solid var(--sv-accent);
border-radius: 6px;
transition:
background 0.2s,
color 0.2s,
box-shadow 0.2s;
&:hover {
color: var(--sv-gray-5);
background: var(--sv-accent);
box-shadow: 0 0 18px var(--sv-accent-glow);
}
}
</style>
</sv-template>
<div class="content"></div>
<!-- Create cards -->
<script>
89 collapsed lines
{
/**
* @typedef {Object} CharacterData
* @property {string} name
* @property {string} icon
* @property {string} [class]
* @property {string} [element]
* @property {string} [origin]
* @property {number} hp
* @property {number} attack
* @property {number} [defense]
* @property {string[]} skills
* @property {string} [lore]
* @property {boolean} [legendary]
*/
/** @type {CharacterData[]} */
const characters = [
{
name: "Seraphel",
icon: "🔥",
class: "Arcane Knight",
element: "fire",
origin: "Dying sun of Astreth",
hp: 88,
attack: 95,
defense: 72,
skills: ["Solar Flare", "Ember Shield", "Ashen Blade"],
lore: "Forged in the dying sun of Astreth, she carries its last ember.",
legendary: true,
},
{
name: "Vorrk",
icon: "🪨",
class: "Stonewarden",
element: "earth",
hp: 99,
attack: 60,
defense: 97,
skills: ["Granite Wall", "Tremor"],
lore: "His shield has never been broken. His spirit never will.",
},
{
name: "Nyxara",
icon: "🌑",
class: "Shadow Dancer",
element: "void",
origin: "The Between",
hp: 65,
attack: 91,
defense: 44,
skills: ["Blink Strike", "Void Step", "Eclipse"],
lore: "She moves between heartbeats, unseen until it's too late.",
legendary: true,
},
{
name: "Thalion",
icon: "",
element: "lightning",
hp: 78,
attack: 83,
skills: ["Chain Lightning", "Static Dash"],
},
{
name: "Mireille",
icon: "🌿",
class: "Grove Sage",
element: "nature",
hp: 72,
attack: 55,
defense: 68,
skills: ["Regrowth", "Thorn Bind", "Sporesong"],
lore: "She speaks to roots older than memory.",
},
{
name: "Caldris",
icon: "❄️",
class: "Frost Arbiter",
element: "ice",
origin: "The Shattered Shelf",
hp: 81,
attack: 77,
defense: 80,
skills: ["Glacial Rift", "Permafrost", "Ice Mirror"],
legendary: true,
},
];
/** @type {Template} */
const cardTemplate = document.querySelector("#card-template");
/** @type {HTMLElement} */
const content = document.querySelector(".content");
for (const charData of characters) {
const templateInstance = cardTemplate.create(charData);
content.append(templateInstance);
}
}
</script>
<style>
24 collapsed lines
.content {
--gold: #c9a84c;
--gold-glow: rgb(201 168 76 / 35%);
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 2em;
padding: 1em;
margin: 0 auto;
}
@keyframes pulse {
0%,
100% {
opacity: 0.6;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.1);
}
}
</style>