Navigation
File tree
Shows folders and files, to open, close and choose from.
Installation
npx sinaui add file-treeCopy src/lib/ui/FileTree.svelte with SearchField.svelte, Input.svelte, InputButton.svelte, Icon.svelte, scroll-edges.ts and tokens.css. No other dependencies.
Examples
Archive
Folders open smoothly and the rows below slide down to make room. Type a name's first letters to jump to it.
Selected: canon-1
<script lang="ts">
import FileTree, { type TreeNode } from '#lib/ui/FileTree.svelte';
const archive: TreeNode[] = [
{
id: 'medicine',
name: 'Medicine',
children: [
{
id: 'canon',
name: 'The Canon of Medicine',
children: [
{ id: 'canon-1', name: 'Book one — General principles.pdf' },
{ id: 'canon-2', name: 'Book two — Simple medicines.pdf' },
{ id: 'canon-scan', name: 'Frontispiece.png' }
]
},
{ id: 'tasrif', name: 'Kitāb al-Taṣrīf.pdf' }
]
},
{
id: 'mathematics',
name: 'Mathematics',
children: [
{ id: 'jabr', name: 'Al-Jabr.pdf' },
{ id: 'tables', name: 'Astronomical tables.json' }
]
},
{ id: 'optics', name: 'Optics', children: [{ id: 'manazir', name: 'Kitāb al-Manāẓir.pdf' }] },
{ id: 'readme', name: 'Catalog notes.md' }
];
let expanded = $state(['medicine', 'canon']);
let selected = $state('canon-1');
</script>
<div class="layout">
<FileTree label="Archive" items={archive} bind:expanded bind:selected />
<p>Selected: <strong>{selected}</strong></p>
</div>
<style>
.layout {
display: grid;
gap: 0.75rem;
inline-size: min(100%, 22rem);
}
p {
margin: 0;
color: var(--ui-muted);
font-size: 0.875rem;
}
</style>Over 120,000 items, with search
Only the rows in view are ever drawn, so it scrolls as easily as a short list. Search looks through everything and opens the folders that hold the matches.
<script lang="ts">
import FileTree, { type TreeNode } from '#lib/ui/FileTree.svelte';
// 20 wings × 30 shelves × 200 manuscripts: over 120,000 items. Only the rows in view are drawn.
const subjects = [
'Optics',
'Algebra',
'Medicine',
'Astronomy',
'Geography',
'Poetry',
'Law',
'Music'
];
const items: TreeNode[] = Array.from({ length: 20 }, (_, w) => ({
id: `w${w}`,
name: `Wing ${w + 1}`,
children: Array.from({ length: 30 }, (_, s) => ({
id: `w${w}s${s}`,
name: `Shelf ${s + 1} · ${subjects[(w + s) % subjects.length]}`,
children: Array.from({ length: 200 }, (_, m) => ({
id: `w${w}s${s}m${m}`,
name: `MS-${String(w * 6000 + s * 200 + m + 1).padStart(6, '0')}.pdf`
}))
}))
}));
let expanded = $state(['w0', 'w0s0']);
</script>
<FileTree label="Catalog" {items} bind:expanded searchLabel="Search the catalog" height="22rem" />Loaded as it opens
Folders fetch their contents the first time they open, as from a server; a spinner turns in place of its arrow meanwhile.
<script lang="ts">
import FileTree, { type TreeNode } from '#lib/ui/FileTree.svelte';
// Folders start empty and fetch their contents the first time they open, as from a server.
const folder = (id: string, name: string): TreeNode => ({ id, name, children: [] });
const items: TreeNode[] = [
folder('baghdad', 'Baghdad'),
folder('cairo', 'Cairo'),
folder('cordoba', 'Córdoba')
];
async function load(node: TreeNode): Promise<TreeNode[]> {
await new Promise((r) => setTimeout(r, 700));
return [
folder(`${node.id}/letters`, 'Letters'),
...Array.from({ length: 5 }, (_, i) => ({
id: `${node.id}/${i}`,
name: `${node.name} register ${i + 1}.pdf`
}))
];
}
</script>
<FileTree label="Cities" {items} {load} height="16rem" />Props
| Prop | Type | Default |
|---|---|---|
label | string (required) | |
items | { id, name, children? }[] | |
expanded | string[], bindable: open folders | [] |
selected | string, bindable | |
onselect | (node) => void | |
load | (node) => Promise<TreeNode[]>: fetch a folder when first opened | |
searchLabel | string: shows a search box | |
query | string, bindable: the search | '' |
height | CSS length of the scrolling area | '20rem' |
Accessibility
- A tree with one Tab stop; each item is read with its level and place ("3 of 30"), folders say whether they're open, and the chosen item is marked selected.
- However long the tree, the keyboard reaches every item: the list scrolls to follow.
- Search says how many items match; a loading folder says it's busy.
| Key | Action |
|---|---|
| ↓ ↑ | Next / previous visible item. |
| → | Open a folder, or step into an open one. |
| ← | Close a folder, or go up to the folder it's in. |
| Home End | First / last visible item. |
| Page Up Page Down | A screenful up / down. |
| Enter Space | Choose the item (and open or close a folder). |
| Letters | Jump to the next item starting with them. |
Right to left, ← and → swap.