Hero
Opens a page with its headline, its argument, and its primary action. Owns the page's <h1>.
When to Use
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.
Facilitate better
workshops & sprints.
The all-in-one platform for planning, running, and reflecting on remote workshops.
<?= 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'],
],
]) ?>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.
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
| State | Behavior |
|---|---|
| Default | Static. The hero itself has no interactive state — hover and focus belong to the buttons inside it. |
| Omitted parts | Badge, accent, subtitle, buttons, stats, and image each render only when supplied. No empty space is reserved. |
| With an image | Setting image switches to a two-column layout with the visual beside the text. Without it the content spans the container. |
| Responsive | The two-column layout collapses to stacked on narrow viewports; stats wrap. |
| Background pattern | A 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
| Option | Type | Default | Purpose |
|---|---|---|---|
$title | string | required | First positional. The headline, rendered inside the <h1>. |
$accent | string | '' | Second positional. Italic brand-coloured continuation of the headline. Pass '' to omit. |
badge | string | '' | Small label above the heading |
badgeIcon | string | '' | Material icon inside the badge |
subtitle | string | '' | Supporting paragraph |
buttons | array | [] | Rendered HTML strings, not option arrays. Use ws_button() to produce them. |
stats | array | [] | [['value' => …, 'label' => …]], same shape as Stat |
statColor | string | #E54D4D | Stat value colour. The default is the brand red as a literal — pass var(--phase-learn) to keep it token-driven. |
image | string | '' | Hero visual URL. Switches to the two-column layout. |
imageAlt | string | '' | Alt text. Set it whenever image is set — see Accessibility. |
id | string | null | Element ID |
class | string | '' | Additional CSS classes |
Accessibility
| Concern | Behavior |
|---|---|
| Heading | Renders 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. |
| Accent | Sits inside the <h1>, so the announced heading is title plus accent as one phrase. Write them to read continuously. |
| Keyboard | Only the buttons are focusable, and they're the first tab stops on the page. Their order should match their visual priority. |
| Image alt | Your 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 pattern | Purely decorative and rendered as an empty <div>, so it contributes nothing to the accessibility tree. Correct as-is. |
| Stats | Value and label are adjacent text, announced in order. A value like "50+" reads fine; avoid glyph-only values. |
| Contrast | The 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.
| Token | Used for |
|---|---|
--bg-warm | Hero background |
--gradient-primary | Accent underline and badge fill |
--mainsite-primary | Accent text and badge colour |
--phase-learn / --phase-learn-light | Brand accents |
--primary-red / -dark / -hover / -light | Legacy red aliases still referenced by the hero rules |
--radius-md / --radius-lg / --radius-xl | Badge, button, and image corners |
--shadow-lg | Hero visual elevation |
--space-1-5 through --space-10 | Vertical 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.
| Class | Purpose |
|---|---|
.hero | Outer wrapper |
.hero-bg-pattern | Decorative dot overlay |
.hero-container | Max-width flex container |
.hero-content | Text column |
.hero-badge | Icon and label above the heading |
.hero-title | The <h1> |
.hero-title-accent | Italic accent inside the heading |
.hero-subtitle | Supporting paragraph |
.hero-actions | Button row |
.hero-stats / .hero-stat / .hero-stat-divider | Stat row, items, and dividers |
.hero-visual / .hero-image | Image column and the <img> |
Files
| File | Purpose |
|---|---|
includes/components/helpers.php | ws_hero() helper function |
includes/components/hero.php | Template — heading, badge, actions, stats, and visual |
css/styles.css | All hero styles (.hero-* rules). Not in components.css. |