App Design and UX
Apps should feel native inside Shuuka, not bolted on.
Host Card Style — .shk-host-card
The cleanest way to make an internal container look exactly like the host theme's app card is to add the class shk-host-card to it. The platform injects the full card CSS — background, border, radius, shadow, hover lift, and all theme-defined effects such as spotlight gradients and floor-light animations — directly into your iframe as a <style> tag.
<!-- template.html — any internal card-like container -->
<article class="my-card shk-host-card">
…content…
</article>
You do not write any CSS for visual appearance. The class carries everything.
How it works
When your app initialises (during the new Shuuka() constructor), the SDK sends a shuuka:request_card_style postMessage to the parent page. The active theme responds with a shuuka:card_style message containing a cssText string — the complete compiled .shk-host-card CSS block including :hover, :active, and ::before / ::after pseudo-elements. The SDK injects this as <style id="shk-host-card-style"> into your document head automatically.
This works for all app types including Simple apps (runtime_kind: "simple") which do not call init() — the request is sent automatically at construction time.
This means:
- Your card inherits the exact same glass/glow/shadow/hover the theme applies to its own app cards.
- When the profile owner customises card settings in the dashboard, the update is re-broadcast and your card updates live without a reload.
- You never need to duplicate theme values or query CSS variables manually.
What cssText contains
The theme author controls exactly what CSS is generated. For the built-in Aria theme the injected block looks like this (values reflect the user's dashboard configuration):
.shk-host-card {
background: rgba(255, 248, 230, 0.045);
border: 1px solid rgba(201, 168, 76, 0.13);
border-radius: 18px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.60);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
position: relative;
overflow: hidden;
transition: transform 0.28s ease, box-shadow 0.35s ease, border-color 0.35s ease;
}
.shk-host-card::before {
/* overhead spotlight radial gradient */
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: radial-gradient(ellipse 90% 55% at 50% -5%, rgba(201,168,76,0.10) 0%, …);
pointer-events: none;
z-index: 1;
transition: background 0.35s ease;
}
.shk-host-card::after {
/* stage floor-light shimmer line */
content: "";
position: absolute;
bottom: 0; left: 50%;
width: 0; height: 1px;
transform: translateX(-50%);
background: linear-gradient(90deg, …gold shimmer…);
z-index: 2;
transition: width 0.50s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.shk-host-card:hover { transform: translateY(-3px); … }
.shk-host-card:hover::before { /* intensified spotlight */ }
.shk-host-card:hover::after { width: 85%; }
.shk-host-card:active { transform: translateY(0); }
Other themes produce their own CSS block. Your markup stays the same; only the injected styles change.
Fallback CSS variables
If the theme or SDK does not yet support cssText, four CSS variables are also set on :root inside your iframe as a secondary fallback:
| Variable | Description |
|---|---|
--shk-host-card-bg | card background |
--shk-host-card-radius | border radius |
--shk-host-card-shadow | box shadow |
--shk-host-card-border | full border shorthand |
You can reference these in your own CSS for structural defaults while cssText handles the visual layer:
.my-card {
border-radius: var(--shk-host-card-radius, 16px);
background: var(--shk-host-card-bg, transparent);
box-shadow: var(--shk-host-card-shadow, 0 4px 12px rgba(0,0,0,0.05));
border: var(--shk-host-card-border, 1px solid rgba(0,0,0,0.08));
}
Structural rules when using .shk-host-card
The class takes over all visual properties. Your element should:
- not set its own
background,box-shadow,border, orborder-radius - not set
overflow: visible(the class setsoverflow: hiddenfor pseudo-element containment) - add
position: relativeif your content uses absolutely-positioned children withz-index(the class already sets this, so it is inherited automatically)
Host Button Style — .shk-host-btn
The cleanest way to make a button inside your app look exactly like the active theme's primary button is to add the class shk-host-btn. The platform injects the full button CSS — background, color, border, hover lift, active state, disabled state, and all theme-defined effects such as glow and shadow — directly into your iframe as a <style> tag.
<!-- template.html — primary action button -->
<button class="my-submit shk-host-btn" type="button">Submit</button>
<!-- secondary / outlined button -->
<button class="my-cancel shk-host-btn shk-host-btn--secondary" type="button">Cancel</button>
You do not write any CSS for visual appearance. The classes carry everything.
How it works
When your app initialises (during the new Shuuka() constructor), the SDK sends a shuuka:request_button_style postMessage to the parent page. The active theme responds with a shuuka:button_style message containing a cssText string — the complete compiled .shk-host-btn CSS block including :hover, :active, and :disabled pseudo-classes. The SDK injects this as <style id="shk-host-btn-style"> into your document head automatically.
Seven CSS custom properties are also set on :root inside your iframe:
| Variable | Description |
|---|---|
--shk-btn-primary-bg | primary button background |
--shk-btn-primary-color | primary button text color |
--shk-btn-primary-radius | primary button border-radius |
--shk-btn-primary-border | primary button border shorthand |
--shk-btn-secondary-bg | secondary button background |
--shk-btn-secondary-color | secondary button text color |
--shk-btn-secondary-border | secondary button border shorthand |
Opting in with class (recommended for most buttons)
Add shk-host-btn to any <button> or <a> element you want themed. Keep only structural CSS (padding, border-radius, font-size, font-family, width) in your app stylesheet — do not set background, color, or border on the same element.
<button class="my-btn shk-host-btn" type="submit">Enter Giveaway</button>
/* app styles.css — structural only */
.my-btn {
width: 100%;
padding: 11px;
border-radius: 10px;
font-size: 0.9rem;
font-weight: 700;
font-family: inherit;
}
Using CSS variables (for configurable buttons)
If your app lets users configure the button color from the dashboard, keep your own CSS var at the highest priority and fall back to the theme var:
.my-app__button {
background: var(--my-app-btn-color, var(--shk-btn-primary-bg, #111827));
color: var(--my-app-btn-text, var(--shk-btn-primary-color, #ffffff));
}
This gives the profile owner's configuration the final say while still respecting the theme's default when no per-app color is set.
Structural rules when using .shk-host-btn
- Do not set
background,color, orborderon the element (the injected style owns these). - Do keep layout and size properties in your own CSS (padding, width, border-radius, font-size, font-family).
cursor,transition, hover, active, and disabled states are all handled by the injected CSS.
Host Component Style — Typography, Lists, Cards, and Buttons
Beyond cards and buttons, the platform provides exact structural clones of the active theme's typography and list layouts via the shuuka:component_style bridge. These are not convenience helpers. They are the contract native apps are expected to build on.
When requested, the platform injects <style id="shk-host-component-style"> with authoritative !important rules. Theme values coming from createThemeBridge() are meant to win.
Host class checklist
| Class | Use Case | Ownership |
|---|---|---|
.shk-host-font-family | Root app wrapper | Makes the entire app inherit the theme font family. |
.shk-host-title | App titles and section headings | Theme owns size, weight, line-height, and color. |
.shk-host-subtitle | Supporting copy, metadata, helper text | Theme owns subdued text styling. |
.shk-host-paragraph | Main reading copy and primary row text | Theme owns the default body style. |
.shk-host-label | Small labels above values | Theme owns the label treatment. |
.shk-host-list-container | Vertical list parent | Owns row spacing. Do not recreate with local margins. |
.shk-host-list-row | Individual list row or linked row | Owns the shared row surface and hover. |
.shk-host-list-grid | Theme-owned auto-fill grids | Use when the theme should control grid rhythm. |
.shk-host-card | Internal card surface | Owns card background, border, radius, and shadow. |
.shk-host-btn | Primary CTA | Owns button visual styling. |
.shk-host-btn.shk-host-btn--secondary | Secondary or repeated CTA | Owns secondary button styling. |
Rules
- Prefer host primitives over app-local visual CSS.
- Do not recreate list spacing with stacked margins. Use
.shk-host-list-container. - Do not recreate row surfaces with app-local
background,border,border-radius, or hover states. Use.shk-host-list-row. - Do not recreate theme card visuals inside the app. Use
.shk-host-card. - If the app allows the profile owner to configure typography directly, use
.shk-host-font-familyand app-local sizing instead of forcing.shk-host-title,.shk-host-subtitle, or.shk-host-paragraph.
Example: Theme-perfect list markup
<div class="shk-host-font-family">
<div class="shk-host-list-container" role="list">
<a href="https://example.com" class="shk-host-list-row" role="listitem" target="_blank" rel="noopener noreferrer">
<img src="icon.png" alt="" style="width:56px;height:56px;border-radius:8px;object-fit:cover;flex-shrink:0;" />
<div style="flex:1;min-width:0;">
<span class="shk-host-label">Platform</span>
<p class="shk-host-paragraph" style="margin:0;">Instagram</p>
</div>
</a>
</div>
</div>
createThemeBridge() values that style host classes
createThemeBridge({
component: {
titleColor: '#111827',
subtitleColor: '#6b7280',
paragraphColor: '#111827',
titleFontSize: '1rem',
subtitleFontSize: '0.875rem',
paragraphFontSize: '1rem',
titleLineHeight: '1.3',
subtitleLineHeight: '1.5',
paragraphLineHeight: '1.6',
titleFontWeight: 700,
subtitleFontWeight: 400,
paragraphFontWeight: 400,
subtitleOpacity: 0.8,
rowBg: 'rgba(0,0,0,0.03)',
rowBorder: 'rgba(0,0,0,0.08)',
rowHoverBg: 'rgba(0,0,0,0.05)',
rowHoverBorder: 'rgba(0,0,0,0.12)',
},
});
That component object drives:
.shk-host-title.shk-host-subtitle.shk-host-paragraph.shk-host-label.shk-host-list-container.shk-host-list-row.shk-host-list-grid
If your app is still compensating with local CSS after that, it is probably not using the host contract correctly.
Theme-Aware Card Variables (legacy fallback)
If you are not using .shk-host-card, you can still reference individual CSS variables that the platform injects for theme-aware defaults:
| Variable | Use |
|---|---|
--app-card-bg | card background |
--app-card-border | border color |
--app-card-accent | CTA background |
--app-card-accent-text | CTA text |
--app-card-radius | card radius |
--app-card-shadow | card shadow |
Adapting to Dark or Light Themes (Color Schemes)
When the profile owner uses a theme that defines "color_scheme": "dark", the platform passes this to every installed app.
- HTML injection: The platform automatically injects
<html class="dark">into the app's document. - URL Parameter: The iframe URL includes
&theme_mode=dark.
Use this to ensure your text stays legible on dark theme surfaces. If you use Tailwind, you can use standard dark:text-white classes. If you use standard CSS, you can scope rules like .dark p { color: #fff; }.
Note: The platform will NOT forcefully override your app's text color. It is entirely up to you (the app developer) to react to the .dark class and pick appropriate contrast colors for your typography and embedded elements.
Ownership Rule
Only one layer should visually own the card.
| Pattern | Verdict |
|---|---|
| Theme owns the outer card, app renders content inside | Good |
| App intentionally renders its own full surface and disables outer styling | Good |
| Theme card + inner app card both render full shadows/backgrounds | Wrong |
Bottom Sheet Contract
If your app supports Bottom Sheet mode:
- let the host own the sheet chrome
- do not render a second close shell inside the app
- send the required bottom-sheet messages
- test success, close, back, and resize states
Resize Contract
Apps must behave correctly when embedded:
- do not assume fixed height
- avoid clipping your own content
- let the platform resize contract manage outer containers
Vanilla JS vs React
| Approach | Best for |
|---|---|
| Vanilla JS | small cards and minimal interactions |
| React | larger app shells, richer admin tabs, or public pages with more state |
If you ship React, commit the built output. Shuuka does not build your bundle at runtime.
UX Checklist
- handle loading, empty, error, success, and disabled states
- escape user-provided strings before
innerHTML - scope DOM access to your app root
- keep auth-only actions inside admin pages