Skip to content
edmi-ui

Suggestion

Suggested prompts as pills in a scrollable row, with a card variant for the home state.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Built on button (outline, pill).

Terminal window
npx shadcn@latest add @edmi-ui/ai-suggestion
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/ai-suggestion
Terminal window
yarn dlx shadcn@latest add @edmi-ui/ai-suggestion
Terminal window
bunx shadcn@latest add @edmi-ui/ai-suggestion

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/ai-suggestion
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/ai-suggestion
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/ai-suggestion
Terminal window
bunx shadcn-vue@latest add @edmi-ui/ai-suggestion

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-suggestion.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-suggestion.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-suggestion.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-suggestion.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import { Suggestion, Suggestions } from "@/components/ai/suggestion";
<div className="flex w-full max-w-2xl flex-col gap-4">
<Suggestions>
</Suggestions>
<div className="flex flex-wrap gap-2">
<Suggestion suggestion="Rebalance now" variant="card">
Check drift and propose trades
</Suggestion>
<Suggestion suggestion="Draft a post" variant="card">
Write an update for the feed
</Suggestion>
</div>
</div>
<script setup lang="ts">
import { Suggestion, Suggestions } from "@/components/ai/suggestion";
</script>
<template>
<div class="flex w-full max-w-2xl flex-col gap-4">
<Suggestions>
<Suggestion :key="p" :suggestion="p" />
</Suggestions>
<div class="flex flex-wrap gap-2">
<Suggestion suggestion="Rebalance now" variant="card">Check drift and propose trades</Suggestion>
<Suggestion suggestion="Draft a post" variant="card">Write an update for the feed</Suggestion>
</div>
</div>
</template>
<script lang="ts">
import { Suggestion, Suggestions } from "$lib/components/ai/suggestion";
</script>
<div class="flex w-full max-w-2xl flex-col gap-4">
<Suggestions>
{#each prompts as p (p)}
<Suggestion suggestion={p} />
{/each}
</Suggestions>
<div class="flex flex-wrap gap-2">
<Suggestion suggestion="Rebalance now" variant="card">Check drift and propose trades</Suggestion>
<Suggestion suggestion="Draft a post" variant="card">Write an update for the feed</Suggestion>
</div>
</div>
registry/components/ai/suggestion.tsx
"use client";
import { cn } from "cn";
import type { ComponentProps } from "react";
import { useCallback } from "react";
import { Button } from "@/registry/edmi/ui/button";
import { ScrollArea, ScrollBar } from "@/registry/edmi/ui/scroll-area";
export type SuggestionsProps = ComponentProps<typeof ScrollArea>;
/** Row of suggestions; scrolls horizontally with a fade at both edges on narrow screens. */
export const Suggestions = ({
className,
children,
...props
}: SuggestionsProps) => (
<ScrollArea
data-slot="ai-suggestions"
className="w-full overflow-x-auto whitespace-nowrap"
{...props}
>
<div className={cn("flex w-max flex-nowrap items-center gap-2", className)}>
{children}
</div>
<ScrollBar className="hidden" orientation="horizontal" />
</ScrollArea>
);
type ButtonVariant = NonNullable<ComponentProps<typeof Button>["variant"]>;
export type SuggestionProps = Omit<
ComponentProps<typeof Button>,
"onClick" | "variant"
> & {
suggestion: string;
onClick?: (suggestion: string) => void;
/**
* `chip` (default): pill. `card` ✦: a larger tile with a lead-in line, for the home state
* (`children` = description).
*/
variant?: "chip" | "card" | ButtonVariant;
};
export const Suggestion = ({
suggestion,
onClick,
className,
variant = "chip",
size = "sm",
children,
...props
}: SuggestionProps) => {
const handleClick = useCallback(() => {
onClick?.(suggestion);
}, [onClick, suggestion]);
if (variant === "card") {
return (
<Button
data-slot="ai-suggestion"
data-variant="card"
className={cn(
"h-auto min-w-44 flex-col items-start gap-1 rounded-xl px-4 py-3 text-left whitespace-normal",
className,
)}
onClick={handleClick}
type="button"
variant="outline"
{...props}
>
<span className="text-[13.5px] font-medium">{suggestion}</span>
{children && (
<span className="text-xs font-normal text-muted-foreground">
{children}
</span>
)}
</Button>
);
}
return (
<Button
data-slot="ai-suggestion"
data-variant="chip"
className={cn("rounded-full px-4", className)}
onClick={handleClick}
size={size}
type="button"
variant={variant === "chip" ? "outline" : variant}
{...props}
>
{children || suggestion}
</Button>
);
};
registry/components/ai/suggestion/Suggestion.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import type { ButtonVariants } from "@/registry/edmi/ui/button"
import { computed } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { Button } from "@/registry/edmi/ui/button"
import type { Elevation } from "@/registry/edmi/ui/elevation"
const props = withDefaults(defineProps<{
suggestion: string
class?: HTMLAttributes["class"]
/**
* `chip` (default): pill. `card` ✦: a larger tile with a lead-in line, for the home state
* (slot = description).
*/
variant?: "chip" | "card" | NonNullable<ButtonVariants["variant"]>
size?: ButtonVariants["size"]
/** ✦ depth: sunken -1, flat 0, raised +1, floating +2 (follows the Button rules). */
elevation?: Elevation
}>(), {
variant: "chip",
size: "sm",
elevation: undefined,
})
const emit = defineEmits<{
(e: "click", suggestion: string): void
}>()
const isCard = computed(() => props.variant === "card")
const buttonVariant = computed(() =>
props.variant === "chip" || props.variant === "card" ? "outline" : props.variant)
</script>
<template>
<Button
data-slot="ai-suggestion"
:data-variant="isCard ? 'card' : 'chip'"
type="button"
:variant="buttonVariant"
:size="isCard ? undefined : size"
:elevation="elevation"
:class="cn(
isCard
? 'h-auto min-w-44 flex-col items-start gap-1 rounded-xl px-4 py-3 text-left whitespace-normal'
: 'rounded-full px-4',
props.class,
)"
@click="emit('click', suggestion)"
>
<template v-if="isCard">
<span class="text-[13.5px] font-medium">{{ suggestion }}</span>
<span v-if="$slots.default" class="text-xs font-normal text-muted-foreground"><slot /></span>
</template>
<slot v-else>{{ suggestion }}</slot>
</Button>
</template>
registry/components/ai/suggestion/Suggestions.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { ScrollArea, ScrollBar } from "@/registry/edmi/ui/scroll-area"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<!-- Row of suggestions; scrolls horizontally. -->
<ScrollArea data-slot="ai-suggestions" class="w-full overflow-x-auto whitespace-nowrap">
<div :class="cn('flex w-max flex-nowrap items-center gap-2', props.class)">
<slot />
</div>
<ScrollBar class="hidden" orientation="horizontal" />
</ScrollArea>
</template>
registry/components/ai/suggestion/index.ts
export { default as Suggestion } from "./Suggestion.vue";
export { default as Suggestions } from "./Suggestions.vue";
src/lib/registry/ai/suggestion/index.ts
import Suggestion from "./suggestion.svelte";
import Suggestions from "./suggestions.svelte";
export { Suggestion, Suggestions };
src/lib/registry/ai/suggestion/suggestion.svelte
<script lang="ts">
import { Button, type ButtonProps } from "$lib/registry/ui/button/index.js";
import { cn } from "$lib/utils.js";
import type { Snippet } from "svelte";
type ButtonVariant = NonNullable<ButtonProps["variant"]>;
let {
suggestion,
onclick,
class: className,
variant = "chip",
size = "sm",
children,
...restProps
}: Omit<ButtonProps, "onclick" | "variant" | "href"> & {
suggestion: string;
onclick?: (suggestion: string) => void;
/**
* `chip` (default): pill. `card` ✦: a larger tile with a lead-in line, for the home state
* (`children` = description).
*/
variant?: "chip" | "card" | ButtonVariant;
children?: Snippet;
} = $props();
const isCard = $derived(variant === "card");
const buttonVariant = $derived<ButtonVariant>(
variant === "chip" || variant === "card" ? "outline" : variant
);
</script>
<Button
data-slot="ai-suggestion"
data-variant={isCard ? "card" : "chip"}
type="button"
variant={buttonVariant}
size={isCard ? undefined : size}
class={cn(
isCard
? "h-auto min-w-44 flex-col items-start gap-1 rounded-xl px-4 py-3 text-left whitespace-normal"
: "rounded-full px-4",
className
)}
onclick={() => onclick?.(suggestion)}
{...restProps}
>
{#if isCard}
<span class="text-[13.5px] font-medium">{suggestion}</span>
{#if children}
<span class="text-xs font-normal text-muted-foreground">{@render children()}</span>
{/if}
{:else if children}
{@render children()}
{:else}
{suggestion}
{/if}
</Button>
src/lib/registry/ai/suggestion/suggestions.svelte
<script lang="ts">
import { ScrollArea } from "$lib/registry/ui/scroll-area/index.js";
import { cn } from "$lib/utils.js";
import type { Snippet } from "svelte";
let {
children,
class: className,
...restProps
}: {
children?: Snippet;
class?: string;
} = $props();
</script>
<!-- Row of suggestions; scrolls horizontally. -->
<ScrollArea
data-slot="ai-suggestions"
orientation="horizontal"
scrollbarXClasses="hidden"
class="w-full whitespace-nowrap"
{...restProps}
>
<div class={cn("flex w-max flex-nowrap items-center gap-2", className)}>
{@render children?.()}
</div>
</ScrollArea>

See the Elevation guide for the levels, roles and layered mode.

Suggestions are Buttons, so they follow the Button levels: flat by default, sunken (-1), raised (+1) and floating (+2).

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Sunken (-1)

Flat (0)

Raised (+1)

Floating (+2)

Sunken (-1)

Flat (0)

Raised (+1)

Floating (+2)

Sunken (-1)

Flat (0)

Raised (+1)

Floating (+2)

Part Prop Type Default
Suggestion suggestion, onClick string, (suggestion) => void
Suggestion variant chip | card | Button variant chip
Suggestion elevation "auto" | "sunken" | "flat" | "raised" | "floating" "auto"
  • variant="card": a larger tile with a lead-in line; children is the muted description.