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​