Component documentation
Adjust the public props, then interact with the preview. Source follows the current state.
'use client';
import { useState } from 'react';
import { ThemeScope } from '@structured-growth/sg-ui/theme';
import { AsyncMultiSelect, type MultiSelectOption } from '@structured-growth/sg-ui/experimental';
import { Stack, Typography } from '@structured-growth/sg-ui/primitives';
export default function AsyncMultiSelectPlaygroundExample() {
// Local mock results. The host owns fetching, cancellation and stale-response rejection.
const options = [{"id":"design","label":"Design systems"},{"id":"engineering","label":"Engineering"},{"id":"research","label":"Research"},{"id":"archived","label":"Archived course","disabled":true}];
const [query, setQuery] = useState("");
const [selected, setSelected] = useState<MultiSelectOption[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
return (
<>
<ThemeScope density="comfortable">
<Stack gap={3}>
<AsyncMultiSelect label={"Course subject"} disabled={false} readOnly={false} description={"Host-supplied course information."} descriptionPlacement="above" placeholder="Search course subjects" query={query} onQueryChange={setQuery} options={options.filter(option => option.label.toLowerCase().includes(query.toLowerCase()))} value={selected} onValueChange={setSelected} loading={loading} errorMessage={error ? "Sample request failed. Retry to restore local results." : undefined} onRetry={() => { setError(false); setLoading(false); }} name="topics" />
</Stack>
</ThemeScope>
</>
);
}
An independent scope demonstrates the inherited spacing and interaction targets.
'use client';
import { useState } from 'react';
import { ThemeScope } from '@structured-growth/sg-ui/theme';
import { AsyncMultiSelect, type MultiSelectOption } from '@structured-growth/sg-ui/experimental';
import { Stack, Typography } from '@structured-growth/sg-ui/primitives';
export default function AsyncMultiSelectCompactExample() {
// Local mock results. The host owns fetching, cancellation and stale-response rejection.
const options = [{"id":"design","label":"Design systems"},{"id":"engineering","label":"Engineering"},{"id":"research","label":"Research"},{"id":"archived","label":"Archived course","disabled":true}];
const [query, setQuery] = useState("");
const [selected, setSelected] = useState<MultiSelectOption[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
return (
<>
<ThemeScope density="compact">
<Stack gap={3}>
<AsyncMultiSelect label={"Course subject"} disabled={false} readOnly={false} description={"Host-supplied course information."} descriptionPlacement="above" placeholder="Search course subjects" query={query} onQueryChange={setQuery} options={options.filter(option => option.label.toLowerCase().includes(query.toLowerCase()))} value={selected} onValueChange={setSelected} loading={loading} errorMessage={error ? "Sample request failed. Retry to restore local results." : undefined} onRetry={() => { setError(false); setLoading(false); }} name="topics" />
</Stack>
</ThemeScope>
</>
);
}
An independent scope demonstrates the inherited spacing and interaction targets.
'use client';
import { useState } from 'react';
import { ThemeScope } from '@structured-growth/sg-ui/theme';
import { AsyncMultiSelect, type MultiSelectOption } from '@structured-growth/sg-ui/experimental';
import { Stack, Typography } from '@structured-growth/sg-ui/primitives';
export default function AsyncMultiSelectComfortableExample() {
// Local mock results. The host owns fetching, cancellation and stale-response rejection.
const options = [{"id":"design","label":"Design systems"},{"id":"engineering","label":"Engineering"},{"id":"research","label":"Research"},{"id":"archived","label":"Archived course","disabled":true}];
const [query, setQuery] = useState("");
const [selected, setSelected] = useState<MultiSelectOption[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
return (
<>
<ThemeScope density="comfortable">
<Stack gap={3}>
<AsyncMultiSelect label={"Course subject"} disabled={false} readOnly={false} description={"Host-supplied course information."} descriptionPlacement="above" placeholder="Search course subjects" query={query} onQueryChange={setQuery} options={options.filter(option => option.label.toLowerCase().includes(query.toLowerCase()))} value={selected} onValueChange={setSelected} loading={loading} errorMessage={error ? "Sample request failed. Retry to restore local results." : undefined} onRetry={() => { setError(false); setLoading(false); }} name="topics" />
</Stack>
</ThemeScope>
</>
);
}
Callbacks update local host state. No backend persistence is implied.
'use client';
import { useState } from 'react';
import { ThemeScope } from '@structured-growth/sg-ui/theme';
import { AsyncMultiSelect, type MultiSelectOption, Button } from '@structured-growth/sg-ui/experimental';
import { Stack, Typography } from '@structured-growth/sg-ui/primitives';
export default function AsyncMultiSelectFormExample() {
// Local mock results. The host owns fetching, cancellation and stale-response rejection.
const options = [{"id":"design","label":"Design systems"},{"id":"engineering","label":"Engineering"},{"id":"research","label":"Research"},{"id":"archived","label":"Archived course","disabled":true}];
const [query, setQuery] = useState("");
const [selected, setSelected] = useState<MultiSelectOption[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [submits, setSubmits] = useState(0);
return (
<>
<ThemeScope density="comfortable">
<form onSubmit={event => { event.preventDefault(); setSubmits(count => count + 1); }}>
<Stack gap={3}>
<Stack gap={3}>
<AsyncMultiSelect label={"Course subject"} disabled={false} readOnly={false} description={"Host-supplied course information."} descriptionPlacement="above" placeholder="Search course subjects" query={query} onQueryChange={setQuery} options={options.filter(option => option.label.toLowerCase().includes(query.toLowerCase()))} value={selected} onValueChange={setSelected} loading={loading} errorMessage={error ? "Sample request failed. Retry to restore local results." : undefined} onRetry={() => { setError(false); setLoading(false); }} name="topics" />
</Stack>
<Button type="submit">Save locally</Button>
<Typography role="status">Submitted {submits} times. Connect persistence in your host.</Typography>
</Stack>
</form>
</ThemeScope>
</>
);
}
This field starts empty so its placeholder is visible. The icons are decorative and follow the text direction; they add no tab stops. The visible label remains the accessible name. Selection controls keep their dropdown action; fetching and search results belong to the host.
'use client';
import { useState } from 'react';
import { ThemeScope } from '@structured-growth/sg-ui/theme';
import {AsyncMultiSelect,type MultiSelectOption} from '@structured-growth/sg-ui/experimental';
import {Box} from '@structured-growth/sg-ui/primitives';
export default function AsyncMultiSelectcompactnoneExample() {
const options=[{"id":"design","label":"Design systems"},{"id":"research","label":"User research"},{"id":"engineering","label":"Engineering"}];
const [query,setQuery]=useState("");
const [selected,setSelected]=useState<MultiSelectOption[]>([]);
return (
<>
<ThemeScope density="compact">
<Box style={{inlineSize:'22rem',maxInlineSize:'100%'}}>
<AsyncMultiSelect label="Course subject" placeholder="Search course subjects" query={query} onQueryChange={setQuery} options={options.filter(option=>option.label.toLowerCase().includes(query.toLowerCase()))} value={selected} onValueChange={setSelected}/>
</Box>
</ThemeScope>
</>
);
}
This field starts empty so its placeholder is visible. The icons are decorative and follow the text direction; they add no tab stops. The visible label remains the accessible name. Selection controls keep their dropdown action; fetching and search results belong to the host.
'use client';
import { useState } from 'react';
import { ThemeScope } from '@structured-growth/sg-ui/theme';
import {AsyncMultiSelect,type MultiSelectOption} from '@structured-growth/sg-ui/experimental';
import {Box} from '@structured-growth/sg-ui/primitives';
import {SearchIcon} from '@structured-growth/sg-ui/icons';
export default function AsyncMultiSelectcompactstartExample() {
const options=[{"id":"design","label":"Design systems"},{"id":"research","label":"User research"},{"id":"engineering","label":"Engineering"}];
const [query,setQuery]=useState("");
const [selected,setSelected]=useState<MultiSelectOption[]>([]);
return (
<>
<ThemeScope density="compact">
<Box style={{inlineSize:'22rem',maxInlineSize:'100%'}}>
<AsyncMultiSelect label="Course subject" placeholder="Search course subjects" startIcon={<SearchIcon size={16}/>} query={query} onQueryChange={setQuery} options={options.filter(option=>option.label.toLowerCase().includes(query.toLowerCase()))} value={selected} onValueChange={setSelected}/>
</Box>
</ThemeScope>
</>
);
}
This field starts empty so its placeholder is visible. The icons are decorative and follow the text direction; they add no tab stops. The visible label remains the accessible name. Selection controls keep their dropdown action; fetching and search results belong to the host.
'use client';
import { useState } from 'react';
import { ThemeScope } from '@structured-growth/sg-ui/theme';
import {AsyncMultiSelect,type MultiSelectOption} from '@structured-growth/sg-ui/experimental';
import {Box} from '@structured-growth/sg-ui/primitives';
import {SchoolIcon} from '@structured-growth/sg-ui/icons';
export default function AsyncMultiSelectcompactendExample() {
const options=[{"id":"design","label":"Design systems"},{"id":"research","label":"User research"},{"id":"engineering","label":"Engineering"}];
const [query,setQuery]=useState("");
const [selected,setSelected]=useState<MultiSelectOption[]>([]);
return (
<>
<ThemeScope density="compact">
<Box style={{inlineSize:'22rem',maxInlineSize:'100%'}}>
<AsyncMultiSelect label="Course subject" placeholder="Search course subjects" endIcon={<SchoolIcon size={16}/>} query={query} onQueryChange={setQuery} options={options.filter(option=>option.label.toLowerCase().includes(query.toLowerCase()))} value={selected} onValueChange={setSelected}/>
</Box>
</ThemeScope>
</>
);
}
This field starts empty so its placeholder is visible. The icons are decorative and follow the text direction; they add no tab stops. The visible label remains the accessible name. Selection controls keep their dropdown action; fetching and search results belong to the host.
'use client';
import { useState } from 'react';
import { ThemeScope } from '@structured-growth/sg-ui/theme';
import {AsyncMultiSelect,type MultiSelectOption} from '@structured-growth/sg-ui/experimental';
import {Box} from '@structured-growth/sg-ui/primitives';
import {SearchIcon,SchoolIcon} from '@structured-growth/sg-ui/icons';
export default function AsyncMultiSelectcompactbothExample() {
const options=[{"id":"design","label":"Design systems"},{"id":"research","label":"User research"},{"id":"engineering","label":"Engineering"}];
const [query,setQuery]=useState("");
const [selected,setSelected]=useState<MultiSelectOption[]>([]);
return (
<>
<ThemeScope density="compact">
<Box style={{inlineSize:'22rem',maxInlineSize:'100%'}}>
<AsyncMultiSelect label="Course subject" placeholder="Search course subjects" startIcon={<SearchIcon size={16}/>} endIcon={<SchoolIcon size={16}/>} query={query} onQueryChange={setQuery} options={options.filter(option=>option.label.toLowerCase().includes(query.toLowerCase()))} value={selected} onValueChange={setSelected}/>
</Box>
</ThemeScope>
</>
);
}
This field starts empty so its placeholder is visible. The icons are decorative and follow the text direction; they add no tab stops. The visible label remains the accessible name. Selection controls keep their dropdown action; fetching and search results belong to the host.
'use client';
import { useState } from 'react';
import { ThemeScope } from '@structured-growth/sg-ui/theme';
import {AsyncMultiSelect,type MultiSelectOption} from '@structured-growth/sg-ui/experimental';
import {Box} from '@structured-growth/sg-ui/primitives';
export default function AsyncMultiSelectcomfortablenoneExample() {
const options=[{"id":"design","label":"Design systems"},{"id":"research","label":"User research"},{"id":"engineering","label":"Engineering"}];
const [query,setQuery]=useState("");
const [selected,setSelected]=useState<MultiSelectOption[]>([]);
return (
<>
<ThemeScope density="comfortable">
<Box style={{inlineSize:'22rem',maxInlineSize:'100%'}}>
<AsyncMultiSelect label="Course subject" placeholder="Search course subjects" query={query} onQueryChange={setQuery} options={options.filter(option=>option.label.toLowerCase().includes(query.toLowerCase()))} value={selected} onValueChange={setSelected}/>
</Box>
</ThemeScope>
</>
);
}
This field starts empty so its placeholder is visible. The icons are decorative and follow the text direction; they add no tab stops. The visible label remains the accessible name. Selection controls keep their dropdown action; fetching and search results belong to the host.
'use client';
import { useState } from 'react';
import { ThemeScope } from '@structured-growth/sg-ui/theme';
import {AsyncMultiSelect,type MultiSelectOption} from '@structured-growth/sg-ui/experimental';
import {Box} from '@structured-growth/sg-ui/primitives';
import {SearchIcon} from '@structured-growth/sg-ui/icons';
export default function AsyncMultiSelectcomfortablestartExample() {
const options=[{"id":"design","label":"Design systems"},{"id":"research","label":"User research"},{"id":"engineering","label":"Engineering"}];
const [query,setQuery]=useState("");
const [selected,setSelected]=useState<MultiSelectOption[]>([]);
return (
<>
<ThemeScope density="comfortable">
<Box style={{inlineSize:'22rem',maxInlineSize:'100%'}}>
<AsyncMultiSelect label="Course subject" placeholder="Search course subjects" startIcon={<SearchIcon size={16}/>} query={query} onQueryChange={setQuery} options={options.filter(option=>option.label.toLowerCase().includes(query.toLowerCase()))} value={selected} onValueChange={setSelected}/>
</Box>
</ThemeScope>
</>
);
}
This field starts empty so its placeholder is visible. The icons are decorative and follow the text direction; they add no tab stops. The visible label remains the accessible name. Selection controls keep their dropdown action; fetching and search results belong to the host.
'use client';
import { useState } from 'react';
import { ThemeScope } from '@structured-growth/sg-ui/theme';
import {AsyncMultiSelect,type MultiSelectOption} from '@structured-growth/sg-ui/experimental';
import {Box} from '@structured-growth/sg-ui/primitives';
import {SchoolIcon} from '@structured-growth/sg-ui/icons';
export default function AsyncMultiSelectcomfortableendExample() {
const options=[{"id":"design","label":"Design systems"},{"id":"research","label":"User research"},{"id":"engineering","label":"Engineering"}];
const [query,setQuery]=useState("");
const [selected,setSelected]=useState<MultiSelectOption[]>([]);
return (
<>
<ThemeScope density="comfortable">
<Box style={{inlineSize:'22rem',maxInlineSize:'100%'}}>
<AsyncMultiSelect label="Course subject" placeholder="Search course subjects" endIcon={<SchoolIcon size={16}/>} query={query} onQueryChange={setQuery} options={options.filter(option=>option.label.toLowerCase().includes(query.toLowerCase()))} value={selected} onValueChange={setSelected}/>
</Box>
</ThemeScope>
</>
);
}
This field starts empty so its placeholder is visible. The icons are decorative and follow the text direction; they add no tab stops. The visible label remains the accessible name. Selection controls keep their dropdown action; fetching and search results belong to the host.
'use client';
import { useState } from 'react';
import { ThemeScope } from '@structured-growth/sg-ui/theme';
import {AsyncMultiSelect,type MultiSelectOption} from '@structured-growth/sg-ui/experimental';
import {Box} from '@structured-growth/sg-ui/primitives';
import {SearchIcon,SchoolIcon} from '@structured-growth/sg-ui/icons';
export default function AsyncMultiSelectcomfortablebothExample() {
const options=[{"id":"design","label":"Design systems"},{"id":"research","label":"User research"},{"id":"engineering","label":"Engineering"}];
const [query,setQuery]=useState("");
const [selected,setSelected]=useState<MultiSelectOption[]>([]);
return (
<>
<ThemeScope density="comfortable">
<Box style={{inlineSize:'22rem',maxInlineSize:'100%'}}>
<AsyncMultiSelect label="Course subject" placeholder="Search course subjects" startIcon={<SearchIcon size={16}/>} endIcon={<SchoolIcon size={16}/>} query={query} onQueryChange={setQuery} options={options.filter(option=>option.label.toLowerCase().includes(query.toLowerCase()))} value={selected} onValueChange={setSelected}/>
</Box>
</ThemeScope>
</>
);
}
An independent scope demonstrates the inherited spacing and interaction targets.
'use client';
import { useState } from 'react';
import { ThemeScope } from '@structured-growth/sg-ui/theme';
import { AsyncMultiSelect, type MultiSelectOption } from '@structured-growth/sg-ui/experimental';
import { Stack, Typography } from '@structured-growth/sg-ui/primitives';
export default function AsyncMultiSelectCompactExample() {
// Local mock results. The host owns fetching, cancellation and stale-response rejection.
const options = [{"id":"design","label":"Design systems"},{"id":"engineering","label":"Engineering"},{"id":"research","label":"Research"},{"id":"archived","label":"Archived course","disabled":true}];
const [query, setQuery] = useState("");
const [selected, setSelected] = useState<MultiSelectOption[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
return (
<>
<ThemeScope density="compact">
<Stack gap={3}>
<AsyncMultiSelect label={"Course subject"} disabled={false} readOnly={false} description={"Host-supplied course information."} descriptionPlacement="above" placeholder="Search course subjects" query={query} onQueryChange={setQuery} options={options.filter(option => option.label.toLowerCase().includes(query.toLowerCase()))} value={selected} onValueChange={setSelected} loading={loading} errorMessage={error ? "Sample request failed. Retry to restore local results." : undefined} onRetry={() => { setError(false); setLoading(false); }} name="topics" />
</Stack>
</ThemeScope>
</>
);
}
An independent scope demonstrates the inherited spacing and interaction targets.
'use client';
import { useState } from 'react';
import { ThemeScope } from '@structured-growth/sg-ui/theme';
import { AsyncMultiSelect, type MultiSelectOption } from '@structured-growth/sg-ui/experimental';
import { Stack, Typography } from '@structured-growth/sg-ui/primitives';
export default function AsyncMultiSelectCompactExample() {
// Local mock results. The host owns fetching, cancellation and stale-response rejection.
const options = [{"id":"design","label":"Design systems"},{"id":"engineering","label":"Engineering"},{"id":"research","label":"Research"},{"id":"archived","label":"Archived course","disabled":true}];
const [query, setQuery] = useState("");
const [selected, setSelected] = useState<MultiSelectOption[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
return (
<>
<ThemeScope density="compact">
<Stack gap={3}>
<AsyncMultiSelect label={"Course subject"} disabled={false} readOnly={false} description={"Host-supplied course information."} descriptionPlacement="below" placeholder="Search course subjects" query={query} onQueryChange={setQuery} options={options.filter(option => option.label.toLowerCase().includes(query.toLowerCase()))} value={selected} onValueChange={setSelected} loading={loading} errorMessage={error ? "Sample request failed. Retry to restore local results." : undefined} onRetry={() => { setError(false); setLoading(false); }} name="topics" />
</Stack>
</ThemeScope>
</>
);
}
An independent scope demonstrates the inherited spacing and interaction targets.
'use client';
import { useState } from 'react';
import { ThemeScope } from '@structured-growth/sg-ui/theme';
import { AsyncMultiSelect, type MultiSelectOption } from '@structured-growth/sg-ui/experimental';
import { Stack, Typography } from '@structured-growth/sg-ui/primitives';
export default function AsyncMultiSelectComfortableExample() {
// Local mock results. The host owns fetching, cancellation and stale-response rejection.
const options = [{"id":"design","label":"Design systems"},{"id":"engineering","label":"Engineering"},{"id":"research","label":"Research"},{"id":"archived","label":"Archived course","disabled":true}];
const [query, setQuery] = useState("");
const [selected, setSelected] = useState<MultiSelectOption[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
return (
<>
<ThemeScope density="comfortable">
<Stack gap={3}>
<AsyncMultiSelect label={"Course subject"} disabled={false} readOnly={false} description={"Host-supplied course information."} descriptionPlacement="above" placeholder="Search course subjects" query={query} onQueryChange={setQuery} options={options.filter(option => option.label.toLowerCase().includes(query.toLowerCase()))} value={selected} onValueChange={setSelected} loading={loading} errorMessage={error ? "Sample request failed. Retry to restore local results." : undefined} onRetry={() => { setError(false); setLoading(false); }} name="topics" />
</Stack>
</ThemeScope>
</>
);
}
An independent scope demonstrates the inherited spacing and interaction targets.
'use client';
import { useState } from 'react';
import { ThemeScope } from '@structured-growth/sg-ui/theme';
import { AsyncMultiSelect, type MultiSelectOption } from '@structured-growth/sg-ui/experimental';
import { Stack, Typography } from '@structured-growth/sg-ui/primitives';
export default function AsyncMultiSelectComfortableExample() {
// Local mock results. The host owns fetching, cancellation and stale-response rejection.
const options = [{"id":"design","label":"Design systems"},{"id":"engineering","label":"Engineering"},{"id":"research","label":"Research"},{"id":"archived","label":"Archived course","disabled":true}];
const [query, setQuery] = useState("");
const [selected, setSelected] = useState<MultiSelectOption[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
return (
<>
<ThemeScope density="comfortable">
<Stack gap={3}>
<AsyncMultiSelect label={"Course subject"} disabled={false} readOnly={false} description={"Host-supplied course information."} descriptionPlacement="below" placeholder="Search course subjects" query={query} onQueryChange={setQuery} options={options.filter(option => option.label.toLowerCase().includes(query.toLowerCase()))} value={selected} onValueChange={setSelected} loading={loading} errorMessage={error ? "Sample request failed. Retry to restore local results." : undefined} onRetry={() => { setError(false); setLoading(false); }} name="topics" />
</Stack>
</ThemeScope>
</>
);
}
A local 350ms adapter models pending results and failure/retry without an endpoint. Query changes and unmount abort the previous request; a freshness guard rejects late responses even if a real adapter ignores cancellation. Selected records persist independently of search results.
'use client';
import { useState } from 'react';
import {useEffect} from 'react';
import {AsyncMultiSelect,type MultiSelectOption} from '@structured-growth/sg-ui/experimental';
export default function AsyncTopicSearchExample() {
const records=[{"id":"design","label":"Design systems"},{"id":"engineering","label":"Engineering"},{"id":"research","label":"Research"}];
function searchRecords(query:string,signal:AbortSignal,fail:boolean):Promise<MultiSelectOption[]> {
return new Promise((resolve,reject)=>{
const timer=setTimeout(()=>{if(fail)reject(new Error('Sample request failed. Retry to search again.'));else resolve(records.filter(record=>record.label.toLowerCase().includes(query.toLowerCase())));},350);
signal.addEventListener('abort',()=>{clearTimeout(timer);reject(new DOMException('Cancelled','AbortError'));},{once:true});
});
}
const [query,setQuery]=useState("");
const [selected,setSelected]=useState<MultiSelectOption[]>([]);
const [options,setOptions]=useState<MultiSelectOption[]>([]);
const [loading,setLoading]=useState(true);
const [error,setError]=useState<string>();
const [retry,setRetry]=useState(0);
useEffect(()=>{
const controller=new AbortController();let current=true;
setLoading(true);setError(undefined);
searchRecords(query,controller.signal,query.toLowerCase()==='fail'&&retry===0).then(results=>{if(current)setOptions(results);}).catch(reason=>{if(current){setOptions([]);setError(reason instanceof Error?reason.message:'Search failed.');}}).finally(()=>{if(current)setLoading(false);});
return ()=>{current=false;controller.abort();};
},[query,retry]);
return (
<>
<AsyncMultiSelect label="Search course topics" description="Type fail for a recoverable request error, then Retry. Change the query to find topics." query={query} onQueryChange={next=>{setRetry(0);setQuery(next);}} options={options} value={selected} onValueChange={setSelected} loading={loading} errorMessage={error} onRetry={()=>setRetry(value=>value+1)} />
</>
);
}