Patterns · Settings
Option row
The repeatable option/answer row — one bordered row with an inside drag handle, a borderless counted textarea, an optional correct toggle (circle or square) and per-option image, and a floating hover delete; marking correct paints the whole card success-green (SETTINGS-20/34/23/39).
Examples
Orderable
Correct toggle
Toggle shape
Marked correct
Option list anatomy — one bordered row, drag handle inside, borderless field; hover for the delete (top-right). A correct row paints the whole card success-green with the circle toggle filled green + white tick
Correct-toggle shapes — circle (default) vs square (checkboxShape="square"); the second row is marked correct (green card)
Single-line answers (singleLine) — Enter is blocked, value trims on blur
Unordered set — no drag handle (SETTINGS-39), no correct toggle
<!-- Paste-and-run: save as .html and open in a browser. No build step.
<aha-option-row> is the SAME shared custom element React and Vue consume — here in its native form.
It COMPOSES DS leaves (borderless CountedTextarea, ImageActionButton) + a success-green correct
toggle (circle | square). Theming comes only from the --aha-* tokens in tokens.css. Hover a row to
reveal its delete (top-right). -->
<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-option-row.js'; // registers <aha-option-row>
</script>
<div style="display:flex; flex-direction:column; gap:8px; max-width:420px">
<!-- quiz answer: orderable + correct toggle (circle, default) + image; correct paints the card green.
delete disabled at the minimum of 2. Use checkboxshape="square" for the square toggle,
and singleline for a single-line answer (Enter blocked, trims on blur). -->
<aha-option-row id="a" orderable correctable image correct value="Paris" maxlength="80"></aha-option-row>
<aha-option-row orderable correctable image value="London" maxlength="80"></aha-option-row>
<aha-option-row orderable correctable checkboxshape="square" singleline value="Rome" maxlength="80"></aha-option-row>
</div>
<script>
document.getElementById('a').addEventListener('input', (e) => console.log('text:', e.detail.value));
document.getElementById('a').addEventListener('correct-change', (e) => console.log('correct:', e.detail.correct));
</script>
import '@ahaslides-product/design/aha-option-row'; // registers <aha-option-row>
import { useRef, useEffect } from 'react';
// React 18 needs a thin wrapper; React 19 can use <aha-option-row> directly.
// Composes DS leaves; forwards input / correct-change / delete (+ the image control's intents).
function AhaOptionRow({ value, maxLength, orderable, correctable, correct, checkboxShape, singleLine, image, canDelete, onInput, onCorrect, onDelete }) {
const ref = useRef();
useEffect(() => {
const el = ref.current;
el.value = value ?? '';
if (correct) el.setAttribute('correct', ''); else el.removeAttribute('correct');
const hi = (e) => onInput?.(e.detail.value);
const hc = (e) => onCorrect?.(e.detail.correct);
const hd = () => onDelete?.();
el.addEventListener('input', hi); el.addEventListener('correct-change', hc); el.addEventListener('delete', hd);
return () => { el.removeEventListener('input', hi); el.removeEventListener('correct-change', hc); el.removeEventListener('delete', hd); };
});
return (
<aha-option-row
ref={ref}
{...(maxLength ? { maxlength: String(maxLength) } : {})}
{...(orderable ? { orderable: '' } : {})}
{...(correctable ? { correctable: '' } : {})}
{...(checkboxShape ? { checkboxshape: checkboxShape } : {})}
{...(singleLine ? { singleline: '' } : {})}
{...(image ? { image: '' } : {})}
{...(canDelete === false ? { candelete: 'false' } : {})}
/>
);
}
// usage — one per option; disable delete at the minimum count
{options.map((o, i) => (
<AhaOptionRow key={o.id} value={o.text} maxLength={80} orderable correctable image
checkboxShape="circle" correct={o.correct} canDelete={options.length > 2}
onInput={(v) => update(i, v)} onCorrect={(c) => setCorrect(i, c)} onDelete={() => remove(i)} />
))}
// main.ts — register the element + mark aha-* as custom elements
import '@ahaslides-product/design/aha-option-row'; // registers <aha-option-row>
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');
// Component.vue
<script setup>
import { ref } from 'vue';
const options = ref([{ id: 1, text: 'Paris', correct: true }, { id: 2, text: 'London', correct: false }]);
function remove(i) { options.value.splice(i, 1); }
</script>
<template>
<!-- one row per option; delete disables (not hides) at the minimum count -->
<aha-option-row
v-for="(o, i) in options"
:key="o.id"
orderable
correctable
image
checkboxshape="circle"
:value.prop="o.text"
maxlength="80"
:correct="o.correct || undefined"
:candelete="options.length > 2 ? 'true' : 'false'"
@input="o.text = $event.detail.value"
@correct-change="o.correct = $event.detail.correct"
@delete="remove(i)"
/>
</template>
API
| Prop | Type | Default | Notes |
|---|---|---|---|
value | string | "" | The option text |
placeholder | string | Option text | Empty-field hint |
maxlength | number | — | Character cap — turns the focus-only counter on |
orderable | boolean | false | Show the drag handle (order carries meaning) — omit for an unordered set (SETTINGS-39) |
correctable | boolean | false | Show the leading correct-answer checkbox (scored types) |
correct | boolean | false | Whether this option is marked correct — paints the whole card success-green + fills the toggle |
checkboxShape | circle | square (attr checkboxshape) | circle | The correct-answer toggle shape — a 20px outline circle, or a radius-6 square; both fill success-green with a white tick when correct |
singleLine | boolean (attr singleline) | false | Single-line answer — blocks Enter (no newline) and trims the value on blur |
image | boolean | false | Show the per-option image control |
imagestate | empty | loading | filled | empty | The image control's state |
imagesrc | string | — | The image thumbnail URL when filled |
candelete | boolean | true | false disables the delete at the minimum item count |
input / correct-change / delete | CustomEvent | — | Composed events; image-add/change/edit/delete are forwarded from the image control |
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-option-row'; // registers <aha-option-row>
Agent feed for this component (absolute, fetchable anywhere): option-row.agent.json · option-row.md · option-row.llms.txt
When to use
When to use
- Option row — a slide type's option / answer list — the "Items" list (SETTINGS-20)
- Numbered item — a repeatable COMPOSITE item with its own body of fields (a Question)
The row is the one border; the field inside is borderless — never a box-in-a-box (SETTINGS-34). The drag handle lives INSIDE the border and only when order matters (SETTINGS-39). The delete is hover-only and disables (not hides) at the minimum count (SETTINGS-23). No "X of Y" counter — the +Add disables at max and delete at min (SETTINGS-36).
Surfaces
editor settings
Spec
Row the ONE border — 1px #E3E3E3, radius 8; field inside is borderless (no double border) · Drag handle system-drag, FIRST child inside the border; shown only when order matters (orderable) · Text borderless counted textarea — focus-only counter, wraps, scrolls beyond ~4 lines (SETTINGS-21) · Correct toggle optional (correctable), scored types only — 20px circle (default) or radius-6 square; fills --aha-color-success with a white tick when correct · Correct card when correct the WHOLE row paints success-green: --aha-color-success border over a faint --aha-bg-positive tint fill · Single line singleLine → Enter never inserts a newline; value is trimmed on blur (single-line answers) · Image optional per-option ImageActionButton (image) — the full state set (SETTINGS-22) · Delete floats -14px OUTSIDE the top-right, hover-only, disabled (not hidden) at min (candelete=false) · Focus focus-within → #6A1EBB row border + ring (success-green ring when correct), on the one persistent border