Canvas Design System
Main Site Tokens

Hero

Opens a page with its headline, its argument, and its primary action. Owns the page's <h1>.

When to Use

Use when: At the top of a landing page, app homepage, or marketing page — once. It carries the value proposition and the primary call to action.
Don't use when: You're on an interior or detail page — use Section Header, which renders h2/h3. Never place two heroes on one page: the component emits an <h1>, and two would break the outline.

Variants

Full

Badge, title with accent, subtitle, actions, and stats. Two buttons at most — a primary and one lower-commitment alternative.

Workshop Facilitation Platform

Facilitate better
workshops &amp; sprints.

The all-in-one platform for planning, running, and reflecting on remote workshops.

50+ Workshops
120+ Exercises
40+ Icebreakers
<?= ws_hero('Facilitate better', 'workshops & sprints.', [
    'badge'     => 'Workshop Facilitation Platform',
    'badgeIcon' => 'view_kanban',
    'subtitle'  => 'The all-in-one platform for planning, running, and reflecting on workshops.',
    'buttons'   => [
        ws_button('Browse Library',  ['variant' => 'primary',   'icon' => 'explore',
                                      'href' => '/library/workshops/']),
        ws_button('Plan a Workshop', ['variant' => 'secondary', 'icon' => 'calendar_month',
                                      'href' => '/planner/']),
    ],
    'stats'     => [
        ['value' => '50+',  'label' => 'Workshops'],
        ['value' => '120+', 'label' => 'Exercises'],
    ],
]) ?>
info buttons takes an array of rendered HTML strings, not option arrays — call ws_button() yourself and pass the results. That's unusual for this library, and passing option arrays here silently produces nothing.

Title and accent

The accent renders italic in the brand colour with an underline. Write the two parts as one sentence — the accent is the end of the phrase, not a subtitle.

Exercise Library

Browse our curated
exercise library.

Find the perfect activity for any workshop moment.

<?= ws_hero('Browse our curated', 'exercise library.', [
    'badge'    => 'Exercise Library',
    'subtitle' => 'Find the perfect activity for any workshop moment.',
]) ?>

Minimal

Title, subtitle, one action. Pass an empty string for the accent when the headline stands alone.

Welcome to the Coach

Your AI-powered facilitation assistant for live workshop sessions.

<?= ws_hero('Welcome to the Coach', '', [
    'subtitle' => 'Your AI-powered facilitation assistant.',
    'buttons'  => [ws_button('Start Session', ['variant' => 'primary', 'href' => '/coach/'])],
]) ?>

States

StateBehavior
DefaultStatic. The hero itself has no interactive state — hover and focus belong to the buttons inside it.
Omitted partsBadge, accent, subtitle, buttons, stats, and image each render only when supplied. No empty space is reserved.
With an imageSetting image switches to a two-column layout with the visual beside the text. Without it the content spans the container.
ResponsiveThe two-column layout collapses to stacked on narrow viewports; stats wrap.
Background patternA decorative dot overlay is always rendered as .hero-bg-pattern. Not configurable.

Real-World Usage

The homepage hero, with library counts read live so the proof figures can't drift from the database, and the primary action switching on auth state.

<?php
$counts = getLibraryCounts();
?>
<?= ws_hero('Facilitate better', 'workshops & sprints.', [
    'badge'     => 'Workshop Facilitation Platform',
    'badgeIcon' => 'view_kanban',
    'subtitle'  => 'Plan the agenda, run the room, and turn the notes into something worth sending.',
    'buttons'   => [
        $isLoggedIn
            ? ws_button('Open the Planner', ['variant' => 'primary', 'href' => '/planner/'])
            : ws_button('Start free',       ['variant' => 'primary', 'href' => '/signup']),
        ws_button('Browse the library', ['variant' => 'secondary', 'href' => '/library/']),
    ],
    'stats'     => [
        ['value' => $counts['exercises']   . '+', 'label' => 'Exercises'],
        ['value' => $counts['icebreakers'] . '+', 'label' => 'Icebreakers'],
        ['value' => $counts['workshops']   . '+', 'label' => 'Workshop templates'],
    ],
]) ?>

Options

OptionTypeDefaultPurpose
$titlestringrequiredFirst positional. The headline, rendered inside the <h1>.
$accentstring''Second positional. Italic brand-coloured continuation of the headline. Pass '' to omit.
badgestring''Small label above the heading
badgeIconstring''Material icon inside the badge
subtitlestring''Supporting paragraph
buttonsarray[]Rendered HTML strings, not option arrays. Use ws_button() to produce them.
statsarray[][['value' => …, 'label' => …]], same shape as Stat
statColorstring#E54D4DStat value colour. The default is the brand red as a literal — pass var(--phase-learn) to keep it token-driven.
imagestring''Hero visual URL. Switches to the two-column layout.
imageAltstring''Alt text. Set it whenever image is set — see Accessibility.
idstringnullElement ID
classstring''Additional CSS classes

Accessibility

ConcernBehavior
HeadingRenders a real <h1 class="hero-title">. That's the page's single top-level heading — which is exactly why there can only be one hero per page.
AccentSits inside the <h1>, so the announced heading is title plus accent as one phrase. Write them to read continuously.
KeyboardOnly the buttons are focusable, and they're the first tab stops on the page. Their order should match their visual priority.
Image altYour responsibility. imageAlt defaults to an empty string, which marks the image decorative. That's correct for an abstract illustration, but wrong for a screenshot carrying information — describe it.
Background patternPurely decorative and rendered as an empty <div>, so it contributes nothing to the accessibility tree. Correct as-is.
StatsValue and label are adjacent text, announced in order. A value like "50+" reads fine; avoid glyph-only values.
ContrastThe hero sits on --bg-warm with brand-red accents. If you set a custom statColor, check it against that background.

Tokens

Hero is the one component whose styles live in css/styles.css rather than components.css, so it draws on the main-site token set.

TokenUsed for
--bg-warmHero background
--gradient-primaryAccent underline and badge fill
--mainsite-primaryAccent text and badge colour
--phase-learn / --phase-learn-lightBrand accents
--primary-red / -dark / -hover / -lightLegacy red aliases still referenced by the hero rules
--radius-md / --radius-lg / --radius-xlBadge, button, and image corners
--shadow-lgHero visual elevation
--space-1-5 through --space-10Vertical rhythm and stat spacing

CSS Classes

Hero does not use the .ws- prefix — it emits main-site .hero-* classes defined in css/styles.css. A page rendering a hero must load that stylesheet as well as the component library.

ClassPurpose
.heroOuter wrapper
.hero-bg-patternDecorative dot overlay
.hero-containerMax-width flex container
.hero-contentText column
.hero-badgeIcon and label above the heading
.hero-titleThe <h1>
.hero-title-accentItalic accent inside the heading
.hero-subtitleSupporting paragraph
.hero-actionsButton row
.hero-stats / .hero-stat / .hero-stat-dividerStat row, items, and dividers
.hero-visual / .hero-imageImage column and the <img>

Files

FilePurpose
includes/components/helpers.phpws_hero() helper function
includes/components/hero.phpTemplate — heading, badge, actions, stats, and visual
css/styles.cssAll hero styles (.hero-* rules). Not in components.css.