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.
Recommended vite.config.js
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
| Mistake | Consequence | Fix |
|---|---|---|
Missing var(…, fallback) on CSS custom props | Theme 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-container | Double-card shadow effect | Put all card chrome on .shk-theme__app-card only |
Missing meta.color_scheme in theme.config.json | All app iframes default to light mode | Always set "color_scheme": "dark" or "light" |
Missing config.iframe in theme.config.json | No CSS vars emitted in dev preview | Always include config.iframe |
Using absolute paths in index.html for assets | 404 in dev preview proxy | Use ./-relative paths for all theme assets |
Not calling broadcastCardStyle() in DOMContentLoaded | Apps already on page miss initial card style | Always broadcast on load |
Styling .shk-consent-placeholder | Visual conflicts with platform-owned consent UI | Never style this element |
Next Steps
- Theme App-Card Integration — full card-style and button-style broadcast protocol with advanced examples
- Theme Dev Preview — dev preview path resolution, SCSS fallbacks, social icons
- Theme Settings — how to expose settings to the profile owner dashboard