Demos
How to import
<script lang="ts">
import { List, ListItem } from '@fibrae/semi-ui-svelte';
</script>
Basic Usage
You can use size to size list. Supported values include large, default, small. Customize the header and footer with the header and footer snippets, and render each entry of dataSource with the item snippet (argument { item, index }).
Default Size
- 从明天起,做一个幸福的人
- 喂马,劈柴,周游世界
- 从明天起,关心粮食和蔬菜
- 我有一所房子,面朝大海,春暖花开
Small Size
- 从明天起,做一个幸福的人
- 喂马,劈柴,周游世界
- 从明天起,关心粮食和蔬菜
- 我有一所房子,面朝大海,春暖花开
Large Size
- 从明天起,做一个幸福的人
- 喂马,劈柴,周游世界
- 从明天起,关心粮食和蔬菜
- 我有一所房子,面朝大海,春暖花开
Code
<script lang="ts">
import { List, ListItem } from '@fibrae/semi-ui-svelte';
const data = ['从明天起,做一个幸福的人', '喂马,劈柴,周游世界', '从明天起,关心粮食和蔬菜', '我有一所房子,面朝大海,春暖花开'];
const sizes = [
{ title: 'Default Size', size: undefined, margin: '0 0 16px' },
{ title: 'Small Size', size: 'small', margin: '16px 0' },
{ title: 'Large Size', size: 'large', margin: '16px 0' },
] as const;
</script>
<div>
{#each sizes as { title, size, margin } (title)}
<div style="margin-right: 16px">
<h3 style="margin: {margin}">{title}</h3>
<List bordered dataSource={data} {size}>
{#snippet header()}
<div>Header</div>
{/snippet}
{#snippet footer()}
<div>Footer</div>
{/snippet}
{#snippet item({ item: text })}
<ListItem>{text}</ListItem>
{/snippet}
</List>
</div>
{/each}
</div>Template
ListItem has a built-in template provided by the header, main and extra snippets. The alignment of header and main set by align properties using one of flex-start(default), flex-end, center, baseline, and stretch .
- SE示例标题
- SE示例标题
Semi Design 是由抖音前端团队与 UED 团队共同设计开发并维护的设计系统。设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SE示例标题
Semi Design 以用户中心、内容优先、设计人性化的设计系统,打造一致、好看、好用、高效的用户体验。
Code
<script lang="ts">
import { Avatar, Button, ButtonGroup, List, ListItem } from '@fibrae/semi-ui-svelte';
const text = 'Semi Design 是由抖音前端团队与 UED 团队共同设计开发并维护的设计系统。设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。';
const data = [
{
text,
style: 'color: var(--semi-color-text-2); margin: 4px 0; width: 420px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis',
},
{ text, style: 'color: var(--semi-color-text-2); margin: 4px 0; width: 500px' },
{
text: 'Semi Design 以用户中心、内容优先、设计人性化的设计系统,打造一致、好看、好用、高效的用户体验。',
style: 'color: var(--semi-color-text-2); margin: 4px 0; width: 500px',
},
];
</script>
<div style="padding: 12px; border: 1px solid var(--semi-color-border); margin: 12px">
<List dataSource={data}>
{#snippet item({ item: entry })}
<ListItem>
{#snippet header()}
<Avatar color="blue">SE</Avatar>
{/snippet}
{#snippet main()}
<div>
<span style="color: var(--semi-color-text-0); font-weight: 500">示例标题</span>
<p style={entry.style}>{entry.text}</p>
</div>
{/snippet}
{#snippet extra()}
<ButtonGroup theme="borderless">
<Button>编辑</Button>
<Button>更多</Button>
</ButtonGroup>
{/snippet}
</ListItem>
{/snippet}
</List>
</div>Layout
Use layout property to set list layout, one of vertical(default) or horizontal.
- SESemi Design Title 1
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi Design Title 2
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi Design Title 3
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
Code
<script lang="ts">
import { Avatar, List, ListItem, type AvatarColor } from '@fibrae/semi-ui-svelte';
interface Entry {
title: string;
color: AvatarColor;
}
const data: Entry[] = [
{ title: 'Semi Design Title 1', color: 'light-blue' },
{ title: 'Semi Design Title 2', color: 'grey' },
{ title: 'Semi Design Title 3', color: 'light-green' },
];
</script>
<div style="padding: 12px; border: 1px solid var(--semi-color-border); margin: 12px">
<List dataSource={data} layout="horizontal">
{#snippet item({ item: entry })}
<ListItem>
{#snippet header()}
<Avatar color={entry.color}>SE</Avatar>
{/snippet}
{#snippet main()}
<div>
<span style="color: var(--semi-color-text-0); font-weight: 500">{entry.title}</span>
<p style="color: var(--semi-color-text-2); margin: 4px 0">Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
</div>
{/snippet}
</ListItem>
{/snippet}
</List>
</div>Grid
Use grid property to set grid layout. Use span to set the number of occupying spaces for each item and gutter for spacing between items.
Semi UI
| 满意度 | |
|---|---|
| 反馈数 | 124 |
Semi DSM
| 满意度 | |
|---|---|
| 反馈数 | 108 |
Semi D2C
| 满意度 | |
|---|---|
| 反馈数 | 244 |
Code
<script lang="ts">
import { Button, ButtonGroup, Descriptions, List, ListItem, Rating } from '@fibrae/semi-ui-svelte';
const data = [
{ title: 'Semi UI', rating: 4.5, feedbacks: 124 },
{ title: 'Semi DSM', rating: 4, feedbacks: 108 },
{ title: 'Semi D2C', rating: 4.5, feedbacks: 244 },
];
const grid = { gutter: 12, span: 6 };
const itemStyle = 'border: 1px solid var(--semi-color-border); background-color: var(--semi-color-bg-2); border-radius: 3px; padding-left: 20px';
</script>
<div>
<List dataSource={data} {grid}>
{#snippet item({ item: entry })}
<ListItem style={itemStyle}>
<div>
<h3 style="color: var(--semi-color-text-0); font-weight: 500">{entry.title}</h3>
<Descriptions align="center" data={[{ key: '满意度' }, { key: '反馈数', value: entry.feedbacks }]} row size="small">
{#snippet value({ index, item: field })}
{#if index === 0}
<Rating allowHalf size="small" value={entry.rating} />
{:else}
{field.value}
{/if}
{/snippet}
</Descriptions>
<div style="margin: 12px 0; display: flex; justify-content: flex-end">
<ButtonGroup style="margin-top: 8px" theme="borderless">
<Button>编辑</Button>
<Button>更多</Button>
</ButtonGroup>
</div>
</div>
</ListItem>
{/snippet}
</List>
</div>Responsive List
Refer to Grid for responsive dimensions.
审核管理平台
| 满意度 | |
|---|---|
| 反馈数 | 124 |
扁鹊
| 满意度 | |
|---|---|
| 反馈数 | 108 |
直播审核平台
| 满意度 | |
|---|---|
| 反馈数 | 244 |
抖音安全测试
| 满意度 | |
|---|---|
| 反馈数 | 189 |
内容平台
| 满意度 | |
|---|---|
| 反馈数 | 128 |
策略平台
| 满意度 | |
|---|---|
| 反馈数 | 156 |
Code
<script lang="ts">
import { Button, ButtonGroup, Descriptions, List, ListItem, Rating } from '@fibrae/semi-ui-svelte';
const data = [
{ title: '审核管理平台', rating: 4.5, feedbacks: 124 },
{ title: '扁鹊', rating: 4, feedbacks: 108 },
{ title: '直播审核平台', rating: 3.5, feedbacks: 244 },
{ title: '抖音安全测试', feedbacks: 189 },
{ title: '内容平台', rating: 3, feedbacks: 128 },
{ title: '策略平台', rating: 4, feedbacks: 156 },
];
const grid = { gutter: 12, xs: 0, sm: 0, md: 12, lg: 8, xl: 8, xxl: 6 };
const style = 'border: 1px solid var(--semi-color-border); background-color: var(--semi-color-bg-2); border-radius: 3px; padding-left: 20px; margin: 8px 2px';
</script>
<div>
<List dataSource={data} {grid}>
{#snippet item({ item: entry })}
<ListItem {style}>
<div>
<h3 style="color: var(--semi-color-text-0); font-weight: 500">{entry.title}</h3>
<Descriptions align="center" data={[{ key: '满意度' }, { key: '反馈数', value: entry.feedbacks }]} row size="small">
{#snippet value({ index, item: field })}
{#if index === 0}
<Rating allowHalf size="small" value={entry.rating} />
{:else}
{field.value}
{/if}
{/snippet}
</Descriptions>
<div style="margin: 12px 0; display: flex; justify-content: flex-end">
<ButtonGroup style="margin-top: 8px" theme="borderless">
<Button>编辑</Button>
<Button>更多</Button>
</ButtonGroup>
</div>
</div>
</ListItem>
{/snippet}
</List>
</div>Load More
You can use the loadMore snippet to achieve loading state for more incoming contents.
Code
<script lang="ts">
import { onDestroy } from 'svelte';
import { Avatar, Button, List, ListItem, Skeleton, SkeletonAvatar, SkeletonParagraph, SkeletonTitle } from '@fibrae/semi-ui-svelte';
interface Book {
title?: string;
loading: boolean;
}
const count = 3;
const data: Book[] = [];
for (let i = 0; i < 40; i++) {
data.push({ title: `Semi Design Title ${i}`, loading: false });
}
const placeholders: Book[] = [0, 1, 2].map(() => ({ loading: true }));
const fetchData = (page: number) =>
new Promise<Book[]>((resolve) => {
setTimeout(() => resolve(data.slice(page * count, page * count + count)), 1000);
});
let dataSource = $state.raw<Book[]>([]);
let loading = $state(true);
let noMore = $state(false);
let page = 0;
let disposed = false;
onDestroy(() => {
disposed = true;
});
const load = async () => {
loading = true;
const newDataSource = await fetchData(page);
if (disposed) return;
dataSource = [...dataSource, ...newDataSource];
loading = false;
noMore = !newDataSource.length;
};
const onLoadMore = () => {
page += 1;
void load();
};
void load();
</script>
<List dataSource={loading ? [...dataSource, ...placeholders] : dataSource} {loading}>
{#snippet item({ item: book })}
<Skeleton loading={book.loading}>
{#snippet placeholder()}
<div style="display: flex; align-items: flex-start; padding: 12px; border-bottom: 1px solid var(--semi-color-border)">
<SkeletonAvatar style="margin-right: 12px" />
<div>
<SkeletonTitle style="width: 120px; margin-bottom: 12px; margin-top: 12px" />
<SkeletonParagraph rows={2} style="width: 600px" />
</div>
</div>
{/snippet}
<ListItem>
{#snippet header()}
<Avatar color="grey">SE</Avatar>
{/snippet}
{#snippet main()}
<div>
<span style="color: var(--semi-color-text-0); font-weight: 500">{book.title}</span>
<p style="color: var(--semi-color-text-2); margin: 4px 0">Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
</div>
{/snippet}
</ListItem>
</Skeleton>
{/snippet}
{#snippet loadMore()}
{#if !loading && !noMore}
<div style="text-align: center; margin-top: 12px; height: 32px; line-height: 32px">
<Button onclick={onLoadMore}>显示更多</Button>
</div>
{/if}
{/snippet}
</List>Scroll to Load
You can listen to the onscroll event of the scroll container and load the next page near the bottom. Recommended interaction could be reveal a loadmore button after three scrolling loads.
Code
<script lang="ts">
import { onDestroy } from 'svelte';
import { Avatar, Button, List, ListItem, Spin } from '@fibrae/semi-ui-svelte';
interface Book {
title: string;
}
const count = 5;
const dataList: Book[] = [];
for (let i = 0; i < 100; i++) {
dataList.push({ title: `Semi Design Title ${i}` });
}
const fetchData = (page: number) =>
new Promise<Book[]>((resolve) => {
setTimeout(() => resolve(dataList.slice(page * count, page * count + count)), 1000);
});
let dataSource = $state.raw<Book[]>([]);
let loading = $state(true);
let hasMore = $state(true);
let page = $state(0);
let disposed = false;
onDestroy(() => {
disposed = true;
});
const load = async () => {
loading = true;
const newDataSource = await fetchData(page);
if (disposed) return;
dataSource = [...dataSource, ...newDataSource];
loading = false;
hasMore = !!newDataSource.length;
};
const onLoadMore = () => {
page += 1;
void load();
};
const showLoadMore = $derived((page + 1) % 4 === 0);
const onScroll = (event: Event) => {
const el = event.currentTarget as HTMLElement;
if (loading || !hasMore || showLoadMore) return;
if (el.scrollHeight - el.scrollTop - el.clientHeight <= 20) onLoadMore();
};
void load();
</script>
<section aria-label="可滚动列表" class="light-scrollbar" style="height: 420px; overflow: auto; border: 1px solid var(--semi-color-border); padding: 10px" tabindex="0" onscroll={onScroll}>
<List {dataSource}>
{#snippet item({ item: book })}
<ListItem>
{#snippet header()}
<Avatar color="grey">SE</Avatar>
{/snippet}
{#snippet main()}
<div>
<span style="color: var(--semi-color-text-0); font-weight: 500">{book.title}</span>
<p style="color: var(--semi-color-text-2); margin: 4px 0">Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
</div>
{/snippet}
</ListItem>
{/snippet}
{#snippet loadMore()}
{#if !loading && hasMore && showLoadMore}
<div style="text-align: center; margin-top: 12px; height: 32px; line-height: 32px">
<Button onclick={onLoadMore}>显示更多</Button>
</div>
{/if}
{/snippet}
</List>
{#if loading && hasMore}
<div style="text-align: center">
<Spin />
</div>
{/if}
</section>Scroll to Load Infinite Lists
You can integrate TanStack Virtual (@tanstack/svelte-virtual) to implement infinite scrolling lists with virtualization, which renders only the visible rows and improves the performance for large amounts of data. createVirtualizer returns a store; read it as $virtualizer.
Code
<script lang="ts">
import { untrack } from 'svelte';
import { SvelteSet } from 'svelte/reactivity';
import { createVirtualizer } from '@tanstack/svelte-virtual';
import { Avatar, List, ListItem, Spin } from '@fibrae/semi-ui-svelte';
interface Book {
title: string;
}
const rowCount = 50;
const rowHeight = 118;
// Rows fetched ahead of the window so they arrive before they scroll into view
const prefetch = 15;
const dataList: Book[] = Array.from({ length: rowCount }, (_, i) => ({ title: `Semi Design Title ${i}` }));
let scroller = $state<HTMLElement>();
const loaded = new SvelteSet<number>();
// A requested index is never fetched again, even while pending
const requested = new SvelteSet<number>();
const virtualizer = createVirtualizer<HTMLElement, Element>({ count: rowCount, getScrollElement: () => scroller ?? null, estimateSize: () => rowHeight, overscan: 2 });
// The scroller exists only after mount, so the virtualizer is handed it then. setOptions updates the store this
// effect would otherwise be subscribed to, so it runs untracked.
$effect(() => {
if (scroller) untrack(() => $virtualizer.setOptions({ getScrollElement: () => scroller ?? null }));
});
const items = $derived($virtualizer.getVirtualItems());
// Indexes rather than virtual items: the items are new objects on every scroll
const rows = $derived(items.filter((item) => loaded.has(item.index)).map((item) => item.index));
$effect(() => {
if (!items.length) return;
const first = items[0].index;
const last = Math.min(items[items.length - 1].index + prefetch, rowCount - 1);
// requested is written here, so reading it must not make this effect depend on it
const wanted = untrack(() => {
const fresh: number[] = [];
for (let i = first; i <= last; i++) {
if (!requested.has(i)) {
requested.add(i);
fresh.push(i);
}
}
return fresh;
});
if (!wanted.length) return;
setTimeout(() => {
for (const i of wanted) loaded.add(i);
}, 1000);
});
</script>
<div style="border: 1px solid var(--semi-color-border); padding: 10px">
<section aria-label="虚拟滚动列表" bind:this={scroller} style="height: 500px; overflow: auto" tabindex="0">
<List dataSource={rows} style="position: relative; height: {$virtualizer.getTotalSize()}px">
<!-- Replaces the locale's empty text until a row of the window arrives -->
{#snippet emptyContent()}
<Spin />
{/snippet}
{#snippet item({ item: index })}
<ListItem style="position: absolute; inset-inline: 0; top: {index * rowHeight}px; height: {rowHeight}px">
{#snippet header()}
<Avatar color="grey">SE</Avatar>
{/snippet}
{#snippet main()}
<div>
<span style="color: var(--semi-color-text-0); font-weight: 500">{dataList[index].title}</span>
<p style="color: var(--semi-color-text-2); margin: 4px 0">Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
</div>
{/snippet}
</ListItem>
{/snippet}
</List>
</section>
</div>Drag Sort
You can integrate the Svelte build of dnd-kit (@dnd-kit/svelte) to implement drag and drop sort. The dragged element is the li rendered by ListItem. @dnd-kit/svelte leaves out OptimisticSortingPlugin (it moves DOM nodes itself, which collides with Svelte's keyed each), so the order during a drag is driven by state set in onDragOver. Write the sortable items inside the List tag with {#each ... (item.id)}, keyed by id; the dataSource plus item snippet form is keyed by position, so after a reorder a node would be matched to other data.
- SESemi Design Title 1
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi Design Title 2
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi Design Title 3
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi Design Title 4
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi Design Title 5
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
Code
<script lang="ts">
import { untrack, type ComponentProps } from 'svelte';
import { RestrictToVerticalAxis } from '@dnd-kit/abstract/modifiers';
import { Accessibility, DragDropManager } from '@dnd-kit/dom';
import { isSortable, OptimisticSortingPlugin, Sortable } from '@dnd-kit/dom/sortable';
import { DragDropProvider } from '@dnd-kit/svelte';
import { Avatar, List, ListItem, type AvatarColor } from '@fibrae/semi-ui-svelte';
interface Entry {
id: number;
title: string;
color: AvatarColor;
}
type ProviderProps = ComponentProps<typeof DragDropProvider>;
const data: Entry[] = [
{ id: 1, title: 'Semi Design Title 1', color: 'red' },
{ id: 2, title: 'Semi Design Title 2', color: 'grey' },
{ id: 3, title: 'Semi Design Title 3', color: 'light-green' },
{ id: 4, title: 'Semi Design Title 4', color: 'light-blue' },
{ id: 5, title: 'Semi Design Title 5', color: 'pink' },
];
let listItems = $state.raw(data);
// The order shown while dragging; cleared on drop, so `listItems` stays the only truth
let preview = $state.raw<Entry[] | null>(null);
let draggingId = $state<number | string>();
const ordered = $derived(preview ?? listItems);
// The provider overwrites the plugins and modifiers of the manager it is given with its own props, so they go there
const manager = new DragDropManager();
// Accessibility would add role="button" and a Tab stop to each li; pointer dragging only
const plugins: ProviderProps['plugins'] = (defaults) => defaults.filter((plugin) => plugin !== Accessibility);
const modifiers = [RestrictToVerticalAxis];
// OptimisticSortingPlugin moves DOM nodes itself, which collides with Svelte's keyed each; `preview` drives the order instead
const sortablePlugins: ConstructorParameters<typeof Sortable>[0]['plugins'] = (defaults) => defaults.filter((plugin) => plugin !== OptimisticSortingPlugin);
// A non-clickable ListItem hands the attachment its li, which is the element dragged
const sortable = (id: number, position: () => number) => (element: HTMLElement) => {
const item = new Sortable({ id, index: untrack(position), element, plugins: sortablePlugins }, manager);
$effect(() => {
item.index = position();
});
return () => item.destroy();
};
const onDragStart: ProviderProps['onDragStart'] = ({ operation: { source } }) => {
draggingId = source?.id;
};
const onDragOver: ProviderProps['onDragOver'] = ({ operation: { source, target } }) => {
if (!isSortable(source) || !isSortable(target) || source.id === target.id) return;
const next = [...ordered];
const from = next.findIndex((entry) => entry.id === source.id);
const to = next.findIndex((entry) => entry.id === target.id);
if (from < 0 || to < 0) return;
next.splice(to, 0, ...next.splice(from, 1));
preview = next;
};
const onDragEnd: ProviderProps['onDragEnd'] = ({ canceled }) => {
if (!canceled) listItems = ordered;
preview = null;
draggingId = undefined;
};
</script>
<div style="padding: 12px; border: 1px solid var(--semi-color-border); margin: 12px">
<DragDropProvider {manager} {modifiers} {onDragEnd} {onDragOver} {onDragStart} {plugins}>
<List>
<!-- Keyed by id: dnd-kit reorders li nodes mid-drag, and the keyed each moves them with their data -->
{#each ordered as entry, position (entry.id)}
{@const dragging = draggingId === entry.id}
<ListItem {@attach sortable(entry.id, () => position)} class={dragging ? 'isDragging' : undefined} style="border: 1px solid var(--semi-color-border); margin-bottom: 12px; cursor: grabbing{dragging ? '; z-index: 999; position: relative; background-color: var(--semi-color-bg-0)' : ''}">
{#snippet header()}
<Avatar color={entry.color}>SE</Avatar>
{/snippet}
{#snippet main()}
<div>
<span style="color: var(--semi-color-text-0); font-weight: 500">{entry.title}</span>
<p style="color: var(--semi-color-text-2); margin: 4px 0">Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
</div>
{/snippet}
</ListItem>
{/each}
</List>
</DragDropProvider>
</div>With Pagination
You can use Pagination in combination to achieve a paged List
- 围城
- 平凡的世界(全三册)
- 三体(全集)
- 雪中悍刀行(全集)
- 1/3
Code
<script lang="ts">
import { List, ListItem, Pagination } from '@fibrae/semi-ui-svelte';
const data = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚', '月亮与六便士', '沉默的大多数'];
const pageSize = 4;
let page = $state(1);
const pageData = $derived(data.slice((page - 1) * pageSize, page * pageSize));
</script>
<div>
<div style="margin-right: 16px; width: 280px; display: flex; flex-wrap: wrap">
<List class="component-list-demo-booklist" dataSource={pageData} size="small" split={false} style="border: 1px solid var(--semi-color-border); flex-basis: 100%; flex-shrink: 0">
{#snippet item({ item: book })}
<ListItem class="list-item">{book}</ListItem>
{/snippet}
</List>
<Pagination bind:currentPage={page} {pageSize} size="small" style="width: 100%; flex-basis: 100%; justify-content: center" total={data.length} />
</div>
</div>With filter
You can use it by assembling Input to filter the List
- 围城
- 平凡的世界(全三册)
- 三体(全集)
- 雪中悍刀行(全集)
- 撒哈拉的故事
- 明朝那些事
- 一禅小和尚
- 沙丘
- 被讨厌的勇气
- 罪与罚
Code
<script lang="ts">
import { IconSearch } from '@fibrae/semi-icons-svelte';
import { Input, List, ListItem } from '@fibrae/semi-ui-svelte';
const data = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚'];
let list = $state.raw(data);
const onSearch = (keyword?: string) => {
list = keyword ? data.filter((book) => book.includes(keyword)) : data;
};
</script>
<div>
<div style="margin-right: 16px; width: 280px; display: flex; flex-wrap: wrap; border: 1px solid var(--semi-color-border)">
<List class="component-list-demo-booklist" dataSource={list} size="small" split={false} style="flex-basis: 100%; flex-shrink: 0; border-bottom: 1px solid var(--semi-color-border)">
{#snippet header()}
<Input onChange={(value) => !value && onSearch()} onCompositionEnd={(event) => onSearch((event.target as HTMLInputElement).value)} placeholder="搜索">
{#snippet prefix()}
<IconSearch />
{/snippet}
</Input>
{/snippet}
{#snippet item({ item: book })}
<ListItem class="list-item">{book}</ListItem>
{/snippet}
</List>
</div>
</div>Add delete item
- 围城
- 平凡的世界(全三册)
- 三体(全集)
- 雪中悍刀行(全集)
- 撒哈拉的故事
- 明朝那些事
- 一禅小和尚
- 沙丘
Code
<script lang="ts">
import { IconMinusCircle, IconPlusCircle } from '@fibrae/semi-icons-svelte';
import { Button, List } from '@fibrae/semi-ui-svelte';
const data = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚', '月亮与六便士', '沉默的大多数', '第一人称单数'];
let list = $state.raw(data.slice(0, 8));
const removeItem = (book: string) => {
list = list.filter((entry) => entry !== book);
};
const addItem = () => {
list = list.concat(data.slice(list.length, list.length + 1));
};
</script>
<div>
<div style="margin-right: 16px; width: 280px; display: flex; flex-wrap: wrap; border: 1px solid var(--semi-color-border)">
<List class="component-list-demo-booklist" dataSource={list} size="small" split={false} style="flex-basis: 100%; flex-shrink: 0; border-bottom: 1px solid var(--semi-color-border)">
{#snippet item({ item: book })}
<li class="list-item" style="margin: 4px">
<Button aria-label={`删除${book}`} style="margin-right: 4px" theme="borderless" type="danger" onclick={() => removeItem(book)}>
{#snippet icon()}
<IconMinusCircle />
{/snippet}
</Button>
{book}
</li>
{/snippet}
</List>
<div style="margin: 4px; font-size: 14px">
<Button aria-label="新增书籍" style="margin-right: 4px; color: var(--semi-color-info)" theme="borderless" onclick={addItem}>
{#snippet icon()}
<IconPlusCircle />
{/snippet}
</Button>
新增书籍
</div>
</div>
</div>Single or multiple selection
You can enhance the List into a list selector by combining Radio or Checkbox
- 围城
- 平凡的世界(全三册)
- 三体(全集)
- 雪中悍刀行(全集)
- 撒哈拉的故事
- 明朝那些事
- 一禅小和尚
- 沙丘
Code
<script lang="ts">
import { Checkbox, CheckboxGroup, List, ListItem, Radio, RadioGroup, type CheckboxValue, type RadioValue } from '@fibrae/semi-ui-svelte';
const data = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚', '月亮与六便士', '沉默的大多数', '第一人称单数'];
const pageData = data.slice(0, 8);
let checkboxVal = $state<CheckboxValue[]>([data[0]]);
let radioVal = $state<RadioValue>(data[0]);
const listStyle = 'border: 1px solid var(--semi-color-border); flex-basis: 100%; flex-shrink: 0';
</script>
<div style="display: flex">
<div style="margin-right: 16px; width: 280px; display: flex; flex-wrap: wrap">
<CheckboxGroup aria-label="书单多选" bind:value={checkboxVal}>
<List class="component-list-demo-booklist" dataSource={pageData} size="small" split={false} style={listStyle}>
{#snippet item({ item: book })}
<ListItem class="list-item">
<Checkbox value={book}>{book}</Checkbox>
</ListItem>
{/snippet}
</List>
</CheckboxGroup>
</div>
<div style="margin-right: 16px; width: 280px; display: flex; flex-wrap: wrap">
<RadioGroup aria-label="书单单选" bind:value={radioVal}>
<List class="component-list-demo-booklist" dataSource={pageData} size="small" split={false} style={listStyle}>
{#snippet item({ item: book })}
<ListItem class="list-item">
<Radio value={book}>{book}</Radio>
</ListItem>
{/snippet}
</List>
</RadioGroup>
</div>
</div>Keyboard events
You can monitor the keyboard events of the corresponding keys by yourself to realize the selection of different items. As in the following example, you can use the up and down arrow keys to select different items
- 围城
- 平凡的世界(全三册)
- 三体(全集)
- 雪中悍刀行(全集)
- 撒哈拉的故事
- 明朝那些事
- 一禅小和尚
- 沙丘
- 被讨厌的勇气
- 罪与罚
Code
<script lang="ts">
import { List, ListItem } from '@fibrae/semi-ui-svelte';
const list = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚'];
let hoverIndex = $state(-1);
const changeIndex = (offset: number) => {
let index = hoverIndex + offset;
if (index < 0) {
index = list.length - 1;
}
if (index >= list.length) {
index = 0;
}
hoverIndex = index;
};
const onKeydown = (event: KeyboardEvent) => {
if (event.key === 'ArrowUp') {
event.preventDefault();
changeIndex(-1);
} else if (event.key === 'ArrowDown') {
event.preventDefault();
changeIndex(1);
}
};
</script>
<svelte:window onkeydown={onKeydown} />
<div>
<div style="margin-right: 16px; width: 280px; display: flex; flex-wrap: wrap; border: 1px solid var(--semi-color-border)">
<List class="component-list-demo-booklist" dataSource={list} size="small" split={false} style="flex-basis: 100%; flex-shrink: 0; border-bottom: 1px solid var(--semi-color-border)">
{#snippet item({ item: book, index })}
<ListItem class={index === hoverIndex ? 'component-list-demo-booklist-active-item' : undefined}>
{book}
</ListItem>
{/snippet}
</List>
</div>
</div>The custom styles involved in the Demo of the above book list example are as follows
.component-list-demo-booklist {
.list-item {
&:hover {
background-color: var(--semi-color-fill-0);
}
&:active {
background-color: var(--semi-color-fill-1);
}
}
}
.component-list-demo-booklist-active-item {
background-color: var(--semi-color-fill-0);
}
Accessibility
Pass aria-label or aria-labelledby to List to describe the list; the root element then gets role="group".
A ListItem with onClick or onRightClick (including those set on the List) stays an <li> list item and renders a native <button type="button"> inside it as its only focus target, which screen readers announce as a button.
- The button wraps the content of the
headerandmainsnippets (or the content inside the tag when there are none) and its click area covers the whole row. After tabbing to it, Enter or Space triggersonClickand the context-menu key triggersonRightClick extrais rendered outside the button on a higher layer, so buttons and links in it can be focused and clicked normally, and clicking them does not trigger the Item'sonClick- When the content inside the tag is combined with
headerormain, it sits outside the button and is covered by the full-row button, so put interactive content inextra - A ListItem without a click callback renders no button and is not focusable
A List.Item used outside a List renders as a plain list item without grid. With grid, the Row that holds the items has role="list" and the Col around each item has role="none", so screen readers read the items as members of that list.
API reference
List
Props
| Property | Description | Type | Default |
|---|---|---|---|
| bordered | Whether to show a border. | boolean | false |
| children | ListItem children, written inside the tag and rendered after the items of dataSource. | Snippet | - |
| dataSource | Data source of the list; each entry is rendered through the item snippet. | T[] | - |
| emptyContent | Snippet for the content shown when there is no data; the locale's empty text by default. A list with content inside its tag (children) is never empty. | Snippet | - |
| footer | Snippet for the list footer. | Snippet | - |
| grid | Grid configuration: align, gutter, justify and type of Row, and span, offset, order, push, pull, the responsive breakpoints, class and style of Col. class and style land on the Col around each item. | Grid | - |
| header | Snippet for the list header. | Snippet | - |
| item | Snippet that renders each entry of dataSource, receiving { item, index } and usually returning a ListItem. | TypeSnippet<[ListItemSnippetProps<T>]> | - |
| layout | List layout: vertical or horizontal. | 'vertical' | 'horizontal' | "vertical" |
| loading | Whether the list is loading; a Spin is shown when true. | boolean | false |
| loadMore | Snippet for the load-more button. | Snippet | - |
| onClick | Click handler for the items that have no onClick of their own; an item with a handler renders a native button. It is a function prop rather than the native onclick because its presence decides whether an item renders a button. | Type(event: MouseEvent) => void | - |
| onRightClick | Context-menu handler for the items that have no onRightClick of their own; those items also render a native button. | Type(event: MouseEvent) => void | - |
| size | List size: small, default or large. | Size | "default" |
| split | Whether to show dividers between items. | boolean | true |
class, style and native attributes (such as data-*) land on the root element; when aria-label or aria-labelledby is passed without a role, the root element gets role="group". The payload of onClick and onRightClick is the native MouseEvent, and they act on the items, not on the root element.
List grid props
grid accepts only the Row props align, gutter, justify, type and the Col props span, offset, order, push, pull, the responsive breakpoints, class and style; other attributes are not accepted. class and style land on the Col around each item, and style is a string. Refer to Grid for their meaning.
| Properties | Instructions | type | Default |
|---|---|---|---|
| span | Number of grid spaces | number | - |
| gutter | Grid spacing | number | 0 |
| xs | < 576px responsive grid, a number or an object containing other attributes | number|object | - |
| sm | ≥ 576px responsive grid, a number or an object containing other properties | number|object | - |
| md | ≥ 768px responsive grid, a number or an object containing other properties | number|object | - |
| lg | ≥ 992px responsive grid, a number or an object containing other properties | number|object | - |
| xl | ≥ 1200px responsive grid, a number or an object containing other properties | number|object | - |
| xxl | ≥ 1600px responsive grid, a number or an object containing other properties | number|object | - |
List.Item
Props
| Property | Description | Type | Default |
|---|---|---|---|
| align | Vertical alignment of header and main: flex-start, flex-end, center, baseline or stretch; flex-start when omitted. | 'flex-start' | 'flex-end' | 'center' | 'baseline' | 'stretch' | "flex-start" |
| children | Item content written inside the tag; inside the button when the item is clickable and has no header or main, otherwise outside it. | Snippet | - |
| extra | Snippet for additional item content, rendered outside the button; interactive elements in it stay focusable and clickable, and clicking them does not trigger the row's click. | Snippet | - |
| header | Snippet for the item header content. | Snippet | - |
| main | Snippet for the item main content. | Snippet | - |
| onClick | Click handler, taking precedence over the List's onClick; an item with a handler renders a native button. | Type(event: MouseEvent) => void | - |
| onRightClick | Context-menu handler, taking precedence over the List's onRightClick; an item with only this handler also renders a native button. | Type(event: MouseEvent) => void | - |
class, style and native attributes (such as data-*) land on the li, not on the outer Col in grid mode. When clickable, aria-* and tabindex land on the inner button instead.
Content Guidelines
- Capitalize the first letter
- do not follow punctuation at the end
- Grammatical parallelism: mixed use of active and passive, declarative and imperative sentences
Design Tokens
| Sass variable | Value | Usage |
|---|---|---|
| $color-list_default-border-default | var(--semi-color-border) | 列表描边颜色 |
| $color-list_empty-text-default | var(--semi-color-text-2) | 空状态下列表文字颜色 |
| $spacing-list_empty-paddingX | 0px | 空状态列表水平内边距 |
| $spacing-list_empty-paddingY | $spacing-base-tight | 空状态列表垂直内边距 |
| $spacing-list_footer-paddingX | $spacing-base-tight | 列表 footer 水平内边距 |
| $spacing-list_footer-paddingY | $spacing-loose | 列表 footer 垂直内边距 |
| $spacing-list_item-paddingX | $spacing-base-tight | 列表 item 水平内边距 - 默认尺寸 |
| $spacing-list_item-paddingY | $spacing-loose | 列表 item 垂直内边距 - 默认尺寸 |
| $spacing-list_small-paddingX | $spacing-tight | 列表 item 水平内边距 - 小尺寸 |
| $spacing-list_small-paddingY | $spacing-base | 列表 item 垂直内边距 - 小尺寸 |
| $spacing-list_large-paddingX | $spacing-base | 列表 item 水平内边距 - 大尺寸 |
| $spacing-list_large-paddingY | $spacing-loose | 列表 item 垂直内边距 - 大尺寸 |
| $spacing-list_header-marginRight | $spacing-base-loose | 列表 header 右侧外边距 |
| $spacing-list_extra-marginLeft | $spacing-super-loose | 列表 extra 额外内容左侧外边距 |