iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
自我挑戰組

30 天打造 Accessible UI Kit:從 shadcn/ui 到自己的 Design System系列 第 20 篇

Day 20:組出第一個 Data List:元件開始真的拿來做系統

  • 分享至 

  • xImage
  •  

前幾天,我們開始離開單一元件的世界。

Day 17 做了:

Badge
Alert
Empty State

Day 18 做了:

Table

Day 19 又加入:

Pagination

到目前為止,每一個元件單獨看都已經可以使用。

但真實系統不會長這樣:

這裡一顆 Badge

下面一張 Table

再下面一個 Pagination

真正的管理系統通常會是:

申請紀錄

[ 搜尋........................ ]

共 237 筆資料

┌──────────────────────────────────────┐
│ 姓名    申請項目    狀態    更新時間 │
├──────────────────────────────────────┤
│ 王小明  學分抵免    已通過  10/01   │
│ 陳小華  休學申請    待審核  10/01   │
│ 林小美  獎學金申請  已退回  09/30   │
└──────────────────────────────────────┘

        <  1  [2]  3  4  …  10  >

今天要做的事情,就是第一次把前面完成的元件真正組起來。

從 Component 開始進入 Pattern。


今天要完成什麼?

今天不會新增很多底層元件。

主要使用前面已經完成的:

Input
Field
Table
Badge
Pagination

組成:

Data List
│
├── Heading
├── Search
├── Result Count
├── Table
│   └── Badge
└── Pagination

同時開始處理幾個「單獨做元件時不太會遇到」的問題:

  • Search 需要 Label 嗎?
  • Placeholder 可以當 Label 嗎?
  • 搜尋結果數量要放在哪?
  • 搜尋後結果改變,要不要通知 Screen Reader?
  • Table 和 Pagination 是什麼關係?
  • Pagination 的 aria-label 要不要更具體?
  • Responsive Data List 怎麼安排?
  • Pattern 和 Component 到底差在哪?

1. Component 和 Pattern 有什麼不同?

到目前為止,我們做的大部分東西都是 Component。

例如:

<Input />
<Badge />
<Table />
<Pagination />

它們各自解決一個相對單純的 UI 問題。

但是:

Search
+
Result Count
+
Table
+
Pagination

組合起來之後,就開始形成一個:

Pattern

也就是一種在系統裡會反覆出現的 UI 組合方式。

例如:

學生資料列表
課程列表
申請紀錄
公告管理
帳號管理
報名資料

其實都可能使用:

Data List Pattern

只是裡面的資料不同。


2. 今天先不要急著做 <DataList />

看到:

Data List Pattern

很容易立刻想:

<DataList
  data={applications}
  columns={columns}
  searchable
  pagination
/>

然後開始設計一個超大型 Component。

但今天先不要。

因為我們現在還不知道:

Search 一定存在嗎?
Pagination 一定存在嗎?
Result Count 一定存在嗎?
Table 一定是唯一呈現方式嗎?
Filter 以後怎麼加入?
Action 又怎麼加入?

如果現在就做:

<DataList />

很可能最後得到:

<DataList
  searchable
  filterable
  paginated
  selectable
  sortable
  loading
  empty
  error
  compact
  striped
  ...
/>

然後成功養出一隻 Props 怪獸。

所以今天先做:

Composition Pattern

也就是使用現有元件把它組起來。

等 Pattern 穩定之後,再決定哪些東西真的值得抽象。


3. 準備測試資料

先準備一組比較接近真實系統的資料:

const applications = [
  {
    id: 1,
    name: "王小明",
    type: "學分抵免",
    status: "success",
    statusLabel: "已通過",
    updatedAt: "2026/10/01",
  },
  {
    id: 2,
    name: "陳小華",
    type: "休學申請",
    status: "warning",
    statusLabel: "待審核",
    updatedAt: "2026/10/01",
  },
  {
    id: 3,
    name: "林小美",
    type: "獎學金申請",
    status: "error",
    statusLabel: "已退回",
    updatedAt: "2026/09/30",
  },
  {
    id: 4,
    name: "張志豪",
    type: "交換學生申請",
    status: "primary",
    statusLabel: "審核中",
    updatedAt: "2026/09/29",
  },
] as const

今天先用 Client-side filtering 模擬 Search。

真正串 API、Server-side Pagination 並不是今天的重點。


4. 加入 Search

首先加入:

const [query, setQuery] = useState("")

然後過濾資料:

const filteredApplications = applications.filter((application) => {
  const keyword = query.trim().toLowerCase()

  if (!keyword) {
    return true
  }

  return (
    application.name.toLowerCase().includes(keyword) ||
    application.type.toLowerCase().includes(keyword) ||
    application.statusLabel.toLowerCase().includes(keyword)
  )
})

接著使用之前完成的 Input:

<Input
  type="search"
  value={query}
  onChange={(event) => setQuery(event.target.value)}
  placeholder="搜尋姓名、申請項目或狀態"
/>

看起來已經可以用了。

但是 Accessibility Check 馬上會發現一件事:

這個 Input 的 Label 在哪裡?


5. Placeholder 不是 Label

我們很常看到:

<Input placeholder="搜尋" />

然後覺得:

使用者看得到「搜尋」,應該知道這是什麼。

問題是 Placeholder 有幾個限制。

當使用者開始輸入:

王小明

原本:

搜尋姓名、申請項目或狀態

就消失了。

而且 Placeholder 的角色本來就比較接近:

輸入格式或提示。

不是欄位名稱。

所以:

placeholder
≠
label

這跟 Day 12 做 Field 時的原則完全一樣。


6. Search 還是需要 Accessible Name

最直接的方式:

<Field>
  <FieldLabel htmlFor="application-search">
    搜尋申請紀錄
  </FieldLabel>

  <Input
    id="application-search"
    type="search"
    value={query}
    onChange={(event) => setQuery(event.target.value)}
    placeholder="輸入姓名、申請項目或狀態"
  />
</Field>

這樣非常清楚。

畫面:

搜尋申請紀錄
[ 輸入姓名、申請項目或狀態........ ]

如果設計上真的不希望顯示 Label,也可以:

<FieldLabel
  htmlFor="application-search"
  className="sr-only"
>
  搜尋申請紀錄
</FieldLabel>

視覺上只看到:

[ 搜尋........................ ]

但 Input 仍然具有:

Accessible Name
→ 搜尋申請紀錄

7. 可以直接用 aria-label 嗎?

也可以:

<Input
  type="search"
  aria-label="搜尋申請紀錄"
/>

但既然 CUI 已經建立:

Field
FieldLabel
Input

在一般 Form Pattern 裡,我還是比較傾向優先使用真正的:

<label>

aria-label 可以留給真的不方便提供 Visible / Visually Hidden Label 的特殊情境。

也就是:

Native HTML first
ARIA when necessary

這個原則從前面做到現在都沒有改變。


8. 加入 Search Icon

搜尋框很常有:

🔍 Search...

如果加入 Lucide:

<SearchIcon />

這個 Icon 通常只是裝飾。

因為 Input 已經有:

搜尋申請紀錄

這個 Accessible Name。

所以:

<SearchIcon aria-hidden="true" />

即可。

不要讓 Screen Reader 得到:

搜尋圖示,搜尋申請紀錄,編輯文字

Icon 沒有增加新的資訊,就不需要被重複朗讀。


9. 顯示 Result Count

搜尋之後,我們還希望顯示:

共 4 筆資料

搜尋「王」之後:

找到 1 筆資料

可以:

<p className="text-sm text-muted-foreground">
  共 {filteredApplications.length} 筆資料
</p>

這看起來很普通,但它其實是 Data List 裡非常重要的 Feedback。

因為使用者輸入:

王

之後,需要知道:

我的搜尋到底有沒有作用?

Result Count 就是其中一個非常直接的回饋。


10. 搜尋結果改變,要不要 role="alert"?

假設:

共 237 筆

輸入搜尋條件後變成:

找到 3 筆資料

要不要:

<p role="alert">

?

通常不用。

搜尋結果數量的更新:

不是 Error
不是緊急警告
不是必須立即打斷使用者的重要訊息

所以 role="alert" 太強了。

如果真的希望動態搜尋結果可以被輔助科技得知,可以考慮:

<p
  aria-live="polite"
  className="text-sm text-muted-foreground"
>
  找到 {filteredApplications.length} 筆資料
</p>

polite 的意思比較接近:

有空的時候告訴我這個內容更新了。

而不是:

現在立刻打斷所有事情念給我聽。


11. aria-live 也不是看到動態內容就加

這裡也要小心。

如果 Search 是:

每輸入一個字
→ filter
→ result count 更新

那:

王
王小
王小明

可能每打一個字都更新 Live Region。

如果搜尋結果很多、輸入速度快,就可能變得很吵。

所以實際產品中還要考慮:

Debounce
Submit Search
Search Button
Live Region 更新頻率

今天 Demo 資料很少,可以先展示:

aria-live="polite"

這個 Pattern。

但不是說:

所有搜尋結果數量都必須加 Live Region。

Accessibility 不是「ARIA 越多越好」。


12. 把 Table 接進來

接著使用 Day 18 的 Table:

<Table>
  <TableCaption className="sr-only">
    申請紀錄搜尋結果
  </TableCaption>

  <TableHeader>
    <TableRow>
      <TableHead scope="col">
        姓名
      </TableHead>

      <TableHead scope="col">
        申請項目
      </TableHead>

      <TableHead scope="col">
        狀態
      </TableHead>

      <TableHead scope="col">
        更新時間
      </TableHead>
    </TableRow>
  </TableHeader>

  <TableBody>
    {filteredApplications.map((application) => (
      <TableRow key={application.id}>
        <TableCell>
          {application.name}
        </TableCell>

        <TableCell>
          {application.type}
        </TableCell>

        <TableCell>
          <Badge variant={application.status}>
            {application.statusLabel}
          </Badge>
        </TableCell>

        <TableCell>
          <time dateTime={application.updatedAt.replaceAll("/", "-")}>
            {application.updatedAt}
          </time>
        </TableCell>
      </TableRow>
    ))}
  </TableBody>
</Table>

現在:

Input
+
Table
+
Badge

已經第一次真正形成一個使用流程。


13. 再把 Pagination 接進來

接著放入 Day 19:

<Pagination aria-label="申請紀錄分頁">
  <PaginationContent>
    <PaginationItem>
      <PaginationPrevious href="?page=1" />
    </PaginationItem>

    <PaginationItem>
      <PaginationLink
        href="?page=1"
        isActive
      >
        1
      </PaginationLink>
    </PaginationItem>

    <PaginationItem>
      <PaginationLink href="?page=2">
        2
      </PaginationLink>
    </PaginationItem>

    <PaginationItem>
      <PaginationLink href="?page=3">
        3
      </PaginationLink>
    </PaginationItem>

    <PaginationItem>
      <PaginationEllipsis />
    </PaginationItem>

    <PaginationItem>
      <PaginationLink href="?page=10">
        10
      </PaginationLink>
    </PaginationItem>

    <PaginationItem>
      <PaginationNext href="?page=2" />
    </PaginationItem>
  </PaginationContent>
</Pagination>

這裡把:

aria-label="分頁"

改成更具體的:

aria-label="申請紀錄分頁"

因為它現在已經不是孤立的 Pagination Demo。

它是:

這一份申請紀錄的 Pagination。


14. 元件組合後,Accessible Name 也要開始看 Context

這其實是今天很重要的一個發現。

昨天單獨做 Pagination 時:

aria-label="分頁"

完全合理。

但今天頁面可能未來還有:

申請紀錄
登入紀錄
操作紀錄

每一區都有自己的 Pagination。

如果全部:

分頁
分頁
分頁

就開始不清楚。

所以組成 Pattern 之後:

Component Accessibility

會開始變成:

Component
+
Context
=
Real Accessibility

這也是為什麼只在 Storybook 看單顆元件是不夠的。


15. 第一個完整 Data List

現在可以把它整理成:

<section
  aria-labelledby="application-list-title"
  className="space-y-4"
>
  <div className="space-y-1">
    <h2
      id="application-list-title"
      className="text-xl font-semibold"
    >
      申請紀錄
    </h2>

    <p className="text-sm text-muted-foreground">
      查看目前的申請與審核狀態。
    </p>
  </div>

  <Field>
    <FieldLabel htmlFor="application-search">
      搜尋申請紀錄
    </FieldLabel>

    <Input
      id="application-search"
      type="search"
      value={query}
      onChange={(event) => setQuery(event.target.value)}
      placeholder="輸入姓名、申請項目或狀態"
    />
  </Field>

  <p
    aria-live="polite"
    className="text-sm text-muted-foreground"
  >
    共 {filteredApplications.length} 筆資料
  </p>

  <Table>
    ...
  </Table>

  <Pagination aria-label="申請紀錄分頁">
    ...
  </Pagination>
</section>

這已經不是 Component Showcase。

而是一個真的可以出現在:

Admin Dashboard
校務系統
報名管理
帳號管理

裡面的 Pattern。


16. 為什麼外面使用 <section>?

現在這一整塊內容:

申請紀錄
搜尋
結果數量
Table
Pagination

本身就是頁面裡的一個主題區域。

因此可以使用:

<section>

並透過:

aria-labelledby="application-list-title"

和 Heading 建立關係:

<h2 id="application-list-title">
  申請紀錄
</h2>

最後:

<section aria-labelledby="application-list-title">

這比:

<div className="data-list">

更能描述這塊內容在 Document Structure 裡的角色。

不過同樣地:

不是看到一組 Component 就一定要加 <section>。

它要真的構成一個有 Heading、具有主題意義的內容區域才適合。


17. Search、Table、Pagination 誰負責什麼?

做到這裡,可以重新畫一次責任:

Data List Pattern
│
├── Search
│   └── Input / Field
│
├── Feedback
│   └── Result Count
│
├── Data Display
│   └── Table
│       └── Badge
│
└── Navigation
    └── Pagination

每一層負責的事情不同。

Input

負責:

接受搜尋文字

Field

負責:

Label
Description
Error relationship

Badge

負責:

Status presentation

Table

負責:

Row / Column data relationship

Pagination

負責:

Page navigation

Data List Pattern

負責:

把這些東西放在合理的使用流程裡

18. Pattern 不代表所有東西都要包成 Component

今天的 Data List 可以直接存在:

App.tsx

作為 Pattern Demo。

目前不用急著建立:

src/components/ui/data-list.tsx

因為 Data List 已經開始帶有:

Application-specific behavior

例如:

搜尋哪些欄位?
顯示哪些 Column?
Status 有哪些?
Pagination URL 怎麼產生?

這些東西不是所有系統都一樣。

因此目前更合理的是:

UI Primitives
→ CUI components

Pattern
→ Documentation / Example / Composition

等未來真的出現大量重複,再決定抽象程度。


19. Search 不應該藏進 Table

另一個很容易出現的 API 是:

<Table
  searchable
  searchPlaceholder="搜尋..."
/>

看起來很方便。

但這會讓 Table 開始知道:

Search
Filter
Query
Result Count

而 Table 原本的責任只是:

表達 Row / Column Data。

如果未來某個 Table 不需要 Search:

?

如果 Search 放在 Table 外面:

?

如果同時還有 Filter:

?

API 很快就開始膨脹。

所以 CUI 目前保持:

Table
→ Table

Search
→ Search

Data List Pattern
→ 組合兩者

這條界線會比較乾淨。


20. Pagination 也不應該藏進 Table

同樣:

<Table
  pagination
  currentPage={2}
  totalPages={10}
/>

看起來很方便。

但不是所有 Table 都需要 Pagination。

而且 Pagination 可能是:

URL Navigation
Client-side State
Server-side Pagination
Infinite Data

Table 不需要知道這些事情。

所以:

Table
+
Pagination

是:

Composition

而不是:

Table feature

21. Responsive Data List

現在整個 Pattern 變成:

Heading
Search
Result Count
Table
Pagination

Desktop 可以:

申請紀錄

搜尋申請紀錄
[ 搜尋............................ ]   共 237 筆

┌──────────────────────────────────────────┐
│ Table                                    │
└──────────────────────────────────────────┘

             Pagination

手機:

申請紀錄

搜尋申請紀錄
[ 搜尋.............. ]

共 237 筆

┌───────────────────┐
│ Table → → →       │
└───────────────────┘

< 1 [2] 3 … 10 >

Table 本身繼續使用 Day 18 的:

overflow-x-auto

Pagination 則可以減少顯示頁碼或隱藏視覺文字。

不需要因為 Responsive 就重新改變整個 Semantic Structure。


22. Search 和 Result Count 可以放在同一個 Toolbar 嗎?

可以。

例如:

[ 搜尋................ ]        共 237 筆

Desktop 很常這樣設計。

我們可以:

<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
  <Field className="max-w-sm">
    ...
  </Field>

  <p className="text-sm text-muted-foreground">
    共 {filteredApplications.length} 筆資料
  </p>
</div>

手機自動變成:

Search
↓
Result Count

Desktop:

Search                         Result Count

這種 Responsive 只改 Layout。

HTML 的閱讀順序仍然是:

Search
↓
Result Count
↓
Table
↓
Pagination

因此 Keyboard / Screen Reader order 仍然合理。


23. CSS 視覺順序不要亂改 DOM 順序

假設 Desktop 想讓:

Result Count          Search

但 DOM 是:

Search
Result Count

不要為了視覺效果大量使用:

order

把內容順序完全翻轉。

因為可能出現:

Visual Order
A → B → C

Keyboard / Screen Reader
C → A → B

使用者看到 Focus 跳來跳去就會非常困惑。

Responsive Layout 可以改排列方式。

但最好讓 DOM Order 本身就具有合理的閱讀順序。


24. 如果搜尋結果是 0 筆呢?

現在有一個新問題。

假設輸入:

財前五郎

資料裡沒有。

結果:

找到 0 筆資料

下面還留著:

姓名   申請項目   狀態   更新時間
--------------------------------

這樣可以,但使用者體驗不算很好。

我們 Day 17 已經做過:

Empty State

是不是可以直接:

<EmptyState>
  <EmptyStateTitle>
    找不到符合條件的資料
  </EmptyStateTitle>

  <EmptyStateDescription>
    請嘗試其他搜尋關鍵字。
  </EmptyStateDescription>
</EmptyState>

可以。

但是這裡出現了一個很重要的區別:

Empty
≠
No Results

25. Empty 和 No Results 不一樣

假設系統本來就沒有任何資料:

尚無申請紀錄

目前沒有任何申請資料。

這叫:

Empty

但原本有 237 筆資料,只是搜尋:

ABCDE

結果沒有符合:

找不到符合「ABCDE」的資料

請嘗試其他搜尋條件。

這叫:

No Results

它們視覺上都可能使用 Empty State Pattern。

但內容與 Action 不應該一樣。

例如:

Empty
→ 新增第一筆資料

No Results
→ 清除搜尋

這就是 Pattern 開始比 Component 更重要的地方。


26. 今天先處理 No Results

可以:

{filteredApplications.length > 0 ? (
  <>
    <Table>
      ...
    </Table>

    <Pagination>
      ...
    </Pagination>
  </>
) : (
  <EmptyState>
    <EmptyStateTitle>
      找不到符合條件的資料
    </EmptyStateTitle>

    <EmptyStateDescription>
      請嘗試其他搜尋關鍵字。
    </EmptyStateDescription>

    <EmptyStateAction>
      <Button
        variant="outline"
        onClick={() => setQuery("")}
      >
        清除搜尋
      </Button>
    </EmptyStateAction>
  </EmptyState>
)}

這時 Day 17 的 Empty State 也正式被真正的 Pattern 使用。


27. 為什麼 0 筆時 Pagination 也要消失?

如果:

找到 0 筆資料

下面還顯示:

< 1 >

其實沒有什麼 Navigation 意義。

所以:

No Results
→ Empty State
→ 不顯示 Pagination

這也是:

UI state 應該影響整個 Pattern,而不是只有 Table Body。


28. 現在 Data List 已經有兩種 State

到這裡,我們其實已經開始碰到下一個主題:

Data List
│
├── Has Data
│   ├── Table
│   └── Pagination
│
└── No Results
    └── Empty State

但真實世界還有:

Loading
Error
Empty

所以完整狀態其實會變成:

Data List
│
├── Loading
├── Data
├── Empty
├── No Results
└── Error

這部分今天先不要全部塞進來。

因為下一篇會專門處理。


29. 今天其實沒有做很多「新元件」

如果只看:

src/components/ui

今天可能幾乎沒有新增東西。

但這反而是很重要的一天。

因為 Design System 的價值不是:

有 50 顆 Component。

而是:

這 50 顆 Component 能不能合理地組成產品。

今天第一次把:

Field
Input
Badge
Table
Pagination
Empty State

真正組成:

Data List Pattern

也第一次開始看到:

Component 在 Demo 裡正確

不代表:

放進真實 Context 後就一定正確

30. Component Accessibility vs Pattern Accessibility

單獨的 Input 可以做到:

Focus ✓
Disabled ✓
Invalid ✓

但放進 Data List 後才會發現:

Search 的 Label 是什麼?

單獨 Pagination 可以做到:

aria-current ✓
Previous / Next ✓

但放進頁面後才會發現:

這是哪一份資料的 Pagination?

單獨 Empty State 可以做到:

Title ✓
Description ✓
Action ✓

但放進 Search 後才會發現:

這是 Empty 還是 No Results?

所以:

Accessible Component
+
Accessible Composition
=
Accessible Interface

只把每顆元件 individually 做到 Accessibility,還不代表整個頁面就一定 Accessible。


31. 今天的 Data List Accessibility Check

最後完整檢查一次。

Structure

Section
└── Heading
    ↓
Search
    ↓
Result Count
    ↓
Table / No Results
    ↓
Pagination

閱讀順序合理。


Search

有真正的:

<label>

或其他有效 Accessible Name。

不要只依賴:

placeholder

Search Icon 如果只是裝飾:

aria-hidden="true"

Result Count

搜尋結果更新如果需要通知:

aria-live="polite"

不要使用過強的:

role="alert"

也不要讓 Live Region 更新得過度頻繁。


Table

保留:

table
thead
tbody
tr
th
td

Column Header:

scope="col"

狀態仍然使用文字:

已通過
待審核
已退回

而不是只靠 Badge 顏色。


Pagination

使用:

nav

並提供 Context-specific label:

aria-label="申請紀錄分頁"

Current Page:

aria-current="page"

No Results

搜尋不到資料時:

說明為什麼沒有資料
+
提供合理的下一步

例如:

清除搜尋

不要顯示沒有意義的 Pagination。


32. Keyboard Test

最後再用鍵盤走一次。

預期:

Tab
 ↓
Search Input
 ↓
清除搜尋(如果有)
 ↓
Table 裡的 Interactive Controls(如果有)
 ↓
Pagination Previous
 ↓
Page Links
 ↓
Pagination Next

注意:

Table Row
Table Cell
Result Count
Badge

如果本身沒有 Interaction,就不需要加入 Tab Order。

不要為了「Accessibility」把整個頁面所有東西都變成:

tabIndex={0}

那反而會讓鍵盤使用變得非常痛苦。


33. 最後執行檢查

完成後:

npm run build
npm run lint
npm run check:contrast

確認:

git status
git diff

如果今天主要修改:

App.tsx

作為 Data List Pattern Demo,可以 Commit:

git add src/App.tsx
git commit -m "feat: add data list pattern"

如果另外拆出了 Demo data 或 Pattern example,再依實際檔案一起加入。


Day 20 完成

今天終於出現了一個很像真正產品的畫面:

申請紀錄

搜尋申請紀錄
[ 搜尋............................. ]

共 237 筆資料

┌──────────────────────────────────────────┐
│ 姓名    申請項目    狀態       更新時間 │
├──────────────────────────────────────────┤
│ 王小明  學分抵免    [已通過]   10/01    │
│ 陳小華  休學申請    [待審核]   10/01    │
│ 林小美  獎學金申請  [已退回]   09/30    │
└──────────────────────────────────────────┘

          <  1  [2]  3  4  …  10  >

有趣的是,今天幾乎沒有發明新的 Primitive。

只是把以前做過的:

Input
Field
Badge
Table
Pagination
Empty State

重新組合。

但這正是 UI Kit 開始有價值的時刻。

因為我們終於從:

「我做了一顆 Accessible Button。」

慢慢走到:

「我可以用這些元件組出一個 Accessible 的系統介面。」

而且組合之後,我們也開始發現新的 Accessibility 問題。

這些問題不是單獨看 Component 就能發現的。


Next:Day 21

現在 Data List 已經可以:

搜尋
顯示資料
顯示狀態
換頁
沒有搜尋結果

但是目前我們一直假設一件事:

資料一定成功拿得到。

現實當然沒這麼善良。

API 可能還在跑:

Loading...

資料可能真的一筆都沒有:

Empty

也可能直接:

500 Internal Server Error

所以接下來要處理完整的 Data State:

Data List
│
├── Loading
├── Success
├── Empty
├── No Results
└── Error

並加入:

Skeleton
Empty State
Alert / Error State
Retry
aria-live
aria-busy

Day 21:

資料列表不只有「有資料」:Loading、Empty、Error。


上一篇
Day 19: Pagination:上一頁下一頁也是 Navigation
系列文
30 天打造 Accessible UI Kit:從 shadcn/ui 到自己的 Design System 共 20 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言