Skip to content
edmi-ui

Toolbar

Floating toolbar attached to a selected node.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

A solid --popover chip with a 1px border (rule 7), shown when the node is selected.

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

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

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

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

import { Canvas } from "@/components/ai/canvas";
<div className="h-64 w-full overflow-hidden rounded-[calc(var(--radius)*1.4)] border border-border">
<Canvas
edges={[]}
fitViewOptions={{ maxZoom: 1, padding: 0.4 }}
/>
</div>
<script setup lang="ts">
import { Canvas } from "@/components/ai/canvas";
import {
Node,
NodeContent,
NodeDescription,
NodeHeader,
NodeTitle,
} from "@/components/ai/node";
import { Toolbar } from "@/components/ai/toolbar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
</script>
<template>
<div class="h-64 w-full overflow-hidden rounded-[calc(var(--radius)*1.4)] border border-border">
<Canvas :edges="[]" :fit-view-on-init="false">
<template #node-decision="{ selected }">
<Toolbar :is-visible="true">
<Button size="icon-xs" variant="ghost" aria-label="Settings"><SettingsIcon /></Button>
<Button size="icon-xs" variant="ghost" aria-label="Duplicate"><CopyIcon /></Button>
<Button size="icon-xs" variant="ghost" aria-label="Delete"><Trash2Icon /></Button>
</Toolbar>
<Node :handles="{ target: true, source: true }" :selected="selected">
<NodeHeader>
<NodeTitle>Decision</NodeTitle>
<NodeDescription>drift &gt; 2%?</NodeDescription>
</NodeHeader>
<NodeContent>
<Badge variant="success">yes</Badge>
</NodeContent>
</Node>
</template>
</Canvas>
</div>
</template>
<script lang="ts">
import { Canvas } from "$lib/components/ai/canvas";
</script>
<div style="height: 256px" class="w-full overflow-hidden rounded-[calc(var(--radius)*1.4)] border border-border">
<Canvas bind:nodes bind:edges fitViewOptions={{ maxZoom: 1, padding: 0.4 }} />
</div>
registry/components/ai/toolbar.tsx
import { NodeToolbar, Position } from "@xyflow/react";
import { cn } from "cn";
import type { ComponentProps } from "react";
export type ToolbarProps = ComponentProps<typeof NodeToolbar>;
/** Floating actions attached to a node (solid chip: `--popover` + 1px border). Above the node by default. */
export const Toolbar = ({ className, ...props }: ToolbarProps) => (
<NodeToolbar
data-slot="ai-toolbar"
className={cn(
"flex items-center gap-0.5 rounded-[9px] border border-border bg-popover p-[3px]",
className,
)}
position={Position.Top}
{...props}
/>
);
registry/components/ai/toolbar/Toolbar.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { Position } from "@vue-flow/core"
import { NodeToolbar } from "@vue-flow/node-toolbar"
import { reactiveOmit } from "@vueuse/core"
import { cn } from "@/registry/edmi/lib/utils"
/** Floating actions attached to a node (solid chip: --popover + 1px border). Above the node by default. */
const props = withDefaults(defineProps<{
class?: HTMLAttributes["class"]
position?: Position
isVisible?: boolean
offset?: number
}>(), {
position: Position.Top,
isVisible: undefined,
})
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<NodeToolbar
data-slot="ai-toolbar"
v-bind="delegatedProps"
:class="cn('flex items-center gap-0.5 rounded-[9px] border border-border bg-popover p-[3px]', props.class)"
>
<slot />
</NodeToolbar>
</template>
registry/components/ai/toolbar/index.ts
export { default as Toolbar } from "./Toolbar.vue";
src/lib/registry/ai/toolbar/toolbar.svelte
<script lang="ts">
import { NodeToolbar, Position, type NodeToolbarProps } from "@xyflow/svelte";
import { cn } from "$lib/utils.js";
/** Floating actions attached to a node (solid chip: --popover + 1px border). Above the node by default. */
let {
class: className,
position = Position.Top,
children,
...restProps
}: NodeToolbarProps = $props();
</script>
<NodeToolbar
data-slot="ai-toolbar"
{position}
class={cn(
"flex items-center gap-0.5 rounded-[9px] border border-border bg-popover p-[3px]",
className
)}
{...restProps}
>
{@render children?.()}
</NodeToolbar>
src/lib/registry/ai/toolbar/index.ts
import Toolbar from "./toolbar.svelte";
export { Toolbar };
Part Prop Type Default
Toolbar isVisible, position, offset xyflow NodeToolbar props position defaults to top