Controls
Zoom and fit controls for the workflow canvas.
import { Canvas } from "@/components/ai/canvas";import { Controls } from "@/components/ai/controls";import { Node, NodeDescription, NodeHeader, NodeTitle,} from "@/components/ai/node";
const nodes = [ { id: "a", type: "step", position: { x: 0, y: 0 }, data: { t: "Start" } }, { id: "b", type: "step", position: { x: 300, y: 40 }, data: { t: "Check drift" }, },];
const nodeTypes = { step: ({ data }: { data: { t: string } }) => ( <Node className="w-40" handles={{ target: false, source: false }}> <NodeHeader className="border-b-0"> <NodeTitle>{data.t}</NodeTitle> <NodeDescription>Step</NodeDescription> </NodeHeader> </Node> ),};
export default function Demo() { return ( <div className="h-56 w-full overflow-hidden rounded-[calc(var(--radius)*1.4)] border border-border"> <Canvas edges={[]} nodes={nodes} nodeTypes={nodeTypes}> <Controls position="bottom-left" showFitView showInteractive showZoom /> </Canvas> </div> );}<script setup lang="ts">import { Canvas } from "@/components/ai/canvas";import { Controls } from "@/components/ai/controls";import { Node, NodeDescription, NodeHeader, NodeTitle } from "@/components/ai/node";
const nodes = [ { id: "a", type: "step", position: { x: 0, y: 0 }, data: { t: "Start" } }, { id: "b", type: "step", position: { x: 300, y: 40 }, data: { t: "Check drift" } },];</script>
<template> <div class="h-56 w-full overflow-hidden rounded-[calc(var(--radius)*1.4)] border border-border"> <Canvas :nodes="nodes" :edges="[]"> <template #node-step="{ data }"> <Node class="w-40"> <NodeHeader class="border-b-0"> <NodeTitle>{{ data.t }}</NodeTitle> <NodeDescription>Step</NodeDescription> </NodeHeader> </Node> </template> <Controls position="bottom-left" /> </Canvas> </div></template><script lang="ts"> import { Canvas } from "$lib/components/ai/canvas"; import { Controls } from "$lib/components/ai/controls"; import Step from "./_shared/ai-workflow-step.svelte";
const nodeTypes = { step: Step };
const step = (title: string) => ({ title, description: "Step", compact: true, handles: { target: false, source: false } });
let nodes = $state.raw([ { id: "a", type: "step", position: { x: 0, y: 0 }, data: step("Start") }, { id: "b", type: "step", position: { x: 300, y: 40 }, data: step("Check drift") }, ]); let edges = $state.raw([]);</script>
<div style="height: 224px" class="w-full overflow-hidden rounded-[calc(var(--radius)*1.4)] border border-border"> <Canvas bind:nodes bind:edges {nodeTypes}> <Controls position="bottom-left" /> </Canvas></div>Stacked 30px buttons in one bordered card group.
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-controlspnpm dlx shadcn@latest add @edmi-ui/ai-controlsyarn dlx shadcn@latest add @edmi-ui/ai-controlsbunx shadcn@latest add @edmi-ui/ai-controlsPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @xyflow/reactTerminal window pnpm add @xyflow/reactTerminal window yarn add @xyflow/reactTerminal window bun add @xyflow/reactCopy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/ai-controlspnpm dlx shadcn-vue@latest add @edmi-ui/ai-controlsyarn dlx shadcn-vue@latest add @edmi-ui/ai-controlsbunx shadcn-vue@latest add @edmi-ui/ai-controlsPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @vue-flow/controls @vueuse/coreTerminal window pnpm add @vue-flow/controls @vueuse/coreTerminal window yarn add @vue-flow/controls @vueuse/coreTerminal window bun add @vue-flow/controls @vueuse/coreCopy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-controls.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-controls.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-controls.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-controls.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Copy the code from Component source into your project and update the import paths to match your aliases.
import { Canvas } from "@/components/ai/canvas";import { Controls } from "@/components/ai/controls";
<div className="h-56 w-full overflow-hidden rounded-[calc(var(--radius)*1.4)] border border-border"> <Canvas edges={[]}> <Controls position="bottom-left" showFitView showInteractive showZoom /> </Canvas></div><script setup lang="ts">import { Canvas } from "@/components/ai/canvas";import { Controls } from "@/components/ai/controls";import { Node, NodeDescription, NodeHeader, NodeTitle,} from "@/components/ai/node";</script>
<template> <div class="h-56 w-full overflow-hidden rounded-[calc(var(--radius)*1.4)] border border-border"> <Canvas :edges="[]"> <template #node-step="{ data }"> <Node class="w-40"> <NodeHeader class="border-b-0"> <NodeTitle>{{ data.t }}</NodeTitle> <NodeDescription>Step</NodeDescription> </NodeHeader> </Node> </template> <Controls position="bottom-left" /> </Canvas> </div></template><script lang="ts"> import { Canvas } from "$lib/components/ai/canvas"; import { Controls } from "$lib/components/ai/controls";</script>
<div style="height: 224px" class="w-full overflow-hidden rounded-[calc(var(--radius)*1.4)] border border-border"> <Canvas bind:nodes bind:edges > <Controls position="bottom-left" /> </Canvas></div>Component source
Section titled “Component source”"use client";
import { Controls as ControlsPrimitive } from "@xyflow/react";import { cn } from "cn";import type { ComponentProps } from "react";
export type ControlsProps = ComponentProps<typeof ControlsPrimitive>;
/** Zoom in, zoom out, fit view and lock, stacked in one bordered card group. */export const Controls = ({ className, ...props }: ControlsProps) => ( <ControlsPrimitive data-slot="ai-controls" className={cn( "overflow-hidden rounded-[calc(var(--radius)*0.9)] border border-border bg-card shadow-none!", "[&>button]:size-[30px]! [&>button]:rounded-none! [&>button]:border-0! [&>button]:border-b! [&>button]:border-border! [&>button]:bg-transparent! [&>button]:text-foreground-2! [&>button]:last:border-b-0! [&>button]:hover:bg-accent!", "[&>button>svg]:size-[15px]! [&>button>svg]:max-h-none! [&>button>svg]:max-w-none!", className, )} {...props} />);<script setup lang="ts">import type { HTMLAttributes } from "vue"import { Controls as ControlsPrimitive } from "@vue-flow/controls"import { reactiveOmit } from "@vueuse/core"import { onMounted, ref } from "vue"import { cn } from "@/registry/edmi/lib/utils"import "@vue-flow/controls/dist/style.css"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const delegatedProps = reactiveOmit(props, "class")
// Vue Flow renders icon-only buttons without a name: label them like the other ports.const labels: Record<string, string> = { "vue-flow__controls-zoomin": "Zoom In", "vue-flow__controls-zoomout": "Zoom Out", "vue-flow__controls-fitview": "Fit View", "vue-flow__controls-interactive": "Toggle Interactivity",}const root = ref<{ $el?: HTMLElement } | null>(null)onMounted(() => { const el = root.value?.$el if (!el) return for (const [cls, label] of Object.entries(labels)) { const button = el.querySelector<HTMLElement>(`.${cls}`) if (button && !button.getAttribute("aria-label")) { button.setAttribute("aria-label", label) button.setAttribute("title", label) } }})</script>
<template> <!-- Zoom in, zoom out, fit view and lock, stacked in one bordered card group. --> <ControlsPrimitive ref="root" data-slot="ai-controls" v-bind="delegatedProps" :class="cn( 'overflow-hidden rounded-[calc(var(--radius)*0.9)] border border-border bg-card shadow-none!', '[&>button]:box-border! [&>button]:size-[30px]! [&>button]:rounded-none! [&>button]:border-0! [&>button]:border-b! [&>button]:border-border! [&>button]:bg-transparent! [&>button]:p-0! [&>button]:text-foreground-2! [&>button]:last:border-b-0! [&>button]:hover:bg-accent!', '[&>button>svg]:size-[15px]! [&>button>svg]:max-h-none! [&>button>svg]:max-w-none!', props.class, )" /></template>export { default as Controls } from "./Controls.vue";<script lang="ts"> import { Controls as ControlsPrimitive, type ControlsProps } from "@xyflow/svelte"; import { cn } from "$lib/utils.js";
let { class: className, ...restProps }: ControlsProps = $props();</script>
<!-- Zoom in, zoom out, fit view and lock, stacked in one bordered card group. --><ControlsPrimitive data-slot="ai-controls" class={cn( "overflow-hidden rounded-[calc(var(--radius)*0.9)] border border-border bg-card shadow-none!", "[&>button]:box-border! [&>button]:size-[30px]! [&>button]:rounded-none! [&>button]:border-0! [&>button]:border-b! [&>button]:border-border! [&>button]:bg-transparent! [&>button]:p-0! [&>button]:text-foreground-2! [&>button]:last:border-b-0! [&>button]:hover:bg-accent!", "[&>button>svg]:size-[15px]! [&>button>svg]:max-h-none! [&>button>svg]:max-w-none!", className )} {...restProps}/>import Controls from "./controls.svelte";
export { Controls };| Part | Prop | Type | Default |
|---|---|---|---|
| Controls | showZoom, showFitView, showInteractive, position |
xyflow props |