Patterns · Settings
Card select
Single-select as an icon+label card grid — pick one of N options each shown as its own visual card, with roving radiogroup keyboard navigation. Two layouts: labelled tiles (stretch to fill `columns`) or compact 44px icon-only squares.
Examples
Columns
Icon only
Single-select card grid — Tab in, then Arrow keys move-and-select
Icon only — compact 44px squares (label kept as title + aria-label)
Disabled
<!-- Paste-and-run: save as .html and open in a browser. No build step.
<aha-card-select> 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 whole grid is one radiogroup:
Tab in, then Arrow keys move-and-select. Icons are summoned by name — never an inline <svg>. -->
<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-card-select.js'; // registers <aha-card-select>
</script>
<!-- labelled tiles — stretch to fill each of `columns` tracks -->
<aha-card-select id="chart" columns="3" value="bar" style="max-width:360px"></aha-card-select>
<!-- iconOnly — compact 44px squares packed to the start; the label is kept as title + aria-label -->
<aha-card-select id="chartCompact" icon-only columns="3" value="bar"></aha-card-select>
<script type="module">
// options: value + label + an icon NAME from the DS library
const options = [
{ value: 'bar', label: 'Bar', icon: 'system-chart-bar' },
{ value: 'line', label: 'Line', icon: 'system-chart-line-up' },
{ value: 'pie', label: 'Pie', icon: 'system-chart-pie' },
];
const el = document.getElementById('chart');
el.options = options;
el.addEventListener('change', (e) => console.log('chart type:', e.detail.value));
document.getElementById('chartCompact').options = options;
</script>
import '@ahaslides-product/design/aha-card-select'; // registers <aha-card-select>
import { useRef, useEffect } from 'react';
// React 18 needs a thin wrapper; React 19 can use <aha-card-select> directly.
// options is a PROPERTY (array), so set it imperatively; the grid is one roving radiogroup.
function AhaCardSelect({ options, value, columns, iconOnly, disabled, onChange }) {
const ref = useRef();
useEffect(() => {
const el = ref.current;
el.options = options || [];
if (value != null) el.value = value;
el.iconOnly = !!iconOnly; // compact 44px squares; label kept as title + aria-label
el.disabled = !!disabled;
const h = (e) => onChange?.(e.detail.value); // composed CustomEvent
el.addEventListener('change', h);
return () => el.removeEventListener('change', h);
}, [options, value, iconOnly, disabled, onChange]);
return <aha-card-select ref={ref} {...(columns ? { columns: String(columns) } : {})} />;
}
// usage — labelled tiles (default) or compact iconOnly squares
<AhaCardSelect
value={chartType}
columns={3}
iconOnly={false}
options={[
{ value: 'bar', label: 'Bar', icon: 'system-chart-bar' },
{ value: 'line', label: 'Line', icon: 'system-chart-line-up' },
{ value: 'pie', label: 'Pie', icon: 'system-chart-pie' },
]}
onChange={setChartType}
/>
// main.ts — register the element + mark aha-* as custom elements
import '@ahaslides-product/design/aha-card-select'; // registers <aha-card-select>
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');
// Component.vue
<script setup>
import { ref } from 'vue';
const chartType = ref('bar');
const options = [
{ value: 'bar', label: 'Bar', icon: 'system-chart-bar' },
{ value: 'line', label: 'Line', icon: 'system-chart-line-up' },
{ value: 'pie', label: 'Pie', icon: 'system-chart-pie' },
];
</script>
<template>
<!-- options + value are bound as PROPERTIES (.prop); the grid is one roving radiogroup -->
<!-- add icon-only for compact 44px squares (label kept as title + aria-label) -->
<aha-card-select
columns="3"
:options.prop="options"
:value.prop="chartType"
@change="chartType = $event.detail.value"
/>
</template>
API
| Prop | Type | Default | Notes |
|---|---|---|---|
options | Array<{value,label,icon}> | [] | The cards — each an icon name + a label; set as a property or a JSON attribute |
value | string | — | The selected option's value (single-select) |
columns | number | 3 | Grid column count — labelled tiles fill each track; iconOnly lays out that many 44px squares |
iconOnly | boolean | false | Compact layout — fixed 44px icon squares packed to the start; the label is hidden but kept as title + aria-label |
disabled | boolean | false | Non-interactive; grey fill |
change | CustomEvent<{value}> | — | Composed event on selection; read e.detail.value |
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-card-select'; // registers <aha-card-select>
Agent feed for this component (absolute, fetchable anywhere): card-select.agent.json · card-select.md · card-select.llms.txt
When to use
When to use
- Card select — pick one of N options, each carrying a visual (icon/preview) — a chart-type, layout or mode picker
- Segmented — 2–4 short text-only options in one row (SETTINGS-38)
- Select — a longer or space-constrained set with no per-option visual
Reach for CardSelect only when each option carries a visual. Do not hand-roll a grid of bespoke <button>/<div> tiles with inline <svg> icons and a manual selected state (SETTINGS-51) — the roving radiogroup a11y is the hard part and it lives here.
Surfaces
editor settings
Spec
Card icon 24 over label; padding 16 12; radius 8; 1px #D4D4D4 border · Grid repeat(columns, minmax(0,1fr)) — tiles stretch to fill; columns default 3; 8px gap · iconOnly repeat(columns, 44px) — fixed 44px squares packed to the start; label hidden, kept as title + aria-label · Selected #6A1EBB border + #F9F5FF tint + #6A1EBB label/icon (persistent node) · Hover #D3B4FF border · Keyboard roving radiogroup — one tab-stop; Arrow keys move-and-select; Space/Enter select · Disabled #F1F1F1 fill, #EBEBEB border, not-allowed