Use Case: Retheme a Hero Banner¶
A client wants Hero Banner in their brand, Split as the go-to layout, and a promo treatment the component doesn't ship. Four levels of intervention, lightest first, following the priority order.
See it live¶
Where the walkthrough ends: step 4's promo card on step 1's brand tokens. Switch Layout to Split for step 3, or the theme to Bootstrap theme for the shipped look.
1. Reskin it: --theme-*, not a template override¶
Hero Banner ships pre-compiled, so it's reskinned from the theme's :root, with no component file touched:
:root {
--theme-color-primary: #10564f;
--theme-radius: 0.75rem;
--theme-container-lg: 1100px;
/* …the rest of the contract — see Token reference */
}
--theme-radius sets the CTA's corners, through var(--hero-banner-cta-radius, var(--theme-radius, 0)). Bootstrap has no runtime variable for it, so the token is the only lever. The CTA's colour is a stock .btn-outline-light / -dark, so it follows $light / $dark like every other button on the site.
2. A component-scoped token when only Hero Banner should move¶
When the shared values are right everywhere else, set a --hero-* token instead, scoped to a class, never :root:
| Token | Controls |
|---|---|
--hero-min-height-mobile / -desktop |
Minimum height per breakpoint (defaults below) |
--hero-gradient-vertical |
The legibility scrim under light-variant text |
--hero-carousel-control-width |
Width of the prev/next hit area |
--hero-split-container-inset |
Split's content inset from the viewport edge |
--hero-split-media-min-height |
Split's media-column minimum height |
.hero-banner--split {
--hero-split-media-min-height: 320px;
}
Each layout sets its own min-heights, so override them on .hero-banner rather than assuming one default:
| Layout | Mobile | Desktop |
|---|---|---|
| Simple | 360px |
480px |
| Full | 80dvh |
90dvh |
| Split | auto (content sets it) |
600px |
Suggested image aspect ratios¶
Backgrounds are object-fit: cover, so any image works. These crops flatter each layout most:
| Layout | Mobile | Desktop |
|---|---|---|
| Simple | 1:1 |
3:1 |
| Full | 9:16 |
16:9 |
| Split | 16:9 |
4:3 |
Shoot looser than you need: a 4:3 photo crops cleanly to 16:9, but not the other way round.
3. Pick an existing layout, not the shipped default¶
layout is a prop (simple | full | split, default simple), so editors pick Split per instance in Canvas or Layout Builder, with nothing to build. Making Split the site-wide default is the smallest version of step 4: override the component and restate only layout with a new default:.
4. Add a layout the component doesn't ship¶
A compact promo card pinned to a corner of the image needs a new enum value, its own markup and its own SCSS:
# themes/custom/<theme>/components/hero_banner/hero_banner.component.yml
name: Hero Banner
replaces: 'ixm_blocks:hero_banner'
props:
type: object
properties:
layout:
type: string
enum: [simple, full, split, promo-card]
default: simple
libraryOverrides:
css:
component:
public/css/hero-banner.css: { minified: true }
{# themes/custom/<theme>/components/hero_banner/hero_banner.twig #}
{# The existing layout_classes already yield .hero-banner--promo-card; only
the markup needs a branch, one that skips the carousel wrapper. #}
{% if layout == 'promo-card' %}
<div{{ create_attribute().addClass(layout_classes).addClass('position-relative') }}>
{{ slides }}
</div>
{% else %}
{# …existing simple/full/split carousel markup, unchanged… #}
{% endif %}
// themes/custom/<theme>/components/hero_banner/src/scss/hero-banner.scss
@use "sass:map";
@import "@theme/global/src/scss/abstracts/scaffold";
// Slides still carry .carousel-item, which floats; outside .carousel-inner
// that collapses the wrapper to nothing.
.hero-banner--promo-card .carousel-item {
float: none;
margin-right: 0;
}
// The content block carries Bootstrap's .position-relative and .py-5
// utilities, both !important, so position and padding need it too.
.hero-banner--promo-card .hero__banner .hero__banner-content {
position: absolute !important;
inset-block-end: map.get($spacers, 4);
inset-inline-end: map.get($spacers, 4);
max-width: 20rem;
padding: map.get($spacers, 4) !important;
background: $primary;
border-radius: $border-radius-lg;
}
// The slide's content column is .col-lg-8 — two-thirds of a 20rem card.
.hero-banner--promo-card .hero__banner .hero__banner-content .row > * {
flex: 0 0 100%;
max-width: 100%;
}
sdc_prop_inherit saves retyping the other props; see Frameworks. Reaching for $primary directly is fine here: this file lives in the project theme, where a brand palette belongs.
The shape of it¶
| Change | Lever | Priority-order tier |
|---|---|---|
| Brand colours, radius, spacing across every component | --theme-* in the theme's :root |
2 — escape hatch |
| Only Hero Banner's gradient, min-height or split inset | A --hero-* component token, scoped to a class |
2 |
| A different shipped layout, per instance | The layout prop, set in Canvas or Layout Builder |
Authoring, not theming |
| A different shipped layout, as the new default everywhere | replaces: override, prop schema only |
2, unchanged markup |
| A layout the component doesn't have at all | replaces: override with new markup and its own SCSS |
1 — Bootstrap Sass, direct |