Demos
How to import
import { List } from '@fibrous/semi-ui-react';
Basic Usage
You can use size to size list. Supported values include large, default, small. Header and Footer customized.
Default Size
- 从明天起,做一个幸福的人
- 喂马,劈柴,周游世界
- 从明天起,关心粮食和蔬菜
- 我有一所房子,面朝大海,春暖花开
Small Size
- 从明天起,做一个幸福的人
- 喂马,劈柴,周游世界
- 从明天起,关心粮食和蔬菜
- 我有一所房子,面朝大海,春暖花开
Large Size
- 从明天起,做一个幸福的人
- 喂马,劈柴,周游世界
- 从明天起,关心粮食和蔬菜
- 我有一所房子,面朝大海,春暖花开
Code
import { List } from '@fibrous/semi-ui-react';
export default function ListDemo() {
const data = ['从明天起,做一个幸福的人', '喂马,劈柴,周游世界', '从明天起,关心粮食和蔬菜', '我有一所房子,面朝大海,春暖花开'];
return (
<div>
<div style={{ marginRight: 16 }}>
<h3 style={{ marginBottom: 16 }}>Default Size</h3>
<List header={<div>Header</div>} footer={<div>Footer</div>} bordered dataSource={data} renderItem={(item) => <List.Item>{item}</List.Item>} />
</div>
<div style={{ marginRight: 16 }}>
<h3 style={{ margin: '16px 0' }}>Small Size</h3>
<List size="small" header={<div>Header</div>} footer={<div>Footer</div>} bordered dataSource={data} renderItem={(item) => <List.Item>{item}</List.Item>} />
</div>
<div style={{ marginRight: 16 }}>
<h3 style={{ margin: '16px 0' }}>Large Size</h3>
<List size="large" header={<div>Header</div>} footer={<div>Footer</div>} bordered dataSource={data} renderItem={(item) => <List.Item>{item}</List.Item>} />
</div>
</div>
);
}Template
List.Item has a built-in template consisting of: header, main, and extra. 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
import type { CSSProperties } from 'react';
import { List, Avatar, ButtonGroup, Button } from '@fibrous/semi-ui-react';
const text = 'Semi Design 是由抖音前端团队与 UED 团队共同设计开发并维护的设计系统。设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。';
const data: { text: string; style: CSSProperties }[] = [
{
text,
style: { color: 'var(--semi-color-text-2)', margin: '4px 0', width: 420, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
},
{ text, style: { color: 'var(--semi-color-text-2)', margin: '4px 0', width: 500 } },
{
text: 'Semi Design 以用户中心、内容优先、设计人性化的设计系统,打造一致、好看、好用、高效的用户体验。',
style: { color: 'var(--semi-color-text-2)', margin: '4px 0', width: 500 },
},
];
export default function ListDemo() {
return (
<div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}>
<List
dataSource={data}
renderItem={(item) => (
<List.Item
header={<Avatar color="blue">SE</Avatar>}
main={
<div>
<span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>示例标题</span>
<p style={item.style}>{item.text}</p>
</div>
}
extra={
<ButtonGroup theme="borderless">
<Button>编辑</Button>
<Button>更多</Button>
</ButtonGroup>
}
/>
)}
/>
</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
import { List, Avatar, type AvatarColor } from '@fibrous/semi-ui-react';
interface Item {
title: string;
color: AvatarColor;
}
export default function ListDemo() {
const data: Item[] = [
{
title: 'Semi Design Title 1',
color: 'light-blue',
},
{
title: 'Semi Design Title 2',
color: 'grey',
},
{
title: 'Semi Design Title 3',
color: 'light-green',
},
];
return (
<div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}>
<List
dataSource={data}
layout="horizontal"
renderItem={(item) => (
<List.Item
header={<Avatar color={item.color}>SE</Avatar>}
main={
<div>
<span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</span>
<p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0' }}>Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
</div>
}
/>
)}
/>
</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
import { List, Descriptions, ButtonGroup, Rating, Button } from '@fibrous/semi-ui-react';
export default function ListDemo() {
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 style = {
border: '1px solid var(--semi-color-border)',
backgroundColor: 'var(--semi-color-bg-2)',
borderRadius: '3px',
paddingLeft: '20px',
};
return (
<div>
<List
grid={{
gutter: 12,
span: 6,
}}
dataSource={data}
renderItem={(item) => (
<List.Item style={style}>
<div>
<h3 style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</h3>
<Descriptions
align="center"
size="small"
row
data={[
{ key: '满意度', value: <Rating allowHalf size="small" value={item.rating} /> },
{ key: '反馈数', value: item.feedbacks },
]}
/>
<div style={{ margin: '12px 0', display: 'flex', justifyContent: 'flex-end' }}>
<ButtonGroup theme="borderless" style={{ marginTop: 8 }}>
<Button>编辑</Button>
<Button>更多</Button>
</ButtonGroup>
</div>
</div>
</List.Item>
)}
/>
</div>
);
}Responsive List
Refer to Grid for responsive dimensions.
审核管理平台
| 满意度 | |
|---|---|
| 反馈数 | 124 |
扁鹊
| 满意度 | |
|---|---|
| 反馈数 | 108 |
直播审核平台
| 满意度 | |
|---|---|
| 反馈数 | 244 |
抖音安全测试
| 满意度 | |
|---|---|
| 反馈数 | 189 |
内容平台
| 满意度 | |
|---|---|
| 反馈数 | 128 |
策略平台
| 满意度 | |
|---|---|
| 反馈数 | 156 |
Code
import { List, Descriptions, Rating, Button, ButtonGroup } from '@fibrous/semi-ui-react';
export default function ListDemo() {
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 style = {
border: '1px solid var(--semi-color-border)',
backgroundColor: 'var(--semi-color-bg-2)',
borderRadius: '3px',
paddingLeft: '20px',
margin: '8px 2px',
};
return (
<div>
<List
grid={{
gutter: 12,
xs: 0,
sm: 0,
md: 12,
lg: 8,
xl: 8,
xxl: 6,
}}
dataSource={data}
renderItem={(item) => (
<List.Item style={style}>
<div>
<h3 style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</h3>
<Descriptions
align="center"
size="small"
row
data={[
{ key: '满意度', value: <Rating allowHalf size="small" value={item.rating} /> },
{ key: '反馈数', value: item.feedbacks },
]}
/>
<div style={{ margin: '12px 0', display: 'flex', justifyContent: 'flex-end' }}>
<ButtonGroup theme="borderless" style={{ marginTop: 8 }}>
<Button>编辑</Button>
<Button>更多</Button>
</ButtonGroup>
</div>
</div>
</List.Item>
)}
/>
</div>
);
}Load More
You can use loadMore to achieve loading state for more incoming contents.
Code
import { useState, useEffect } from 'react';
import { List, Skeleton, Button, Avatar, type AvatarColor } from '@fibrous/semi-ui-react';
interface Book {
color?: AvatarColor;
title?: string;
loading: boolean;
}
const count = 3;
const data: Book[] = [];
for (let i = 0; i < 40; i++) {
data.push({
color: 'grey',
title: `Semi Design Title ${i}`,
loading: false,
});
}
const placeholders: Book[] = [0, 1, 2].map(() => ({ loading: true }));
const fetchData = (page: number) => {
return new Promise<Book[]>((res) => {
setTimeout(() => {
res(data.slice(page * count, page * count + count));
}, 1000);
});
};
export default function ListDemo() {
const [page, setPage] = useState(0);
const [loading, setLoading] = useState(true);
const [dataSource, setDataSource] = useState<Book[]>([]);
const [noMore, setNoMore] = useState(false);
const list = loading ? [...dataSource, ...placeholders] : dataSource;
useEffect(() => {
let ignore = false;
void fetchData(page).then((newDataSource) => {
if (!ignore) {
setDataSource((prevData) => [...prevData, ...newDataSource]);
setLoading(false);
setNoMore(!newDataSource.length);
}
});
return () => {
ignore = true;
};
}, [page]);
const onLoadMore = () => {
setLoading(true);
setPage(page + 1);
};
const loadMore =
!loading && !noMore ? (
<div
style={{
textAlign: 'center',
marginTop: 12,
height: 32,
lineHeight: '32px',
}}
>
<Button onClick={onLoadMore}>显示更多</Button>
</div>
) : null;
const placeholder = (
<div
style={{
display: 'flex',
alignItems: 'flex-start',
padding: 12,
borderBottom: '1px solid var(--semi-color-border)',
}}
>
<Skeleton.Avatar style={{ marginRight: 12 }} />
<div>
<Skeleton.Title style={{ width: 120, marginBottom: 12, marginTop: 12 }} />
<Skeleton.Paragraph style={{ width: 600 }} rows={2} />
</div>
</div>
);
return (
<List
loading={loading}
loadMore={loadMore}
dataSource={list}
renderItem={(item) => (
<Skeleton placeholder={placeholder} loading={item.loading}>
<List.Item
header={<Avatar color={item.color}>SE</Avatar>}
main={
<div>
<span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</span>
<p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0' }}>Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
</div>
}
/>
</Skeleton>
)}
/>
);
}Scroll to Load
You can integrate react-infinite-scroller to implement scrolling load list. Recommended interaction could be reveal a loadmore button after three scrolling loads.
Code
import { useState, useEffect } from 'react';
import InfiniteScroll from 'react-infinite-scroller';
import { List, Button, Avatar, Spin } from '@fibrous/semi-ui-react';
interface Book {
title: string;
}
const count = 5;
const dataList: Book[] = Array.from({ length: 100 }, (_, i) => ({ title: `Semi Design Title ${i}` }));
const fetchData = (page: number) => {
return new Promise<Book[]>((res) => {
setTimeout(() => {
res(dataList.slice(page * count, page * count + count));
}, 1000);
});
};
export default function ListDemo() {
const [page, setPage] = useState(0);
const [loading, setLoading] = useState(true);
const [dataSource, setDataSource] = useState<Book[]>([]);
const [hasMore, setHasMore] = useState(true);
useEffect(() => {
let ignore = false;
void fetchData(page).then((newDataSource) => {
if (!ignore) {
setDataSource((prevData) => [...prevData, ...newDataSource]);
setLoading(false);
setHasMore(!!newDataSource.length);
}
});
return () => {
ignore = true;
};
}, [page]);
const onLoadMore = () => {
setLoading(true);
setPage(page + 1);
};
const showLoadMore = (page + 1) % 4 === 0;
const loadMore =
!loading && hasMore && showLoadMore ? (
<div
style={{
textAlign: 'center',
marginTop: 12,
height: 32,
lineHeight: '32px',
}}
>
<Button onClick={onLoadMore}>显示更多</Button>
</div>
) : null;
return (
<section className="light-scrollbar" tabIndex={0} aria-label="可滚动列表" style={{ height: 420, overflow: 'auto', border: '1px solid var(--semi-color-border)', padding: 10 }}>
<InfiniteScroll initialLoad={false} pageStart={0} threshold={20} loadMore={onLoadMore} hasMore={!loading && hasMore && !showLoadMore} useWindow={false}>
<List
loadMore={loadMore}
dataSource={dataSource}
renderItem={(item) => (
<List.Item
header={<Avatar color="grey">SE</Avatar>}
main={
<div>
<span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</span>
<p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0' }}>Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
</div>
}
/>
)}
/>
{loading && hasMore && (
<div style={{ textAlign: 'center' }}>
<Spin />
</div>
)}
</InfiniteScroll>
</section>
);
}Scroll to Load Infinite Lists
You can integrate TanStack Virtual to implement infinite scrolling lists with virtualization, which renders only the visible rows and improves the performance for large amounts of data.
Code
import { useEffect, useRef, useState } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { Avatar, List, Spin } from '@fibrous/semi-ui-react';
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}` }));
export default function ListDemo() {
// useVirtualizer returns one mutable instance for the component's life; React Compiler's memoized reads of it would go stale
'use no memo';
const scrollRef = useRef<HTMLElement>(null);
const [loaded, setLoaded] = useState<ReadonlySet<number>>(() => new Set());
// A requested index is never fetched again, even while pending
const requested = useRef(new Set<number>());
const virtualizer = useVirtualizer({ count: rowCount, getScrollElement: () => scrollRef.current, estimateSize: () => rowHeight, overscan: 2 });
const items = virtualizer.getVirtualItems();
const start = items[0]?.index;
const stop = items.at(-1)?.index;
useEffect(() => {
if (start === undefined || stop === undefined) return;
const wanted: number[] = [];
for (let i = start; i <= Math.min(stop + prefetch, rowCount - 1); i++) {
if (!requested.current.has(i)) {
requested.current.add(i);
wanted.push(i);
}
}
if (!wanted.length) return;
setTimeout(() => setLoaded((prev) => new Set([...prev, ...wanted])), 1000);
}, [start, stop]);
return (
<div style={{ border: '1px solid var(--semi-color-border)', padding: 10 }}>
<section ref={scrollRef} aria-label="虚拟滚动列表" tabIndex={0} style={{ height: '500px', overflow: 'auto' }}>
<List style={{ position: 'relative', height: virtualizer.getTotalSize() }} emptyContent={<Spin />}>
{items
.filter((item) => loaded.has(item.index))
.map((item) => (
<List.Item
key={item.index}
style={{ position: 'absolute', insetInline: 0, top: item.start, height: rowHeight }}
header={<Avatar color="grey">SE</Avatar>}
main={
<div>
<span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{dataList[item.index].title}</span>
<p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0' }}>Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
</div>
}
/>
))}
</List>
</section>
</div>
);
}Drag Sort
You can integrate dnd-kit to implement drag and drop sort.
- 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
import { useState, type ComponentProps, type CSSProperties } from 'react';
import { RestrictToVerticalAxis } from '@dnd-kit/abstract/modifiers';
import { Accessibility } from '@dnd-kit/dom';
import { move } from '@dnd-kit/helpers';
import { DragDropProvider } from '@dnd-kit/react';
import { useSortable } from '@dnd-kit/react/sortable';
import { List, Avatar, type AvatarColor, type ListItemProps } from '@fibrous/semi-ui-react';
interface Item {
id: number;
title: string;
color: AvatarColor;
}
const data: Item[] = [
{
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',
},
];
interface SortableListItemProps extends Omit<ListItemProps, 'id' | 'title' | 'color'> {
id: number;
index: number;
title: string;
color: AvatarColor;
}
function ListItem({ id, index, title, color, ...props }: SortableListItemProps) {
const { ref, isDragging } = useSortable({ id, index });
const styles: CSSProperties = {
...props.style,
border: '1px solid var(--semi-color-border)',
marginBottom: 12,
cursor: 'grabbing',
...(isDragging ? { zIndex: 999, position: 'relative', backgroundColor: 'var(--semi-color-bg-0)' } : {}),
};
// The li itself is dragged, so items stay direct children; no Accessibility plugin, so no button role or Tab stop
// (pointer dragging only)
return <List.Item {...props} ref={ref} style={styles} className={isDragging ? 'isDragging' : undefined} />;
}
function renderDraggable(item: Item, index: number) {
return (
<ListItem
{...item}
// dnd-kit reorders li nodes mid-drag; an index key would give a moved node another item's data
key={item.id}
index={index}
header={<Avatar color={item.color}>SE</Avatar>}
main={
<div>
<span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</span>
<p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0' }}>Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
</div>
}
/>
);
}
export default function ListDemo() {
const [listItems, setListItems] = useState(data);
const handleDragEnd: ComponentProps<typeof DragDropProvider>['onDragEnd'] = (event) => {
if (!event.canceled) {
setListItems((items) => move(items, event));
}
};
return (
<div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}>
<DragDropProvider plugins={(defaults) => defaults.filter((plugin) => plugin !== Accessibility)} modifiers={[RestrictToVerticalAxis]} onDragEnd={handleDragEnd}>
<List dataSource={listItems} renderItem={renderDraggable} />
</DragDropProvider>
</div>
);
}With Pagination
You can use Pagination in combination to achieve a paged List
- 围城
- 平凡的世界(全三册)
- 三体(全集)
- 雪中悍刀行(全集)
- 1/3
Code
import { useState } from 'react';
import { List, Pagination } from '@fibrous/semi-ui-react';
export default function ListDemo() {
const data = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚', '月亮与六便士', '沉默的大多数'];
const [page, onPageChange] = useState(1);
let pageSize = 4;
const getData = (page: number) => {
let start = (page - 1) * pageSize;
let end = page * pageSize;
return data.slice(start, end);
};
return (
<div>
<div style={{ marginRight: 16, width: 280, display: 'flex', flexWrap: 'wrap' }}>
<List dataSource={getData(page)} split={false} size="small" className="component-list-demo-booklist" style={{ border: '1px solid var(--semi-color-border)', flexBasis: '100%', flexShrink: 0 }} renderItem={(item) => <List.Item className="list-item">{item}</List.Item>} />
<Pagination size="small" style={{ width: '100%', flexBasis: '100%', justifyContent: 'center' }} pageSize={pageSize} total={data.length} currentPage={page} onChange={(cPage) => onPageChange(cPage)} />
</div>
</div>
);
}With filter
You can use it by assembling Input to filter the List
- 围城
- 平凡的世界(全三册)
- 三体(全集)
- 雪中悍刀行(全集)
- 撒哈拉的故事
- 明朝那些事
- 一禅小和尚
- 沙丘
- 被讨厌的勇气
- 罪与罚
Code
import { useState } from 'react';
import { IconSearch } from '@fibrous/semi-icons-react';
import { List, Input } from '@fibrous/semi-ui-react';
export default function ListDemo() {
const data = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚'];
const [list, setList] = useState(data);
const onSearch = (string?: string) => {
let newList: string[];
if (string) {
newList = data.filter((item) => item.includes(string));
} else {
newList = data;
}
setList(newList);
};
return (
<div>
<div
style={{
marginRight: 16,
width: 280,
display: 'flex',
flexWrap: 'wrap',
border: '1px solid var(--semi-color-border)',
}}
>
<List
className="component-list-demo-booklist"
dataSource={list}
split={false}
header={<Input onCompositionEnd={(v) => onSearch((v.target as HTMLInputElement).value)} onChange={(v) => (!v ? onSearch() : null)} placeholder="搜索" prefix={<IconSearch />} />}
size="small"
style={{ flexBasis: '100%', flexShrink: 0, borderBottom: '1px solid var(--semi-color-border)' }}
renderItem={(item) => <List.Item className="list-item">{item}</List.Item>}
/>
</div>
</div>
);
}Add delete item
- 围城
- 平凡的世界(全三册)
- 三体(全集)
- 雪中悍刀行(全集)
- 撒哈拉的故事
- 明朝那些事
- 一禅小和尚
- 沙丘
Code
import { useState } from 'react';
import { IconMinusCircle, IconPlusCircle } from '@fibrous/semi-icons-react';
import { List, Button } from '@fibrous/semi-ui-react';
export default function ListDemo() {
const data = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚', '月亮与六便士', '沉默的大多数', '第一人称单数'];
const [list, setList] = useState(data.slice(0, 8));
const updateList = (item?: string) => {
let newList: string[];
if (item) {
newList = list.filter((i) => item !== i);
} else {
newList = list.concat(data.slice(list.length, list.length + 1));
}
setList(newList);
};
return (
<div>
<div
style={{
marginRight: 16,
width: 280,
display: 'flex',
flexWrap: 'wrap',
border: '1px solid var(--semi-color-border)',
}}
>
<List
className="component-list-demo-booklist"
dataSource={list}
split={false}
size="small"
style={{ flexBasis: '100%', flexShrink: 0, borderBottom: '1px solid var(--semi-color-border)' }}
renderItem={(item) => (
<li style={{ margin: 4 }} className="list-item">
<Button type="danger" theme="borderless" aria-label={`删除${item}`} icon={<IconMinusCircle />} onClick={() => updateList(item)} style={{ marginRight: 4 }} />
{item}
</li>
)}
/>
<div style={{ margin: 4, fontSize: 14 }}>
<Button theme="borderless" aria-label="新增书籍" icon={<IconPlusCircle />} onClick={() => updateList()} style={{ marginRight: 4, color: 'var(--semi-color-info)' }} />
新增书籍
</div>
</div>
</div>
);
}Single or multiple selection
You can enhance the List into a list selector by combining Radio or Checkbox
- 围城
- 平凡的世界(全三册)
- 三体(全集)
- 雪中悍刀行(全集)
- 撒哈拉的故事
- 明朝那些事
- 一禅小和尚
- 沙丘
Code
import { useState } from 'react';
import { List, Checkbox, Radio, RadioGroup, CheckboxGroup, type CheckboxValue } from '@fibrous/semi-ui-react';
const data = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚', '月亮与六便士', '沉默的大多数', '第一人称单数'];
const dataSource = data.slice(0, 8);
export default function ListDemo() {
const [checkboxVal, setCV] = useState<CheckboxValue[]>([data[0]]);
const [radioVal, setRV] = useState(data[0]);
return (
<div style={{ display: 'flex' }}>
<div style={{ marginRight: 16, width: 280, display: 'flex', flexWrap: 'wrap' }}>
<CheckboxGroup value={checkboxVal} onChange={(value) => setCV(value)}>
<List
dataSource={dataSource}
className="component-list-demo-booklist"
split={false}
size="small"
style={{ border: '1px solid var(--semi-color-border)', flexBasis: '100%', flexShrink: 0 }}
renderItem={(item) => (
<List.Item className="list-item">
<Checkbox value={item}>{item}</Checkbox>
</List.Item>
)}
/>
</CheckboxGroup>
</div>
<div style={{ marginRight: 16, width: 280, display: 'flex', flexWrap: 'wrap' }}>
<RadioGroup value={radioVal} onChange={(e) => setRV(e.target.value)}>
<List
className="component-list-demo-booklist"
dataSource={dataSource}
split={false}
size="small"
style={{ border: '1px solid var(--semi-color-border)', flexBasis: '100%', flexShrink: 0 }}
renderItem={(item) => (
<List.Item className="list-item">
<Radio value={item}>{item}</Radio>
</List.Item>
)}
/>
</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
import { useState, useEffect } from 'react';
import { List } from '@fibrous/semi-ui-react';
const list = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚'];
export default function ListDemo() {
const [hoverIndex, setHi] = useState(-1);
useEffect(() => {
let changeIndex = (offset: number) => {
setHi((currentIndex) => {
let index = currentIndex + offset;
if (index < 0) {
index = list.length - 1;
}
if (index >= list.length) {
index = 0;
}
return index;
});
};
let keydownHandler = (event: KeyboardEvent) => {
switch (event.key) {
case 'ArrowUp':
event.preventDefault();
changeIndex(-1);
break;
case 'ArrowDown':
event.preventDefault();
changeIndex(1);
break;
default:
break;
}
};
window.addEventListener('keydown', keydownHandler);
return () => {
window.removeEventListener('keydown', keydownHandler);
};
}, []);
return (
<div>
<div
style={{
marginRight: 16,
width: 280,
display: 'flex',
flexWrap: 'wrap',
border: '1px solid var(--semi-color-border)',
}}
>
<List
className="component-list-demo-booklist"
dataSource={list}
split={false}
size="small"
style={{ flexBasis: '100%', flexShrink: 0, borderBottom: '1px solid var(--semi-color-border)' }}
renderItem={(item, index) => <List.Item className={index === hoverIndex ? 'component-list-demo-booklist-active-item' : ''}>{item}</List.Item>}
/>
</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 List.Item 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
headerandmain(orchildrenwhen 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
childrenis combined withheaderormain,childrensits outside the button and is covered by the full-row button, so put interactive content inextra - A List.Item 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 | Child elements rendered after the items generated from dataSource; the empty state is hidden when children are present. | React.ReactNode | - |
| className | Class name of the root element. | string | - |
| dataSource | Data source of the list; each entry is rendered by renderItem, and nothing is rendered without renderItem. | T[] | - |
| emptyContent | Content shown when there is no data; the locale's empty text by default. | React.ReactNode | - |
| footer | List footer, rendered after the list content. | React.ReactNode | - |
| grid | Grid configuration: align, gutter, justify and type of Row, and span, offset, order, push, pull, the responsive breakpoints, className and style of Col. className and style land on the Col around each item. | Grid | - |
| header | List header, rendered before the list content. | React.ReactNode | - |
| layout | List layout: vertical or horizontal. | 'vertical' | 'horizontal' | "vertical" |
| loading | Whether the list is loading; a Spin is shown over the list content when true. | boolean | false |
| loadMore | Load-more button, rendered at the end of the root element. | React.ReactNode | - |
| onClick | Click handler for the items that have no onClick of their own; an item with a handler renders a native button. | TypeReact.MouseEventHandler<HTMLLIElement> | - |
| onRightClick | Context-menu handler for the items that have no onRightClick of their own; those items also render a native button. | TypeReact.MouseEventHandler<HTMLLIElement> | - |
| ref | Reference to the root div element. | TypeReact.Ref<HTMLDivElement> | - |
| renderItem | Renders each entry of dataSource, receiving the entry and its index and returning a ReactNode, usually a List.Item. | Type(item: T, ind: number) => React.ReactNode | - |
| size | List size: small, default or large. | Size | "default" |
| split | Whether to show dividers between items. | boolean | true |
| style | Style of the root element. | React.CSSProperties | - |
Other native attributes (such as data-* and id) land on the root element; when aria-label or aria-labelledby is passed without a role, the root element gets role="group". onClick and onRightClick 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, className and style; other div attributes are not accepted. 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' | 'flex-end' | 'center' | 'baseline' | 'stretch' | "flex-start" |
| children | Item content. On a clickable item set together with header or main, it renders outside the button and is covered by the full-row button, so put interactive content in extra. | React.ReactNode | - |
| className | Class name of the li; with grid it lands on the li, not on the outer Col. | string | - |
| extra | 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. | React.ReactNode | - |
| header | Item header content. | React.ReactNode | - |
| main | Item main content. | React.ReactNode | - |
| onRightClick | Context-menu handler, taking precedence over the List's onRightClick; an item with only this handler also renders a native button. | TypeReact.MouseEventHandler<HTMLLIElement> | - |
| ref | Reference to the li element. | TypeReact.Ref<HTMLLIElement> | - |
| style | Style of the li element; with grid it lands on the li, not on the outer Col. | React.CSSProperties | - |
Other native attributes (such as data-*) land on the li; when clickable, aria-* and tabIndex land on the inner button instead. onClick is applied to the li and takes precedence over the List's onClick.
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 额外内容左侧外边距 |