Skip to content
edmi-ui

Sandbox

Code execution sandbox: state header with tabs for code and output.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
1const r = await quote("NVDAx")
2console.log(r.price)
188.20
✓ done in 412 ms
1const r = await quote("NVDAx")
2console.log(r.price)
188.20
✓ done in 412 ms
1const r = await quote("NVDAx")
2console.log(r.price)
188.20
✓ done in 412 ms

Built on collapsible, tabs and badge.

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

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-sandbox
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/ai-sandbox
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/ai-sandbox
Terminal window
bunx shadcn-vue@latest add @edmi-ui/ai-sandbox

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-sandbox.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-sandbox.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-sandbox.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-sandbox.json

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

import {
Sandbox,
SandboxContent,
SandboxHeader,
SandboxTabContent,
SandboxTabs,
SandboxTabsBar,
SandboxTabsList,
SandboxTabsTrigger,
} from "@/components/ai/sandbox";
<div className="flex w-full max-w-xl flex-col gap-4">
<Sandbox>
<SandboxHeader state="input-available" title="quote.ts" />
<SandboxContent>
<SandboxTabs defaultValue="code">
<SandboxTabsBar>
<SandboxTabsList>
<SandboxTabsTrigger value="code">Code</SandboxTabsTrigger>
<SandboxTabsTrigger value="output">Output</SandboxTabsTrigger>
</SandboxTabsList>
</SandboxTabsBar>
<SandboxTabContent value="code">
<Code />
</SandboxTabContent>
<SandboxTabContent value="output">
<pre className="m-0 rounded-md bg-muted px-3 py-2.5 font-mono text-xs leading-[1.6]">
Waiting for output…
</pre>
</SandboxTabContent>
</SandboxTabs>
</SandboxContent>
</Sandbox>
{/* … */}
</div>
<script setup lang="ts">
import {
Sandbox,
SandboxContent,
SandboxHeader,
SandboxTabContent,
SandboxTabs,
SandboxTabsBar,
SandboxTabsList,
SandboxTabsTrigger,
} from "@/components/ai/sandbox";
</script>
<template>
<div class="flex w-full max-w-xl flex-col gap-4">
<Sandbox>
<SandboxHeader state="input-available" title="quote.ts" />
<SandboxContent>
<SandboxTabs default-value="code">
<SandboxTabsBar>
<SandboxTabsList>
<SandboxTabsTrigger value="code">Code</SandboxTabsTrigger>
<SandboxTabsTrigger value="output">Output</SandboxTabsTrigger>
</SandboxTabsList>
</SandboxTabsBar>
<SandboxTabContent value="code">
<pre class="m-0 overflow-hidden rounded-md border border-border bg-card py-2.5 font-mono text-[12.5px] leading-[1.7]"><span class="mr-3.5 inline-block w-[38px] pr-3.5 text-right text-muted-foreground-2 select-none">1</span><span class="text-chart-2">const</span> r = <span class="text-chart-2">await</span> <span class="text-chart-4">quote</span>(<span class="text-success-text">"NVDAx"</span>)
<!-- … -->
</template>
<script lang="ts">
import {
Sandbox,
SandboxContent,
SandboxHeader,
SandboxTabContent,
SandboxTabs,
SandboxTabsBar,
SandboxTabsList,
SandboxTabsTrigger,
} from "$lib/components/ai/sandbox";
</script>
<div class="flex w-full max-w-xl flex-col gap-4">
<Sandbox>
<SandboxHeader state="input-available" title="quote.ts" />
<SandboxContent>
<SandboxTabs value="code">
<SandboxTabsBar>
<SandboxTabsList>
<SandboxTabsTrigger value="code">Code</SandboxTabsTrigger>
<SandboxTabsTrigger value="output">Output</SandboxTabsTrigger>
</SandboxTabsList>
</SandboxTabsBar>
<SandboxTabContent value="code">
<pre
class="m-0 overflow-hidden rounded-md border border-border bg-card py-2.5 font-mono text-[12.5px] leading-[1.7]"><span
class="mr-3.5 inline-block w-[38px] pr-3.5 text-right text-muted-foreground-2 select-none"
>1</span
><span class="text-chart-2">const</span> r = <span class="text-chart-2">await</span> <span
class="text-chart-4">quote</span
>(<span class="text-success-text">"NVDAx"</span>)
<!-- … -->
registry/components/ai/sandbox.tsx
"use client";
import type { ToolUIPart } from "ai";
import { cn } from "cn";
import type { ComponentProps, ReactNode } from "react";
import { ClockIcon, CheckCircleIcon, TerminalIcon, CaretDownIcon } from "@phosphor-icons/react";
import { Badge } from "@/registry/edmi/ui/badge";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/registry/edmi/ui/collapsible";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/registry/edmi/ui/tabs";
const CircleIcon = () => (
<svg
aria-hidden="true"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.7"
viewBox="0 0 24 24"
>
<circle cx="12" cy="12" r="9" />
</svg>
);
const CircleXIcon = () => (
<svg
aria-hidden="true"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.7"
viewBox="0 0 24 24"
>
<circle cx="12" cy="12" r="9" />
<path d="m9 9 6 6M15 9l-6 6" />
</svg>
);
type SandboxState = ToolUIPart["state"];
const statusLabels: Record<SandboxState, string> = {
"approval-requested": "Awaiting Approval",
"approval-responded": "Responded",
"input-available": "Running",
"input-streaming": "Pending",
"output-available": "Completed",
"output-denied": "Denied",
"output-error": "Error",
};
const statusVariants: Record<
SandboxState,
ComponentProps<typeof Badge>["variant"]
> = {
"approval-requested": "warning",
"approval-responded": "info",
"input-available": "info",
"input-streaming": "secondary",
"output-available": "success",
"output-denied": "warning",
"output-error": "destructive",
};
const statusIcons: Record<SandboxState, ReactNode> = {
"approval-requested": (
<ClockIcon
/>
),
"approval-responded": (
<CheckCircleIcon
/>
),
"input-available": (
<ClockIcon
/>
),
"input-streaming": <CircleIcon />,
"output-available": (
<CheckCircleIcon
/>
),
"output-denied": <CircleXIcon />,
"output-error": <CircleXIcon />,
};
const getStatusBadge = (status: SandboxState) => (
<Badge shape="pill" variant={statusVariants[status]}>
{statusIcons[status]}
{statusLabels[status]}
</Badge>
);
export type SandboxRootProps = ComponentProps<typeof Collapsible>;
export const Sandbox = ({ className, ...props }: SandboxRootProps) => (
<Collapsible
data-slot="ai-sandbox"
className={cn(
"not-prose group/ai-sandbox w-full overflow-hidden rounded-[calc(var(--radius)*1.2)] border border-border bg-card",
className,
)}
defaultOpen
{...props}
/>
);
export interface SandboxHeaderProps {
title?: string;
state: ToolUIPart["state"];
className?: string;
}
export const SandboxHeader = ({
className,
title,
state,
...props
}: SandboxHeaderProps) => (
<CollapsibleTrigger
data-slot="ai-sandbox-header"
className={cn(
"group/ai-sandbox-trigger flex w-full items-center gap-2.5 px-3.5 py-2.5 text-left outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring",
className,
)}
{...props}
>
<TerminalIcon
className="size-[15px] shrink-0 text-muted-foreground"
/>
<span className="font-mono text-[12.5px]">{title}</span>
{getStatusBadge(state)}
<CaretDownIcon
className="ml-auto size-3.5 shrink-0 text-muted-foreground transition-transform group-data-[panel-open]/ai-sandbox-trigger:rotate-180"
/>
</CollapsibleTrigger>
);
export type SandboxContentProps = ComponentProps<typeof CollapsibleContent>;
export const SandboxContent = ({
className,
...props
}: SandboxContentProps) => (
<CollapsibleContent
data-slot="ai-sandbox-content"
className={cn(
"h-(--collapsible-panel-height) overflow-hidden border-t border-border outline-none transition-[height] duration-200 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0",
className,
)}
{...props}
/>
);
export type SandboxTabsProps = ComponentProps<typeof Tabs>;
export const SandboxTabs = ({ className, ...props }: SandboxTabsProps) => (
<Tabs
data-slot="ai-sandbox-tabs"
className={cn("w-full gap-0", className)}
{...props}
/>
);
export type SandboxTabsBarProps = ComponentProps<"div">;
export const SandboxTabsBar = ({
className,
...props
}: SandboxTabsBarProps) => (
<div
data-slot="ai-sandbox-tabs-bar"
className={cn("flex w-full items-center px-3 pt-2.5", className)}
{...props}
/>
);
export type SandboxTabsListProps = ComponentProps<typeof TabsList>;
export const SandboxTabsList = ({ ...props }: SandboxTabsListProps) => (
<TabsList {...props} />
);
export type SandboxTabsTriggerProps = ComponentProps<typeof TabsTrigger>;
export const SandboxTabsTrigger = ({
className,
...props
}: SandboxTabsTriggerProps) => (
<TabsTrigger
className={cn("h-[26px] px-3.5 text-[12.5px]", className)}
{...props}
/>
);
export type SandboxTabContentProps = ComponentProps<typeof TabsContent>;
export const SandboxTabContent = ({
className,
...props
}: SandboxTabContentProps) => (
<TabsContent
data-slot="ai-sandbox-tab-content"
className={cn("px-3 pt-2.5 pb-3 text-sm", className)}
{...props}
/>
);
registry/components/ai/sandbox/Sandbox.vue
<script setup lang="ts">
import type { CollapsibleRootEmits, CollapsibleRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { useForwardPropsEmits } from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
import { Collapsible } from "@/registry/edmi/ui/collapsible"
const props = withDefaults(defineProps<CollapsibleRootProps & { class?: HTMLAttributes["class"] }>(), {
defaultOpen: true,
})
const emits = defineEmits<CollapsibleRootEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<Collapsible
data-slot="ai-sandbox"
v-bind="forwarded"
:class="cn(
'not-prose group/ai-sandbox w-full overflow-hidden rounded-[calc(var(--radius)*1.2)] border border-border bg-card',
props.class,
)"
>
<slot />
</Collapsible>
</template>
registry/components/ai/sandbox/SandboxContent.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { CollapsibleContent } from "@/registry/edmi/ui/collapsible"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<CollapsibleContent
data-slot="ai-sandbox-content"
:class="cn('overflow-hidden border-t border-border outline-none', props.class)"
>
<slot />
</CollapsibleContent>
</template>
registry/components/ai/sandbox/SandboxHeader.vue
<script setup lang="ts">
import type { ToolUIPart } from "ai"
import type { HTMLAttributes } from "vue"
import { ChevronDownIcon, Terminal } from "@lucide/vue"
import { cn } from "@/registry/edmi/lib/utils"
import { CollapsibleTrigger } from "@/registry/edmi/ui/collapsible"
import SandboxStatusBadge from "./SandboxStatusBadge.vue"
const props = defineProps<{
title?: string
state: ToolUIPart["state"]
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<CollapsibleTrigger
data-slot="ai-sandbox-header"
:class="cn(
'group/ai-sandbox-trigger flex w-full items-center gap-2.5 px-3.5 py-2.5 text-left outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring',
props.class,
)"
>
<Terminal class="size-[15px] shrink-0 text-muted-foreground" />
<span class="font-mono text-[12.5px]">{{ props.title }}</span>
<SandboxStatusBadge :state="props.state" />
<ChevronDownIcon class="ml-auto size-3.5 shrink-0 text-muted-foreground transition-transform group-data-[state=open]/ai-sandbox-trigger:rotate-180" />
</CollapsibleTrigger>
</template>
registry/components/ai/sandbox/SandboxStatusBadge.vue
<script setup lang="ts">
import type { ToolUIPart } from "ai"
import type { BadgeVariants } from "@/registry/edmi/ui/badge"
import { CircleCheckIcon, ClockIcon } from "@lucide/vue"
import { Badge } from "@/registry/edmi/ui/badge"
type SandboxState = ToolUIPart["state"]
const props = defineProps<{
state: SandboxState
}>()
const labels: Record<SandboxState, string> = {
"approval-requested": "Awaiting Approval",
"approval-responded": "Responded",
"input-available": "Running",
"input-streaming": "Pending",
"output-available": "Completed",
"output-denied": "Denied",
"output-error": "Error",
}
const variants: Record<SandboxState, BadgeVariants["variant"]> = {
"approval-requested": "warning",
"approval-responded": "info",
"input-available": "info",
"input-streaming": "secondary",
"output-available": "success",
"output-denied": "warning",
"output-error": "destructive",
}
</script>
<template>
<Badge shape="pill" :variant="variants[props.state]">
<svg
v-if="props.state === 'input-streaming'"
aria-hidden="true"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.7"
viewBox="0 0 24 24"
>
<circle cx="12" cy="12" r="9" />
</svg>
<svg
v-else-if="props.state === 'output-denied' || props.state === 'output-error'"
aria-hidden="true"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.7"
viewBox="0 0 24 24"
>
<circle cx="12" cy="12" r="9" />
<path d="m9 9 6 6M15 9l-6 6" />
</svg>
<ClockIcon v-else-if="props.state === 'input-available' || props.state === 'approval-requested'" />
<CircleCheckIcon v-else />
{{ labels[props.state] }}
</Badge>
</template>
registry/components/ai/sandbox/SandboxTabContent.vue
<script setup lang="ts">
import type { TabsContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { cn } from "@/registry/edmi/lib/utils"
import { TabsContent } from "@/registry/edmi/ui/tabs"
const props = defineProps<TabsContentProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<TabsContent
data-slot="ai-sandbox-tab-content"
v-bind="delegatedProps"
:class="cn('px-3 pt-2.5 pb-3 text-sm', props.class)"
>
<slot />
</TabsContent>
</template>
registry/components/ai/sandbox/SandboxTabs.vue
<script setup lang="ts">
import type { TabsRootEmits, TabsRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { useForwardPropsEmits } from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
import { Tabs } from "@/registry/edmi/ui/tabs"
const props = defineProps<TabsRootProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<TabsRootEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<Tabs
data-slot="ai-sandbox-tabs"
v-bind="forwarded"
:class="cn('w-full gap-0', props.class)"
>
<slot />
</Tabs>
</template>
registry/components/ai/sandbox/SandboxTabsBar.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="ai-sandbox-tabs-bar"
:class="cn('flex w-full items-center px-3 pt-2.5', props.class)"
>
<slot />
</div>
</template>
registry/components/ai/sandbox/SandboxTabsList.vue
<script setup lang="ts">
import type { TabsListProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { TabsList } from "@/registry/edmi/ui/tabs"
const props = defineProps<TabsListProps & { class?: HTMLAttributes["class"] }>()
</script>
<template>
<TabsList v-bind="props">
<slot />
</TabsList>
</template>
registry/components/ai/sandbox/SandboxTabsTrigger.vue
<script setup lang="ts">
import type { TabsTriggerProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { cn } from "@/registry/edmi/lib/utils"
import { TabsTrigger } from "@/registry/edmi/ui/tabs"
const props = defineProps<TabsTriggerProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<TabsTrigger
v-bind="delegatedProps"
:class="cn('h-[26px] px-3.5 text-[12.5px]', props.class)"
>
<slot />
</TabsTrigger>
</template>
registry/components/ai/sandbox/index.ts
export { default as Sandbox } from "./Sandbox.vue";
export { default as SandboxContent } from "./SandboxContent.vue";
export { default as SandboxHeader } from "./SandboxHeader.vue";
export { default as SandboxTabContent } from "./SandboxTabContent.vue";
export { default as SandboxTabs } from "./SandboxTabs.vue";
export { default as SandboxTabsBar } from "./SandboxTabsBar.vue";
export { default as SandboxTabsList } from "./SandboxTabsList.vue";
export { default as SandboxTabsTrigger } from "./SandboxTabsTrigger.vue";
src/lib/registry/ai/sandbox/index.ts
import Sandbox from "./sandbox.svelte";
import SandboxContent from "./sandbox-content.svelte";
import SandboxHeader from "./sandbox-header.svelte";
import SandboxTabContent from "./sandbox-tab-content.svelte";
import SandboxTabs from "./sandbox-tabs.svelte";
import SandboxTabsBar from "./sandbox-tabs-bar.svelte";
import SandboxTabsList from "./sandbox-tabs-list.svelte";
import SandboxTabsTrigger from "./sandbox-tabs-trigger.svelte";
export {
Sandbox,
SandboxContent,
SandboxHeader,
SandboxTabContent,
SandboxTabs,
SandboxTabsBar,
SandboxTabsList,
SandboxTabsTrigger,
};
src/lib/registry/ai/sandbox/sandbox-content.svelte
<script lang="ts">
import * as Collapsible from "$lib/registry/ui/collapsible/index.js";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: ComponentProps<typeof Collapsible.Content> = $props();
</script>
<Collapsible.Content
bind:ref
data-slot="ai-sandbox-content"
class={cn("overflow-hidden border-t border-border outline-none", className)}
{...restProps}
>
{@render children?.()}
</Collapsible.Content>
src/lib/registry/ai/sandbox/sandbox-header.svelte
<script lang="ts">
import TerminalIcon from "phosphor-svelte/lib/Terminal";
import CaretDownIcon from "phosphor-svelte/lib/CaretDown";
import * as Collapsible from "$lib/registry/ui/collapsible/index.js";
import { cn } from "$lib/utils.js";
import type { ToolUIPart } from "ai";
import type { ComponentProps } from "svelte";
import SandboxStatusBadge from "./sandbox-status-badge.svelte";
let {
ref = $bindable(null),
title,
state: status,
class: className,
...restProps
}: Omit<ComponentProps<typeof Collapsible.Trigger>, "children"> & {
title?: string;
state: ToolUIPart["state"];
} = $props();
</script>
<Collapsible.Trigger
bind:ref
data-slot="ai-sandbox-header"
class={cn(
"group/ai-sandbox-trigger flex w-full items-center gap-2.5 px-3.5 py-2.5 text-left outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring",
className
)}
{...restProps}
>
<TerminalIcon
class="size-[15px] shrink-0 text-muted-foreground"
/>
<span class="font-mono text-[12.5px]">{title}</span>
<SandboxStatusBadge state={status} />
<CaretDownIcon
class="ml-auto size-3.5 shrink-0 text-muted-foreground transition-transform group-data-[state=open]/ai-sandbox-trigger:rotate-180"
/>
</Collapsible.Trigger>
src/lib/registry/ai/sandbox/sandbox-status-badge.svelte
<script lang="ts">
import ClockIcon from "phosphor-svelte/lib/Clock";
import CheckCircleIcon from "phosphor-svelte/lib/CheckCircle";
import { Badge, type BadgeVariant } from "$lib/registry/ui/badge/index.js";
import type { ToolUIPart } from "ai";
type SandboxState = ToolUIPart["state"];
let { state: status }: { state: SandboxState } = $props();
const labels: Record<SandboxState, string> = {
"approval-requested": "Awaiting Approval",
"approval-responded": "Responded",
"input-available": "Running",
"input-streaming": "Pending",
"output-available": "Completed",
"output-denied": "Denied",
"output-error": "Error",
};
const variants: Record<SandboxState, BadgeVariant> = {
"approval-requested": "warning",
"approval-responded": "info",
"input-available": "info",
"input-streaming": "secondary",
"output-available": "success",
"output-denied": "warning",
"output-error": "destructive",
};
</script>
<Badge shape="pill" variant={variants[status]}>
{#if status === "input-streaming"}
<svg
aria-hidden="true"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.7"
viewBox="0 0 24 24"
>
<circle cx="12" cy="12" r="9" />
</svg>
{:else if status === "output-denied" || status === "output-error"}
<svg
aria-hidden="true"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.7"
viewBox="0 0 24 24"
>
<circle cx="12" cy="12" r="9" />
<path d="m9 9 6 6M15 9l-6 6" />
</svg>
{:else if status === "input-available" || status === "approval-requested"}
<ClockIcon
/>
{:else}
<CheckCircleIcon
/>
{/if}
{labels[status]}
</Badge>
src/lib/registry/ai/sandbox/sandbox-tab-content.svelte
<script lang="ts">
import * as Tabs from "$lib/registry/ui/tabs/index.js";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
...restProps
}: ComponentProps<typeof Tabs.Content> = $props();
</script>
<Tabs.Content
bind:ref
data-slot="ai-sandbox-tab-content"
class={cn("px-3 pt-2.5 pb-3 text-sm", className)}
{...restProps}
/>
src/lib/registry/ai/sandbox/sandbox-tabs-bar.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="ai-sandbox-tabs-bar"
class={cn("flex w-full items-center px-3 pt-2.5", className)}
{...restProps}
>
{@render children?.()}
</div>
src/lib/registry/ai/sandbox/sandbox-tabs-list.svelte
<script lang="ts">
import * as Tabs from "$lib/registry/ui/tabs/index.js";
import type { ComponentProps } from "svelte";
let { ref = $bindable(null), ...restProps }: ComponentProps<typeof Tabs.List> = $props();
</script>
<Tabs.List bind:ref {...restProps} />
src/lib/registry/ai/sandbox/sandbox-tabs-trigger.svelte
<script lang="ts">
import * as Tabs from "$lib/registry/ui/tabs/index.js";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
...restProps
}: ComponentProps<typeof Tabs.Trigger> = $props();
</script>
<Tabs.Trigger bind:ref class={cn("h-[26px] px-3.5 text-[12.5px]", className)} {...restProps} />
src/lib/registry/ai/sandbox/sandbox-tabs.svelte
<script lang="ts">
import * as Tabs from "$lib/registry/ui/tabs/index.js";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
value = $bindable(""),
class: className,
children,
...restProps
}: ComponentProps<typeof Tabs.Root> = $props();
</script>
<Tabs.Root
bind:ref
bind:value
data-slot="ai-sandbox-tabs"
class={cn("w-full gap-0", className)}
{...restProps}
>
{@render children?.()}
</Tabs.Root>
src/lib/registry/ai/sandbox/sandbox.svelte
<script lang="ts">
import * as Collapsible from "$lib/registry/ui/collapsible/index.js";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
open = $bindable(true),
class: className,
children,
...restProps
}: ComponentProps<typeof Collapsible.Root> = $props();
</script>
<Collapsible.Root
bind:ref
bind:open
data-slot="ai-sandbox"
class={cn(
"not-prose group/ai-sandbox w-full overflow-hidden rounded-[calc(var(--radius)*1.2)] border border-border bg-card",
className
)}
{...restProps}
>
{@render children?.()}
</Collapsible.Root>
Part Prop Type Default
Sandbox collapsible props Collapsible defaultOpen
SandboxHeader title, state string, ToolUIPart["state"]
SandboxContent, SandboxTabs, SandboxTabsBar, SandboxTabsList, SandboxTabsTrigger, SandboxTabContent

state is the AI SDK tool state: input-streaming (Pending), input-available (Running), output-available (Completed), output-error (Error), plus the approval states.

  • The state badge is built in (soft badge variants with an icon), so the item does not depend on ai-tool.
  • Tabs use the flat tabs track at 26px; the content region is a plain slot, so any code block or output can go in it.