Components · Data Entry
Select field
Pick one value from a short, known set — a lightweight native-select-backed field that embeds anywhere.
Examples
Size
Status
Disabled
Select field · native-<select>-backed leaf · the playground drives this one element (Size · Status · Disabled)
Sizes · small 24 · default 32 · large 40
States · placeholder · value · error · warning · disabled
In a settings row · the leaf is the control aha-settings-list reuses (control.type "select")
<!-- Paste-and-run: save as .html and open in a browser. No build step.
<aha-select> is the SAME shared custom element React and Vue consume. It's a styled control
backed by a NATIVE <select>, so it inherits the OS keyboard model + a11y for free. Pass the
`options` as JSON and an optional `placeholder`; it emits a composed `change` event. -->
<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/icons.js'; // <aha-icon> — the trailing caret
import 'https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/aha-select.js'; // registers <aha-select>
</script>
<aha-select id="theme" placeholder="Pick a theme"
options='[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"High contrast","value":"contrast"}]'></aha-select>
<script>
// Plain DOM — no framework. Read e.detail.value off the composed CustomEvent.
document.getElementById('theme').addEventListener('change', (e) => {
console.log('theme:', e.detail.value);
});
</script>
import '@ahaslides-product/design/aha-select'; // registers <aha-select>
import '@ahaslides-product/design/icons'; // <aha-icon> — the trailing caret
import { useRef, useEffect } from 'react';
// React 18 needs a thin wrapper (options is a property, not a string attribute); React 19 can use
// <aha-select> directly. Same element, same shadow-DOM CSS → byte-identical with Vue.
function AhaSelect({ value, options, placeholder, size, status, disabled, onChange }) {
const ref = useRef();
useEffect(() => {
const el = ref.current;
el.options = options || []; // set the array as a property
if (value != null) el.value = value;
el.disabled = !!disabled;
const h = (e) => onChange?.(e.detail.value); // composed CustomEvent
el.addEventListener('change', h);
return () => el.removeEventListener('change', h);
});
return (
<aha-select ref={ref} placeholder={placeholder} size={size} status={status} />
);
}
// usage
<AhaSelect
placeholder="Pick a theme"
value={theme}
options={[
{ label: 'Light', value: 'light' },
{ label: 'Dark', value: 'dark' },
]}
onChange={setTheme}
/>
// main.ts — register the element + mark aha-* as custom elements
import '@ahaslides-product/design/aha-select'; // registers <aha-select>
import '@ahaslides-product/design/icons'; // <aha-icon> — the trailing caret
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');
// Component.vue
<script setup>
import { ref } from 'vue';
const theme = ref('light');
const options = [
{ label: 'Light', value: 'light' },
{ label: 'Dark', value: 'dark' },
];
</script>
<template>
<aha-select
placeholder="Pick a theme"
:value="theme"
:options.prop="options"
@change="theme = $event.detail.value"
></aha-select>
</template>
API
| Prop | Type | Default | Notes |
|---|---|---|---|
options | { label, value }[] | [] | The selectable set — a JSON `options` attribute or an `.options` property |
value | string | — | Selected value (controlled) |
placeholder | string | — | Dimmed hint shown when no value is selected |
size | small | default | large | default | Control height 24 / 32 / 40 |
status | error | warning | — | Validation border colour |
disabled | boolean | false | Non-interactive, muted control |
change | CustomEvent<{value}> | — | Composed event; read e.detail.value |
open | boolean | false | Reflects/controls the themed listbox popup being open (also toggled by click/keyboard). aria-expanded stays in sync. |
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-select'; // registers <aha-select>
Agent feed for this component (absolute, fetchable anywhere): select-field.agent.json · select-field.md · select-field.llms.txt
When to use
When to use
- Select field (this leaf) — one value from a short, known set (≤ ~10), or a select inside a shadow-DOM surface like a settings row — no search/tags/multi needed
- Select (composite) — a long list needing type-to-search, multiple/tags, async loading, or option groups
- Radio — 2–5 mutually-exclusive options all worth showing at once
Pre-select the recommended default rather than leaving it empty. Keep option labels short and sentence case; keep product nouns — Q&A, Word cloud. This is the leaf half of the hybrid — reach for the composite Select the moment you need search, tags, or a virtualised list.
Surfaces
editor dashboard settings audience
Spec
Control height 32 · radius 8 · 1px #E3E3E3 border · Size small 24 · default 32 · large 40 · Focus brand #6A1EBB border + soft ring · Placeholder #8A8A8A · Caret trailing aha-icon system-caret-down (UA arrow suppressed) · Status error #F5222D · warning #FF7747 border · Popup themed role=listbox — white surface, radius 8, elevation shadow, brand-tint hover, check on selected; Arrow/Home/End + typeahead + Escape; outside-click close