Skip to content

Instantly share code, notes, and snippets.

@vnphanquang
Created May 7, 2025 10:50
Show Gist options
  • Save vnphanquang/41656c37462a767cf923c596afbbe3b6 to your computer and use it in GitHub Desktop.
Save vnphanquang/41656c37462a767cf923c596afbbe3b6 to your computer and use it in GitHub Desktop.
Svelte File Tree
<script lang="ts">
import type { Snippet } from 'svelte';
import type { HTMLLiAttributes } from 'svelte/elements';
let {
open = $bindable(true),
name,
iconSpecifier = 'i-[ph--file]',
...rest
}: HTMLLiAttributes & {
/** @bindable */
open?: boolean;
name: string | Snippet<[]>;
iconSpecifier?: string;
} = $props();
</script>
<li {...rest}>
<i class={['i', iconSpecifier]}></i>
<span>
{#if typeof name === 'string'}
{name}
{:else}
{@render name?.()}
{/if}
</span>
</li>
<style>
@layer components {
li {
align-items: center;
display: flex;
padding-inline: calc(var(--spacing) * 2);
padding-block: calc(var(--spacing) * 1);
gap: calc(var(--spacing) * 2);
transition-property: background-color, color;
transition-duration: 400ms;
transition-timing-function: var(--timing-ease-in);
&:hover {
transition-duration: 200ms;
transition-timing-function: var(--timing-ease-out);
background-color: var(--color-surface-variant);
}
&:active {
transition-duration: 150ms;
background-color: var(--color-on-surface);
color: var(--color-surface);
}
}
}
i {
height: 1.25em;
width: 1.25em;
}
</style>
<script lang="ts">
import type { Snippet } from 'svelte';
import type { HTMLLiAttributes } from 'svelte/elements';
let {
children,
open = $bindable(true),
name,
iconSpecifier = 'i-[ph--folder]',
...rest
}: HTMLLiAttributes & {
/** @bindable */
open?: boolean;
name: string | Snippet<[]>;
iconSpecifier?: string;
} = $props();
</script>
<li {...rest}>
<details bind:open>
<summary class="outline-hidden cursor-pointer marker:hidden">
<i class={['i icon', iconSpecifier]}></i>
<span class="flex-1">
{#if typeof name === 'string'}
{name}
{:else}
{@render name?.()}
{/if}
</span>
<i class="i caret i-[ph--caret-down] transition-transform"></i>
</summary>
<ul>
{@render children?.()}
</ul>
</details>
</li>
<style lang="postcss">
@layer components {
ul {
position: relative;
margin-inline-start: calc(var(--spacing) * 2);
padding-inline-start: calc(var(--spacing) * 4);
&::before {
content: '';
background-color: var(--color-outline-subtle);
width: 1.5px;
inset-inline-start: 0;
position: absolute;
top: 0.75rem;
bottom: 0.75rem;
}
}
summary {
align-items: center;
display: flex;
padding-inline: calc(var(--spacing) * 2);
padding-block: calc(var(--spacing) * 1);
gap: calc(var(--spacing) * 2);
transition-property: background-color, color;
transition-duration: 400ms;
transition-timing-function: var(--timing-ease-in);
&:hover {
transition-duration: 200ms;
transition-timing-function: var(--timing-ease-out);
background-color: var(--color-surface-variant);
}
&:active {
transition-duration: 150ms;
background-color: var(--color-on-surface);
color: var(--color-surface);
}
}
}
.icon {
height: 1.25em;
width: 1.25em;
}
.caret {
height: 1em;
width: 1em;
details[open] > summary > & {
rotate: 180deg;
}
}
</style>
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
let { children, ...rest }: HTMLAttributes<HTMLUListElement> = $props();
</script>
<ul {...rest}>
{@render children?.()}
</ul>
import File from './FileTree.File.svelte';
import Folder from './FileTree.Folder.svelte';
import Root from './FileTree.Root.svelte';
export const FileTree = {
Root,
Folder,
File,
};
@vnphanquang
Copy link
Author

Example Usage:

<script>
	import { FileTree } from '$lib/components/file-tree';
</script>

<div class="not-prose">
	<FileTree.Root class="p-4 border border-current">
		<FileTree.Folder name="sites/sveltevietnam.dev/src/data/blog/posts/:id">

			<FileTree.File name="metadata.ts" />

			<FileTree.Folder name="content">
				<FileTree.File name="en.md.svelte" />
				<FileTree.File name="vi.md.svelte" />
			</FileTree.Folder>

			<FileTree.Folder name="images">
				<FileTree.File
					name="thumbnail.jpg"
					iconSpecifier="i-[ph--image]"
				/>
			</FileTree.Folder>
		</FileTree.Folder>

	</FileTree.Root>
</div>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment