Patterns · Settings
Question list
An editable list of collapsible questions — each a prompt field plus its OptionRow choices — with a full-width "+ Add question" that disables at max; composes NumberedItem + OptionRow (SETTINGS-31/36).
Examples
Editable questions — each a collapsible card (prompt + choices); full-width "+ Add question" below
<!-- Paste-and-run: save as .html and open in a browser. No build step.
<aha-question-list> is the SAME shared custom element React and Vue consume — here in its native
form. It COMPOSES DS composites — NumberedItem (each question card) + OptionRow (each choice).
Theming comes only from the --aha-* tokens in tokens.css. Collapse a question with its caret. -->
<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-question-list.js'; // registers <aha-question-list>
</script>
<div style="max-width:440px">
<aha-question-list id="quiz" min="1" max="10" minoptions="2" maxoptions="6"></aha-question-list>
</div>
<script type="module">
const el = document.getElementById('quiz');
el.questions = [
{ id: 'q1', prompt: 'Capital of France?', options: [
{ id: 'o1', text: 'Paris', correct: true }, { id: 'o2', text: 'London' } ] },
{ id: 'q2', prompt: 'Largest ocean?', options: [
{ id: 'o3', text: 'Pacific', correct: true }, { id: 'o4', text: 'Atlantic' } ] },
];
el.addEventListener('change', (e) => console.log('questions:', e.detail.questions));
</script>
import '@ahaslides-product/design/aha-question-list'; // registers <aha-question-list>
import { useRef, useEffect } from 'react';
// React 18 needs a thin wrapper; React 19 can use <aha-question-list> directly.
// questions is a PROPERTY (the model); onChange gives the full edited model back.
function AhaQuestionList({ questions, min, max, minOptions, maxOptions, disabled, onChange }) {
const ref = useRef();
useEffect(() => {
const el = ref.current;
el.questions = questions || [];
const h = (e) => onChange?.(e.detail.questions); // composed CustomEvent
el.addEventListener('change', h);
return () => el.removeEventListener('change', h);
}, [questions, onChange]);
return (
<aha-question-list
ref={ref}
{...(min != null ? { min: String(min) } : {})}
{...(max != null ? { max: String(max) } : {})}
{...(minOptions != null ? { minoptions: String(minOptions) } : {})}
{...(maxOptions != null ? { maxoptions: String(maxOptions) } : {})}
{...(disabled ? { disabled: '' } : {})}
/>
);
}
// usage
<AhaQuestionList questions={quiz} min={1} max={10} minOptions={2} maxOptions={6} onChange={setQuiz} />
// main.ts — register the element + mark aha-* as custom elements
import '@ahaslides-product/design/aha-question-list'; // registers <aha-question-list>
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');
// Component.vue
<script setup>
import { ref } from 'vue';
const quiz = ref([
{ id: 'q1', prompt: 'Capital of France?', options: [{ id: 'o1', text: 'Paris', correct: true }, { id: 'o2', text: 'London' }] },
]);
</script>
<template>
<!-- questions bound as a PROPERTY; @change returns the full edited model -->
<aha-question-list
min="1"
max="10"
minoptions="2"
maxoptions="6"
:questions.prop="quiz"
@change="quiz = $event.detail.questions"
/>
</template>
API
| Prop | Type | Default | Notes |
|---|---|---|---|
questions | Array<{id,prompt,options:[{id,text,correct}]}> | [] | The model; set as a property or a JSON attribute |
min | number | 1 | Minimum questions — delete disables at this count |
max | number | ∞ | Maximum questions — +Add disables at this count |
minoptions | number | 2 | Minimum options per question — option delete disables at this count |
maxoptions | number | ∞ | Maximum options per question — +Add option disables at this count |
disabled | boolean | false | Disable adding questions |
change | CustomEvent<{questions}> | — | Composed event on any edit; read e.detail.questions (the full model) |
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-question-list'; // registers <aha-question-list>
Agent feed for this component (absolute, fetchable anywhere): question-list.agent.json · question-list.md · question-list.llms.txt
When to use
When to use
- Question list — an editable set of questions, each with its own prompt + choices (a quiz builder, a survey)
- Option row list — a flat list of options for ONE question — compose OptionRow directly
The primary +Add spans the full panel width (SETTINGS-31) — never hugged to one side. Never a textual "X of Y" count (SETTINGS-36) — the +Add disables at max and delete disables at min. Composes NumberedItem + OptionRow; it does not re-implement the numbered card or the option row.
Surfaces
editor settings
Spec
Question a collapsible NumberedItem — prompt (counted input) + OptionRow choices + "+ Add option" · Add question full-width dashed "+ Add question" (system-plus), disabled at max (SETTINGS-31) · Add option per-question "+ Add option", disabled at maxoptions · Limits no "X of Y" counter — +Add disables at max, delete disables at min (SETTINGS-36) · Delete question delete disables at min; option delete disables at minoptions