Skip to main content

HTML Theme Guide

This guide walks through building a complete HTML theme from scratch — from folder structure to production build to dev preview.


What an HTML Theme Is

An HTML theme is a theme where theme.config.json sets "type": "html". The main entry is an .html file containing [[template]] placeholders that the platform replaces with live user data before serving the page.

A small companion JavaScript file (src/index.js) handles:

  • Applying CSS custom properties from the live theme config
  • Broadcasting card style to installed app iframes
  • Responding to dashboard live preview changes

Your SCSS/CSS handles all visual styling.


Full Project Structure

my-theme/
├── manifest.json
├── theme.config.json
├── icon.svg
├── thumbnail.jpg
├── header.jpg
├── index.html ← theme shell (platform replaces [[...]] at serve time)
├── src/
│ ├── index.js ← theme runtime (postMessage, CSS vars, card broadcast)
│ └── theme.scss ← all visual styles
├── dist/
│ ├── index.js ← built runtime (served by platform)
│ └── theme.css ← compiled CSS (served by platform)
└── build.js ← simple build script (or use Vite/esbuild)

The dist/ directory contains what the platform actually serves. Your index.html references ./dist/theme.css and ./dist/index.js with ./-relative paths so the dev preview proxy can rewrite them correctly.


index.html — Full Template

A complete production-quality theme shell:

<!DOCTYPE html>
<html lang="[[user.locale]]">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
[[layout.inline_styles]]
<link rel="stylesheet" href="./dist/theme.css">
</head>
<body class="shk-body">

<div class="shk-wallpaper" aria-hidden="true"></div>

<main class="shk-profile [[layout.article_class]]">

<!-- Avatar and cover image -->
[[IF settings.show_cover_image]]
<div class="shk-cover" style="background-image: url('[[user.cover_image_url]]')"></div>
[[/IF]]

<header class="shk-header">
[[IF settings.show_avatar]]
<img class="shk-avatar" src="[[user.avatar_url]]" alt="" loading="lazy">
[[/IF]]

<div class="shk-identity">
[[IF settings.show_title]]
<h1 class="shk-name">[[user.name]]</h1>
[[/IF]]

[[IF settings.show_nickname]]
<p class="shk-handle">@[[user.nickname]]</p>
[[/IF]]

[[IF settings.show_category]]
<p class="shk-category">[[user.category]]</p>
[[/IF]]

[[IF settings.show_description]]
<p class="shk-bio">[[user.description]]</p>
[[/IF]]
</div>

[[user.verification_badge]]
</header>

<!-- Social link icons -->
[[IF settings.show_links]]
<nav class="shk-links" aria-label="Social links">
[[links.markup]]
</nav>
[[/IF]]

<!-- Installed apps -->
[[IF settings.show_apps]]
<section class="shk-apps" aria-label="Apps">
[[apps.rows_markup]]
</section>
[[/IF]]

</main>

<!-- Platform-managed content (footnote, overlays) -->
[[IF settings.show_footnote]]
[[layout.after_markup]]
[[/IF]]

[[layout.outside_markup]]

<script src="./dist/index.js"></script>

</body>
</html>

src/index.js — Full Theme Runtime

import { createThemeBridge } from '@shuuka';

createThemeBridge({
/**
* 1. Declare your theme's default card visual styles.
* These guarantee apps always receive valid fallback CSS tokens
* even before the platform broadcasts dashboard settings.
*/
cardStyle: {
bg: 'rgba(255, 255, 255, 0.06)',
borderColor: 'rgba(255, 255, 255, 0.12)',
borderStyle: 'solid',
borderWidth: '1px',
radius: '16px',
shadow: '0 4px 24px rgba(0,0,0,0.20)',
},

/**
* 2. Declare your theme's default button styles.
* Apps inherit these if they use the `.shk-host-btn` classes.
*/
btnStyle: {
primaryBg: '#ffffff',
primaryColor: '#0a0a0f',
primaryRadius: '10px',
primaryBorder: 'none',
secondaryBg: 'transparent',
secondaryColor: '#ffffff',
secondaryBorder: '1px solid rgba(255, 255, 255, 0.20)',
}
});

src/theme.scss — Full CSS Architecture

// ============================================================
// Reset & Base
// ============================================================
*, *::before, *::after { box-sizing: border-box; }

html, body {
margin: 0;
padding: 0;
min-height: 100vh;
}

body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
color: #ffffff;
background: var(--wallpaper-color, #0a0a0f);
}

// ============================================================
// Wallpaper — background layer
// ============================================================
.shk-wallpaper {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;

background-color: var(--wallpaper-color, #0a0a0f);
background-image: var(--wallpaper-image, none);
background-size: cover;
background-position: center;

&::after {
content: '';
position: absolute;
inset: 0;
background: var(--wallpaper-overlay, rgba(0,0,0,0.40));
}
}

// ============================================================
// Profile layout
// ============================================================
.shk-profile {
position: relative;
z-index: 1;
max-width: 560px;
margin: 0 auto;
padding: 40px 16px 80px;
display: flex;
flex-direction: column;
gap: 28px;
}

// ============================================================
// Header
// ============================================================
.shk-cover {
width: 100%;
height: 180px;
border-radius: 16px;
background-size: cover;
background-position: center;
background-color: rgba(255,255,255,0.06);
}

.shk-header {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
text-align: center;
}

.shk-avatar {
width: 88px;
height: 88px;
border-radius: 50%;
object-fit: cover;
border: 2px solid rgba(255, 255, 255, 0.15);
background: rgba(255,255,255,0.05);
}

.shk-identity {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}

.shk-name {
font-size: 1.375rem;
font-weight: 700;
margin: 0;
letter-spacing: -0.02em;
}

.shk-handle {
font-size: 0.875rem;
opacity: 0.55;
margin: 0;
}

.shk-category {
font-size: 0.8rem;
opacity: 0.4;
margin: 0;
text-transform: uppercase;
letter-spacing: 0.06em;
}

.shk-bio {
font-size: 0.9rem;
line-height: 1.55;
opacity: 0.70;
margin: 8px 0 0;
max-width: 400px;
}

// ============================================================
// Links (social icon grid)
// ============================================================
.shk-links {
display: flex;
flex-wrap: wrap;
justify-content: var(--links-alignment, center);
gap: var(--links-spacing, 8px);
}

// ============================================================
// App cards
// ============================================================
.shk-apps {
display: flex;
flex-direction: column;
gap: 12px;
}

// Visual surface — ALL card chrome lives here
.shk-theme__app-card {
background: var(--app-card-bg, rgba(255, 255, 255, 0.06));
border: 1px solid var(--app-card-border-color, rgba(255, 255, 255, 0.10));
border-radius: var(--app-card-radius, 16px);
box-shadow: var(--app-card-shadow, 0 4px 24px rgba(0, 0, 0, 0.20));
overflow: visible;
transition: transform 0.28s ease, box-shadow 0.35s ease;
}

// Clip container — sizing and clipping ONLY, no visual styles
.shk-theme__app-card .billboard-app-container {
background: transparent !important;
border: none !important;
box-shadow: none !important;
border-radius: var(--app-card-radius, 16px) !important;
overflow: hidden !important;
}

Build Setup

We strongly recommend using Vite for theme development. The platform SDK's createThemeBridge relies on module resolution to import @shuuka.

You can use the vite-plugin-shuuka-theme (or a similar rollup copy plugin) to automatically package your manifest.json and assets into the dist/ directory alongside your compiled code.

Configure base: '' so asset paths remain ./-relative. The Dev Preview proxy uses this relativity to correctly rewrite asset domains.

import { defineConfig } from 'vite';

export default defineConfig({
base: '',
build: {
outDir: 'dist',
emptyOutDir: true,
}
});

Run your build using:

npm run build
# or explicitly
npx vite build

Sync with platform-apps Folder

When publishing a built-in theme (first-party), keep the resources/platform-apps/Themes/<name>/ copy in sync with your dev source:

# Verify in sync
diff resources/platform-apps/Themes/MyTheme/src/index.js ./src/index.js

# After any change, rebuild dist
cd resources/platform-apps/Themes/MyTheme
npm run build

The built dist/ is what the platform serves in production via ./-relative paths.


Common Mistakes

MistakeConsequenceFix
Missing var(…, fallback) on CSS custom propsTheme looks broken in Vite dev proxy (no CSS vars injected)Always add fallback values matching your theme.config.json defaults
Applying shadows to .billboard-app-containerDouble-card shadow effectPut all card chrome on .shk-theme__app-card only
Missing meta.color_scheme in theme.config.jsonAll app iframes default to light modeAlways set "color_scheme": "dark" or "light"
Missing config.iframe in theme.config.jsonNo CSS vars emitted in dev previewAlways include config.iframe
Using absolute paths in index.html for assets404 in dev preview proxyUse ./-relative paths for all theme assets
Not calling broadcastCardStyle() in DOMContentLoadedApps already on page miss initial card styleAlways broadcast on load
Styling .shk-consent-placeholderVisual conflicts with platform-owned consent UINever style this element

Next Steps