Creating Theme Variants from SAP Fiori Horizon Styles
How to build event/product-branded theme overlays on top of the SAP Fiori Horizon base theme using CSS data-attribute selectors. No changes to the base Fiori styles are needed — each theme is a pure additive CSS layer.
Architecture
Base layer: SAP Fiori Horizon CSS custom properties (--sap* vars)
↓ inherited by all components
Theme layer: CSS overrides scoped via [data-theme="<name>"] selectors
↓ naturally higher specificity than base
Dark layer: [data-theme="<name>"][data-dark] compound selectors
↓ wins over light-mode theme selectorsWhy this works
[data-theme="joule"] .herohas higher specificity than.heroalone[data-theme="joule"][data-dark] .herobeats[data-theme="joule"] .hero- No
!importantneeded (except for elements that already use it in the base) - The base Fiori theme is completely unaffected when no
data-themeattribute is set - Each theme's CSS is self-contained and can be removed without breaking anything
Key base Fiori Horizon values being overridden
| SAP Custom Property | Light Fallback | Dark Fallback | Used For |
|---|---|---|---|
--sapShellColor | #354a5f | #223548 | Nav bar, hero background |
--sapBrandColor | #0070f2 | #0070f2 | Links, active states, rings |
--sapPositiveColor | #107e3e | #57b520 | Completed states (keep this!) |
--sapAccentColor1 | #e38b16 | #e38b16 | Prize/earned states |
--sapTile_Background | #fff | #1d2d3e | Card backgrounds |
--sapTextColor | #32363a | #d1e8ff | Body text |
--sapContent_LabelColor | #556b82 | #89a0b5 | Secondary text |
--sapNeutralBorderColor | #d9d9d9 | #556b82 | Borders, inactive states |
--sapBackgroundColor | #f5f6f7 | #12171c | Page background |
JavaScript Pattern (Vue 3 / VitePress)
Theme detection via URL parameter
import { ref, computed } from 'vue'
import { useData } from 'vitepress'
const { isDark } = useData()
const activeTheme = ref<'joule' | 'sapphire' | null>(null)
// Reactive content that changes per theme
const eventName = computed(() => {
if (activeTheme.value === 'sapphire') return 'SAP Sapphire 2026'
return 'SAP TechEd'
})
onMounted(() => {
const params = new URLSearchParams(window.location.search)
const theme = params.get('theme')
if (theme === 'joule' || theme === 'sapphire') {
activeTheme.value = theme
}
})Template root element
<div class="my-component"
:data-theme="activeTheme ?? undefined"
:data-dark="isDark ? '' : undefined"
>The isDark ref from VitePress is reactive — it watches the html.dark class that VitePress toggles. Setting data-dark as a boolean attribute (empty string = present) means the CSS selectors activate automatically on toggle.
Adding a new theme
- Add the theme name to the union type:
ref<'joule' | 'sapphire' | 'newtheme' | null>(null) - Add detection:
if (theme === 'joule' || theme === 'sapphire' || theme === 'newtheme') - Add content overrides to the computed (event name, etc.)
- Add CSS sections (see template below)
CSS Selector Pattern
Every theme override follows this structure:
/* Light mode override */
.my-component[data-theme="<name>"] .<target-class> {
/* override properties */
}
/* Dark mode override — compound selector wins by specificity */
.my-component[data-theme="<name>"][data-dark] .<target-class> {
/* dark-specific overrides */
}What to override per theme
Each theme needs overrides for these UI surface categories:
| Category | CSS Targets | What Changes |
|---|---|---|
| Hero banner | .hero | Background gradient |
| Hero glass elements | .stat-card, .instruction-icon, .demo-toggle | rgba() tints matching the gradient |
| Card hover | .track-card:hover, .track-card:focus-visible | Border color, box-shadow color |
| Progress ring | .progress-ring-fill | stroke color |
| Progress bar | .track-card__progress-fill | background (gradient or solid) |
| Available state | .step--available .timeline-bubble, .step--available .timeline-content | border-color, color |
| In-progress state | .step--in-progress .timeline-bubble, .step--in-progress .timeline-content | border-color, background, color |
| Tutorial badges | .item-type--tutorial | background, color |
| Links | .timeline-content__title.clickable | color |
| Hover glow | .timeline-bubble.clickable:hover | box-shadow ring color |
| Back button | .back-button:hover | color |
What NOT to override
- Completed state (
.step--completed) — keep the green--sapPositiveColoracross all themes for consistency - Earned/prize state (
.step--earned) — keep gold--sapAccentColor1unless the theme has a distinct "reward" color - Locked state (
.step--locked) — keep the neutral gray/disabled styling - Typography — keep
--sapFontFamilyand font sizes consistent - Spacing/layout — themes change color, not structure
Joule Theme
Brand identity: AI assistant purple gradient Source: SAP Joule brand guidelines Activation: ?theme=joule
Color Palette
| Token | Light Mode | Dark Mode | Usage |
|---|---|---|---|
| Primary | #5D36FF | #8B6FFF | Interactive elements, links, rings |
| Secondary | #A100C2 | #C040E0 | Gradient end, accent |
| Gradient | #5D36FF → #7B42F0 → #A100C2 | #2A1066 → #4B1A8A → #6B0080 | Hero banner |
| Tint 8% | rgba(93, 54, 255, 0.08) | rgba(139, 111, 255, 0.12) | Badge backgrounds, in-progress bg |
| Tint 4% | rgba(93, 54, 255, 0.04) | rgba(139, 111, 255, 0.06) | Content card backgrounds |
| Glass | rgba(255, 255, 255, 0.14–0.22) | rgba(93, 54, 255, 0.12–0.25) | Hero stat cards, instruction icons |
Design Principles
- The gradient runs at 135 degrees (top-left to bottom-right)
- Light mode uses the full-saturation brand colors directly
- Dark mode desaturates and lightens the primary (
#5D36FF→#8B6FFF) for readability on dark backgrounds - Dark mode hero uses deep, muted versions of the gradient colors (not just darkened)
- Progress bars use the full gradient (
primary → secondary) as a linear-gradient - Glass overlay elements on the hero use white
rgba()in light mode, but switch to primary-tintedrgba()in dark mode
Complete CSS — Light Mode
.app-space[data-theme="joule"] .hero {
background: linear-gradient(135deg, #5D36FF 0%, #7B42F0 40%, #A100C2 100%);
}
.app-space[data-theme="joule"] .stat-card {
background: rgba(255, 255, 255, 0.14);
border-color: rgba(255, 255, 255, 0.22);
}
.app-space[data-theme="joule"] .instruction-icon {
background: rgba(255, 255, 255, 0.22);
}
.app-space[data-theme="joule"] .demo-toggle {
border-color: rgba(255, 255, 255, 0.3) !important;
background: rgba(255, 255, 255, 0.1) !important;
}
.app-space[data-theme="joule"] .demo-toggle:hover {
background: rgba(255, 255, 255, 0.2) !important;
}
.app-space[data-theme="joule"] .track-card:hover {
border-color: #5D36FF;
box-shadow: 0 0.25rem 1rem rgba(93, 54, 255, 0.15);
}
.app-space[data-theme="joule"] .track-card:focus-visible {
outline-color: #5D36FF;
}
.app-space[data-theme="joule"] .progress-ring-fill {
stroke: #5D36FF;
}
.app-space[data-theme="joule"] .track-card--complete .progress-ring-fill {
stroke: var(--sapPositiveColor, #107e3e);
}
.app-space[data-theme="joule"] .track-card__progress-fill {
background: linear-gradient(90deg, #5D36FF, #A100C2);
}
.app-space[data-theme="joule"] .track-card--complete .track-card__progress-fill {
background: var(--sapPositiveColor, #107e3e);
}
.app-space[data-theme="joule"] .step--available .timeline-bubble {
border-color: #5D36FF;
color: #5D36FF;
}
.app-space[data-theme="joule"] .step--available .timeline-content {
border-color: #5D36FF;
}
.app-space[data-theme="joule"] .step--in-progress .timeline-bubble {
border-color: #5D36FF;
background: rgba(93, 54, 255, 0.08);
color: #5D36FF;
}
.app-space[data-theme="joule"] .step--in-progress .timeline-content {
border-color: #5D36FF;
background: rgba(93, 54, 255, 0.04);
}
.app-space[data-theme="joule"] .item-type--tutorial {
background: rgba(93, 54, 255, 0.08);
color: #5D36FF;
}
.app-space[data-theme="joule"] .timeline-content__title.clickable {
color: #5D36FF;
}
.app-space[data-theme="joule"] .timeline-bubble.clickable:hover {
box-shadow: 0 0 0 3px rgba(93, 54, 255, 0.1);
}
.app-space[data-theme="joule"] .back-button:hover {
color: #5D36FF !important;
}Complete CSS — Dark Mode
.app-space[data-theme="joule"][data-dark] .hero {
background: linear-gradient(135deg, #2A1066 0%, #4B1A8A 40%, #6B0080 100%);
}
.app-space[data-theme="joule"][data-dark] .stat-card {
background: rgba(93, 54, 255, 0.12);
border-color: rgba(93, 54, 255, 0.25);
}
.app-space[data-theme="joule"][data-dark] .instruction-icon {
background: rgba(93, 54, 255, 0.25);
}
.app-space[data-theme="joule"][data-dark] .track-card:hover {
border-color: #8B6FFF;
box-shadow: 0 0.25rem 1rem rgba(93, 54, 255, 0.25);
}
.app-space[data-theme="joule"][data-dark] .track-card:focus-visible {
outline-color: #8B6FFF;
}
.app-space[data-theme="joule"][data-dark] .progress-ring-fill {
stroke: #8B6FFF;
}
.app-space[data-theme="joule"][data-dark] .track-card__progress-fill {
background: linear-gradient(90deg, #8B6FFF, #C040E0);
}
.app-space[data-theme="joule"][data-dark] .track-card--complete .track-card__progress-fill {
background: var(--sapPositiveColor, #57b520);
}
.app-space[data-theme="joule"][data-dark] .step--available .timeline-bubble {
border-color: #8B6FFF;
color: #8B6FFF;
}
.app-space[data-theme="joule"][data-dark] .step--available .timeline-content {
border-color: #8B6FFF;
}
.app-space[data-theme="joule"][data-dark] .step--in-progress .timeline-bubble {
border-color: #8B6FFF;
background: rgba(139, 111, 255, 0.15);
color: #8B6FFF;
}
.app-space[data-theme="joule"][data-dark] .step--in-progress .timeline-content {
border-color: #8B6FFF;
background: rgba(139, 111, 255, 0.06);
}
.app-space[data-theme="joule"][data-dark] .item-type--tutorial {
background: rgba(139, 111, 255, 0.12);
color: #8B6FFF;
}
.app-space[data-theme="joule"][data-dark] .timeline-content__title.clickable {
color: #8B6FFF;
}
.app-space[data-theme="joule"][data-dark] .timeline-bubble.clickable:hover {
box-shadow: 0 0 0 3px rgba(139, 111, 255, 0.15);
}SAP Sapphire 2026 Theme
Brand identity: SAP Sapphire event — "blue to pink" core colorway Source: SAP Sapphire 2026 Creative Guide Activation: ?theme=sapphireContent override: Event name changes to "SAP Sapphire 2026"
Color Palette (from Creative Guide)
| Token | Hex | Usage |
|---|---|---|
| Blue 2 | #D1EFFF | Very light accent (hover backgrounds) |
| Blue 4 | #89D1FF | Dark-mode primary interactive color |
| Blue 6 | #1B90FF | Light-mode accent, hero gradient start |
| Blue 7 | #0070F2 | Primary brand blue (same as --sapBrandColor) |
| Blue 10 | #002A86 | Deep navy, hero gradient end, dark-mode hero start |
| Blue 11 | #00144A | Darkest navy, dark-mode hero mid |
| Pink accent | #C850C0 | Light-mode earned/reward, progress gradient end |
| Pink accent (dark) | #E070D0 | Dark-mode earned/reward, lightened for readability |
Design Principles
- The "blue to pink" core colorway is Sapphire's visual signature — apply it to progress bars and achievement states
- The hero gradient uses the Blue family only (Blue 6 → Blue 7 → Blue 10) — the pink appears in detail elements, not the hero
- 45 degrees is a signature angle in the Sapphire brand; the hero uses 135 degrees (which creates the same diagonal)
- Light mode primary (
#0070F2) is identical to Fiori's--sapBrandColor— the differentiation comes from the hero gradient depth and the pink accent - Dark mode switches to Blue 4 (
#89D1FF) as the primary interactive color — light enough to be readable on dark navy - Prize/earned states use the pink accent instead of the default gold
--sapAccentColor1to reinforce the blue-to-pink colorway
Deriving dark-mode colors
For dark mode, the rules are:
- Hero: shift the gradient deeper — use Blue 10, Blue 11, and a hint of near-black purple (
#0A0030) - Interactive elements: use Blue 4 (
#89D1FF) — the lightest usable blue from the palette - Tinted backgrounds: use the dark-mode primary at 10-15% opacity
- Pink accent: lighten from
#C850C0to#E070D0for contrast on dark surfaces - Glass overlays: tint with Blue 6 at low opacity instead of white
Complete CSS — Light Mode
.app-space[data-theme="sapphire"] .hero {
background: linear-gradient(135deg, #1B90FF 0%, #0070F2 45%, #002A86 100%);
}
.app-space[data-theme="sapphire"] .stat-card {
background: rgba(255, 255, 255, 0.14);
border-color: rgba(255, 255, 255, 0.22);
}
.app-space[data-theme="sapphire"] .instruction-icon {
background: rgba(255, 255, 255, 0.22);
}
.app-space[data-theme="sapphire"] .demo-toggle {
border-color: rgba(255, 255, 255, 0.3) !important;
background: rgba(255, 255, 255, 0.1) !important;
}
.app-space[data-theme="sapphire"] .demo-toggle:hover {
background: rgba(255, 255, 255, 0.2) !important;
}
.app-space[data-theme="sapphire"] .track-card:hover {
border-color: #0070F2;
box-shadow: 0 0.25rem 1rem rgba(0, 112, 242, 0.12), 0 0 0 1px rgba(200, 80, 192, 0.08);
}
.app-space[data-theme="sapphire"] .track-card:focus-visible {
outline-color: #0070F2;
}
.app-space[data-theme="sapphire"] .progress-ring-fill {
stroke: #0070F2;
}
.app-space[data-theme="sapphire"] .track-card--complete .progress-ring-fill {
stroke: var(--sapPositiveColor, #107e3e);
}
/* Core colorway: blue → pink gradient on progress bars */
.app-space[data-theme="sapphire"] .track-card__progress-fill {
background: linear-gradient(90deg, #0070F2, #C850C0);
}
.app-space[data-theme="sapphire"] .track-card--complete .track-card__progress-fill {
background: var(--sapPositiveColor, #107e3e);
}
.app-space[data-theme="sapphire"] .step--available .timeline-bubble {
border-color: #0070F2;
color: #0070F2;
}
.app-space[data-theme="sapphire"] .step--available .timeline-content {
border-color: #0070F2;
}
.app-space[data-theme="sapphire"] .step--in-progress .timeline-bubble {
border-color: #1B90FF;
background: rgba(27, 144, 255, 0.08);
color: #0070F2;
}
.app-space[data-theme="sapphire"] .step--in-progress .timeline-content {
border-color: #1B90FF;
background: rgba(27, 144, 255, 0.04);
}
.app-space[data-theme="sapphire"] .item-type--tutorial {
background: rgba(0, 112, 242, 0.08);
color: #0070F2;
}
.app-space[data-theme="sapphire"] .timeline-content__title.clickable {
color: #0070F2;
}
.app-space[data-theme="sapphire"] .timeline-bubble.clickable:hover {
box-shadow: 0 0 0 3px rgba(0, 112, 242, 0.1);
}
.app-space[data-theme="sapphire"] .back-button:hover {
color: #0070F2 !important;
}
/* Prize earned uses the pink accent from the core colorway */
.app-space[data-theme="sapphire"] .step--earned .timeline-bubble {
border-color: #C850C0;
background: rgba(200, 80, 192, 0.08);
color: #C850C0;
}
.app-space[data-theme="sapphire"] .step--earned .timeline-content {
border-left: 3px solid #C850C0;
}Complete CSS — Dark Mode
.app-space[data-theme="sapphire"][data-dark] .hero {
background: linear-gradient(135deg, #002A86 0%, #00144A 60%, #0A0030 100%);
}
.app-space[data-theme="sapphire"][data-dark] .stat-card {
background: rgba(27, 144, 255, 0.1);
border-color: rgba(27, 144, 255, 0.2);
}
.app-space[data-theme="sapphire"][data-dark] .instruction-icon {
background: rgba(27, 144, 255, 0.2);
}
.app-space[data-theme="sapphire"][data-dark] .track-card:hover {
border-color: #89D1FF;
box-shadow: 0 0.25rem 1rem rgba(0, 42, 134, 0.35), 0 0 0 1px rgba(200, 80, 192, 0.1);
}
.app-space[data-theme="sapphire"][data-dark] .track-card:focus-visible {
outline-color: #89D1FF;
}
.app-space[data-theme="sapphire"][data-dark] .progress-ring-fill {
stroke: #89D1FF;
}
/* Blue-to-pink gradient — dark variant uses lighter endpoints */
.app-space[data-theme="sapphire"][data-dark] .track-card__progress-fill {
background: linear-gradient(90deg, #89D1FF, #E070D0);
}
.app-space[data-theme="sapphire"][data-dark] .track-card--complete .track-card__progress-fill {
background: var(--sapPositiveColor, #57b520);
}
.app-space[data-theme="sapphire"][data-dark] .step--available .timeline-bubble {
border-color: #89D1FF;
color: #89D1FF;
}
.app-space[data-theme="sapphire"][data-dark] .step--available .timeline-content {
border-color: #89D1FF;
}
.app-space[data-theme="sapphire"][data-dark] .step--in-progress .timeline-bubble {
border-color: #89D1FF;
background: rgba(137, 209, 255, 0.12);
color: #89D1FF;
}
.app-space[data-theme="sapphire"][data-dark] .step--in-progress .timeline-content {
border-color: #89D1FF;
background: rgba(137, 209, 255, 0.05);
}
.app-space[data-theme="sapphire"][data-dark] .item-type--tutorial {
background: rgba(137, 209, 255, 0.1);
color: #89D1FF;
}
.app-space[data-theme="sapphire"][data-dark] .timeline-content__title.clickable {
color: #89D1FF;
}
.app-space[data-theme="sapphire"][data-dark] .timeline-bubble.clickable:hover {
box-shadow: 0 0 0 3px rgba(137, 209, 255, 0.12);
}
/* Prize earned — lightened pink for dark backgrounds */
.app-space[data-theme="sapphire"][data-dark] .step--earned .timeline-bubble {
border-color: #E070D0;
background: rgba(224, 112, 208, 0.12);
color: #E070D0;
}
.app-space[data-theme="sapphire"][data-dark] .step--earned .timeline-content {
border-left: 3px solid #E070D0;
}Creating a New Theme — Checklist
1. Define your palette (minimum 6 tokens)
| Token | Purpose | Light Example | Dark Example |
|---|---|---|---|
| Primary | Interactive elements, links, rings | Brand main color | Lightened 30-40% for dark bg |
| Secondary | Gradient end, accent | Brand secondary | Lightened for dark bg |
| Hero gradient | 3-stop linear-gradient | Vibrant brand colors | Deep/muted versions |
| Tint 8% | Badge backgrounds | rgba(primary, 0.08) | rgba(dark-primary, 0.12) |
| Tint 4% | Content backgrounds | rgba(primary, 0.04) | rgba(dark-primary, 0.06) |
| Glass | Hero overlays | rgba(255,255,255, 0.14) | rgba(primary, 0.12) |
2. Derive dark-mode colors
- Hero: Darken and desaturate; never use the same gradient as light mode
- Primary interactive: Lighten by 30-40% — must pass WCAG AA on
--sapTile_Backgrounddark - Tinted backgrounds: Increase opacity slightly (0.08 → 0.12) to compensate for lower contrast
- Glass overlays: Switch from white-tinted to primary-tinted
- Gradients: Use the lightened endpoints
3. Add the CSS sections
Copy one of the existing themes and replace colors:
/* ═══════════════════════════════════════════════
<Theme Name> — Light Mode
═══════════════════════════════════════════════ */
/* hero, stat-card, instruction-icon, demo-toggle */
/* track-card hover/focus, progress-ring, progress-bar */
/* timeline available, in-progress */
/* badges, links, hover glow, back button */
/* (optionally) earned state if theme has a reward color */
/* ═══════════════════════════════════════════════
<Theme Name> — Dark Mode
═══════════════════════════════════════════════ */
/* same targets, dark-adjusted colors */4. Wire up JavaScript
// Add to union type
const activeTheme = ref<'joule' | 'sapphire' | 'newtheme' | null>(null)
// Add detection
const theme = params.get('theme')
if (theme === 'joule' || theme === 'sapphire' || theme === 'newtheme') {
activeTheme.value = theme
}
// Add content overrides (optional)
const eventName = computed(() => {
if (activeTheme.value === 'sapphire') return 'SAP Sapphire 2026'
if (activeTheme.value === 'newtheme') return 'My Custom Event'
return 'SAP TechEd'
})5. Verify
Test each combination:
?theme=<name>in light mode?theme=<name>in dark mode- No theme parameter (base Fiori must be unaffected)
- All UI states: default, in-progress (demo mode click 1), track complete (demo mode click 2)
- Track detail view: locked, available, in-progress, completed, earned
- Responsive: desktop (1280px), tablet (768px), mobile (375px)