Installation
Complete project setup with vue/reka, then install these components:
pnpm exec frontfriend download collapsible button icon -f vue --primitive reka Folders use Collapsible and files use Button. Tab moves between the controls. Enter or Space opens a folder or marks a file row as selected. This is a nested disclosure list, so it does not claim the arrow-key behavior of an ARIA tree widget.
Usage
Complete example
Save as FileTreeExample.vue.
<script setup lang="ts">
import { computed, ref } from "vue";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Button } from "@/components/ui/button";
import { Icon } from "@/components/ui/icon";
type FileNode = { name: string; path: string; children?: FileNode[] };
const props = withDefaults(
defineProps<{
label?: string;
nodes?: FileNode[];
nested?: boolean;
selected?: string;
}>(),
{ label: "Changed files", nested: false, selected: "" },
);
const emit = defineEmits<{ select: [path: string] }>();
const selection = ref("");
const current = computed(() =>
props.nested ? props.selected : selection.value,
);
const files: FileNode[] = [
{
name: "app",
path: "app",
children: [
{ name: "app.vue", path: "app/app.vue" },
{
name: "settings",
path: "app/settings",
children: [{ name: "index.vue", path: "app/settings/index.vue" }],
},
],
},
{ name: "package.json", path: "package.json" },
];
function select(path: string) {
selection.value = path;
emit("select", path);
}
</script>
<template>
<div
:style="
nested
? { display: 'grid', justifyItems: 'start', paddingInlineStart: '20px' }
: {
width: '100%',
maxWidth: '560px',
minWidth: '0',
marginInline: 'auto',
display: 'grid',
gap: '16px',
paddingBlock: '16px',
fontSize: 'var(--text-sm)',
lineHeight: '1.5',
}
"
>
<strong v-if="!nested">{{ label }}</strong>
<div
:role="nested ? undefined : 'group'"
:aria-label="nested ? undefined : label"
style="display: grid; justify-items: start"
>
<template v-for="node in nodes ?? files" :key="node.path">
<Collapsible v-if="node.children" v-slot="{ open }" default-open>
<CollapsibleTrigger as-child>
<Button variant="ghost" size="sm"
><Icon :name="open ? 'ChevronDown' : 'ChevronRight'" /><Icon
name="Folder"
/>{{ node.name }}</Button
>
</CollapsibleTrigger>
<CollapsibleContent>
<FileTreeExample
:nodes="node.children"
nested
:selected="current"
@select="select"
/>
</CollapsibleContent>
</Collapsible>
<Button
v-else
:variant="current === node.path ? 'secondary' : 'ghost'"
size="sm"
:aria-current="current === node.path ? 'true' : undefined"
@click="select(node.path)"
><Icon name="File" />{{ node.name }}</Button
>
</template>
</div>
<p
v-if="!nested"
role="status"
style="
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
"
>
{{ selection ? "Selected: " + selection : "" }}
</p>
</div>
</template> Components and API
This composition has no separate component API. Use the APIs for its installed components.
| Component | Target reference |
|---|---|
collapsible | Reka UI API |
button | Reka UI API |
icon | Reka UI API |