Patterns · Settings
Mode field
A labelled field with an inline exclusive mode control whose body swaps in place — the settings field for a control that has two-or-more mutually-exclusive modes.
Examples
variant="radio" (default) — OUTLINE segmented control: active = brand border + brand text on a WHITE fill (never a solid brand-filled pill). Switching swaps the body in place (the hidden body keeps its state).
Results appear automatically when everyone has answered.
You reveal results with the "Show results" button.
variant="segmented" — the neutral grey segmented control (the alternative look)
Everyone scores the same for a correct answer.
Faster correct answers score more.
label-variant="section" — semibold header for a grouped-list body
Award points for correct answers.
No points — this is just for fun.
Disabled
No countdown on this slide.
A countdown limits answering time.
<!-- Paste-and-run: save as .html and open in a browser. No build step.
<aha-mode-field> is the SAME shared custom element React and Vue consume — here in its native form.
Theming comes only from the --aha-* tokens in tokens.css. The bodies are light-DOM children tagged
data-mode; switching the active value toggles which is visible IN PLACE (the hidden one keeps its
state). variant="radio" (default) is the OUTLINE mode control; variant="segmented" is the grey one. -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/tokens.css">
<script type="module">
import 'https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/aha-mode-field.js'; // registers <aha-mode-field>
</script>
<aha-mode-field id="results" label="Results" value="auto"
help="Choose when the results chart appears to the audience." style="max-width:360px">
<div data-mode="auto">Results appear automatically when everyone has answered.</div>
<div data-mode="manual">You reveal results with the "Show results" button.</div>
</aha-mode-field>
<script type="module">
const el = document.getElementById('results');
el.options = [{ value: 'auto', label: 'Automatic' }, { value: 'manual', label: 'Manual' }];
el.addEventListener('change', (e) => console.log('value:', e.detail.value));
</script>
import '@ahaslides-product/design/aha-mode-field'; // registers <aha-mode-field>
import { useRef, useEffect } from 'react';
// React 18 needs a thin wrapper; React 19 can use <aha-mode-field> directly.
// options is a PROPERTY; bodies are children tagged data-mode — the hidden one keeps its state.
// variant: 'radio' (default outline) | 'segmented' (neutral grey). labelVariant: 'field' | 'section'.
function AhaModeField({ label, options, value, variant, labelVariant, help, disabled, onChange, children }) {
const ref = useRef();
useEffect(() => {
const el = ref.current;
el.options = options || [];
if (value != null) el.value = value;
if (variant != null) el.variant = variant;
if (labelVariant != null) el.labelVariant = labelVariant;
if (help != null) el.help = help;
el.disabled = !!disabled;
const h = (e) => onChange?.(e.detail.value); // composed CustomEvent
el.addEventListener('change', h);
return () => el.removeEventListener('change', h);
}, [options, value, variant, labelVariant, help, disabled, onChange]);
return <aha-mode-field ref={ref} label={label}>{children}</aha-mode-field>;
}
// usage
<AhaModeField
label="Results"
value={resultsMode}
options={[{ value: 'auto', label: 'Automatic' }, { value: 'manual', label: 'Manual' }]}
help="Choose when the results chart appears."
onChange={setResultsMode}
>
<div data-mode="auto">Results appear automatically.</div>
<div data-mode="manual">You reveal results manually.</div>
</AhaModeField>
// main.ts — register the element + mark aha-* as custom elements
import '@ahaslides-product/design/aha-mode-field'; // registers <aha-mode-field>
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');
// Component.vue
<script setup>
import { ref } from 'vue';
const resultsMode = ref('auto');
const options = [{ value: 'auto', label: 'Automatic' }, { value: 'manual', label: 'Manual' }];
</script>
<template>
<!-- options + value bound as PROPERTIES; bodies are children tagged data-mode -->
<!-- variant="radio" (default outline) | "segmented"; label-variant="field" | "section" -->
<aha-mode-field
label="Results"
help="Choose when the results chart appears."
:options.prop="options"
:value.prop="resultsMode"
@change="resultsMode = $event.detail.value"
>
<div data-mode="auto">Results appear automatically.</div>
<div data-mode="manual">You reveal results manually.</div>
</aha-mode-field>
</template>
API
| Prop | Type | Default | Notes |
|---|---|---|---|
label | string | — | The field label, left of the mode control |
options | Array<{value,label,icon?}> | [] | The exclusive modes; set as a property or a JSON attribute (alias: modes) |
value | string | — | The active option's value (alias: mode) |
variant | 'radio' | 'segmented' | 'radio' | radio = outline segmented (brand border, white fill); segmented = neutral grey |
labelVariant | 'field' | 'section' | 'field' | field = regular weight; section = semibold header for a grouped-list body (attr: label-variant) |
help | string | — | When set, a "?" tooltip after the label (icon system-question-mark) |
disabled | boolean | false | Non-interactive; grey buttons |
change | CustomEvent<{value}> | — | Composed event on change; read e.detail.value (e.detail.mode is a back-compat alias) |
Install
# .npmrc — once: point the @ahaslides-product scope at GitHub Packages
@ahaslides-product:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} # a GitHub token with read:packages
npm i @ahaslides-product/design
import '@ahaslides-product/design/tokens.css'; // once, at the app root
import '@ahaslides-product/design/aha-mode-field'; // registers <aha-mode-field>
Agent feed for this component (absolute, fetchable anywhere): mode-field.agent.json · mode-field.md · mode-field.llms.txt
When to use
When to use
- Mode field — a single setting whose control has 2+ exclusive modes and a mode-specific body (automatic vs manual, value-source A vs B)
- Segmented — an exclusive choice with NO mode-specific body to swap
- Switch — a plain immediate on/off with no modes
Only the active value's body is live — the others stay in the DOM (persistent) but hidden, so switching back restores their state. Default `variant='radio'` is an OUTLINE segmented control (brand border + brand text on white); a solid brand-filled pill is NOT the mode-field look — pick `variant='segmented'` for the neutral grey alternative. A mode toggle for an action may keep a verb-led label ('Show results manually') per SETTINGS-01.
Surfaces
editor settings
Spec
Head label LEFT / mode control RIGHT on one row; gap 12, wraps (row-gap 8) on narrow; 32 min-height · Radio variant (default) OUTLINE segmented, 32 high, semibold 13; selected = #6A1EBB border + #6A1EBB text on WHITE fill (no tint pill) · Segmented variant neutral grey control — #F7F7F7 track, secondary-grey labels, active = white raised on default-text · Label variant field = regular 400; section = semibold 600 (a header for a grouped-list body) · Help optional "?" tooltip (system-question-mark) after the label when `help` is set; opens below (bottom-start) so it clears the panel top, and the bubble wraps within 240px · Body the active value's light-DOM child; others hidden in place (persistent, keep state); margin-top 12; de-emphasised help text — secondary #4A4A4A, 13/20, regular — so the label stays the field's primary line · Hover #D3B4FF border + #6A1EBB label on a radio-variant button · Disabled #F1F1F1 fill, #EBEBEB border, not-allowed