Skip to content
edmi-ui

Controls

Zoom and fit controls for the workflow canvas.

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.

Stacked 30px buttons in one bordered card group.

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

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

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-controls.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-controls.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-controls.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-controls.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";
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>
registry/components/ai/controls.tsx
"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}
/>
);
registry/components/ai/controls/Controls.vue
<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>
registry/components/ai/controls/index.ts
export { default as Controls } from "./Controls.vue";
src/lib/registry/ai/controls/controls.svelte
<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}
/>
src/lib/registry/ai/controls/index.ts
import Controls from "./controls.svelte";
export { Controls };
Part Prop Type Default
Controls showZoom, showFitView, showInteractive, position xyflow props