docs: improve lighthouse score and accessibility (#3673)

This commit is contained in:
Hugo Richard
2025-03-25 16:23:35 +01:00
committed by GitHub
parent df00149598
commit 9509c37af8
17 changed files with 97 additions and 70 deletions

View File

@@ -38,7 +38,7 @@ onMounted(() => {
}
.carbon-poweredby {
@apply block text-[10px] text-center text-(--ui-text-dimmed) pt-2;
@apply block text-xs text-center text-(--ui-text-muted) pt-2;
}
&:hover {

View File

@@ -97,10 +97,11 @@ function getHeader(column: Column<Payment>, label: string) {
const isSorted = column.getIsSorted()
return h(UDropdownMenu, {
content: {
'content': {
align: 'start'
},
items: [{
'aria-label': 'Actions dropdown',
'items': [{
label: 'Asc',
type: 'checkbox',
icon: 'i-lucide-arrow-up-narrow-wide',
@@ -126,11 +127,12 @@ function getHeader(column: Column<Payment>, label: string) {
}
}]
}, () => h(UButton, {
color: 'neutral',
variant: 'ghost',
'color': 'neutral',
'variant': 'ghost',
label,
icon: isSorted ? (isSorted === 'asc' ? 'i-lucide-arrow-up-narrow-wide' : 'i-lucide-arrow-down-wide-narrow') : 'i-lucide-arrow-up-down',
class: '-mx-2.5 data-[state=open]:bg-(--ui-bg-elevated)'
'icon': isSorted ? (isSorted === 'asc' ? 'i-lucide-arrow-up-narrow-wide' : 'i-lucide-arrow-down-wide-narrow') : 'i-lucide-arrow-up-down',
'class': '-mx-2.5 data-[state=open]:bg-(--ui-bg-elevated)',
'aria-label': `Sort by ${isSorted === 'asc' ? 'descending' : 'ascending'}`
}))
}

View File

@@ -145,12 +145,12 @@ const columns: TableColumn<Payment>[] = [{
header: ({ table }) => h(UCheckbox, {
'modelValue': table.getIsSomePageRowsSelected() ? 'indeterminate' : table.getIsAllPageRowsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all'
'aria-label': 'Select all'
}),
cell: ({ row }) => h(UCheckbox, {
'modelValue': row.getIsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
'ariaLabel': 'Select row'
'aria-label': 'Select row'
}),
enableSorting: false,
enableHiding: false
@@ -242,15 +242,17 @@ const columns: TableColumn<Payment>[] = [{
}]
return h('div', { class: 'text-right' }, h(UDropdownMenu, {
content: {
'content': {
align: 'end'
},
items
items,
'aria-label': 'Actions dropdown'
}, () => h(UButton, {
icon: 'i-lucide-ellipsis-vertical',
color: 'neutral',
variant: 'ghost',
class: 'ml-auto'
'icon': 'i-lucide-ellipsis-vertical',
'color': 'neutral',
'variant': 'ghost',
'class': 'ml-auto',
'aria-label': 'Actions dropdown'
})))
}
}]
@@ -294,6 +296,7 @@ function randomize() {
variant="outline"
trailing-icon="i-lucide-chevron-down"
class="ml-auto"
aria-label="Columns select dropdown"
/>
</UDropdownMenu>
</div>

View File

@@ -17,7 +17,7 @@ const { data, status } = await useFetch<User[]>('https://jsonplaceholder.typicod
transform: (data) => {
return data?.map(user => ({
...user,
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, alt: `${user.name} avatar` }
})) || []
},
lazy: true

View File

@@ -97,15 +97,17 @@ const columns: TableColumn<Payment>[] = [{
id: 'actions',
cell: ({ row }) => {
return h('div', { class: 'text-right' }, h(UDropdownMenu, {
content: {
'content': {
align: 'end'
},
items: getRowItems(row)
'items': getRowItems(row),
'aria-label': 'Actions dropdown'
}, () => h(UButton, {
icon: 'i-lucide-ellipsis-vertical',
color: 'neutral',
variant: 'ghost',
class: 'ml-auto'
'icon': 'i-lucide-ellipsis-vertical',
'color': 'neutral',
'variant': 'ghost',
'class': 'ml-auto',
'aria-label': 'Actions dropdown'
})))
}
}]

View File

@@ -48,14 +48,15 @@ const data = ref<Payment[]>([{
const columns: TableColumn<Payment>[] = [{
id: 'expand',
cell: ({ row }) => h(UButton, {
color: 'neutral',
variant: 'ghost',
icon: 'i-lucide-chevron-down',
square: true,
ui: {
'color': 'neutral',
'variant': 'ghost',
'icon': 'i-lucide-chevron-down',
'square': true,
'aria-label': 'Expand',
'ui': {
leadingIcon: ['transition-transform', row.getIsExpanded() ? 'duration-200 rotate-180' : '']
},
onClick: () => row.toggleExpanded()
'onClick': () => row.toggleExpanded()
})
}, {
accessorKey: 'id',

View File

@@ -50,12 +50,12 @@ const columns: TableColumn<Payment>[] = [{
header: ({ table }) => h(UCheckbox, {
'modelValue': table.getIsSomePageRowsSelected() ? 'indeterminate' : table.getIsAllPageRowsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all'
'aria-label': 'Select all'
}),
cell: ({ row }) => h(UCheckbox, {
'modelValue': row.getIsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
'ariaLabel': 'Select row'
'aria-label': 'Select row'
})
}, {
accessorKey: 'date',

View File

@@ -50,12 +50,12 @@ const columns: TableColumn<Payment>[] = [{
header: ({ table }) => h(UCheckbox, {
'modelValue': table.getIsSomePageRowsSelected() ? 'indeterminate' : table.getIsAllPageRowsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all'
'aria-label': 'Select all'
}),
cell: ({ row }) => h(UCheckbox, {
'modelValue': row.getIsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
'ariaLabel': 'Select row'
'aria-label': 'Select row'
})
}, {
accessorKey: 'date',

View File

@@ -95,7 +95,7 @@ function getDropdownActions(user: User): DropdownMenuItem[][] {
<UTable :data="data" :columns="columns" class="flex-1">
<template #name-cell="{ row }">
<div class="flex items-center gap-3">
<UAvatar :src="`https://i.pravatar.cc/120?img=${row.original.id}`" size="lg" />
<UAvatar :src="`https://i.pravatar.cc/120?img=${row.original.id}`" size="lg" :alt="`${row.original.name} avatar`" />
<div>
<p class="font-medium text-(--ui-text-highlighted)">
{{ row.original.name }}
@@ -108,7 +108,7 @@ function getDropdownActions(user: User): DropdownMenuItem[][] {
</template>
<template #action-cell="{ row }">
<UDropdownMenu :items="getDropdownActions(row.original)">
<UButton icon="i-lucide-ellipsis-vertical" color="neutral" variant="ghost" />
<UButton icon="i-lucide-ellipsis-vertical" color="neutral" variant="ghost" aria-label="Actions" />
</UDropdownMenu>
</template>
</UTable>

View File

@@ -136,7 +136,7 @@ const communityLinks = computed(() => [{
v-bind="link"
>
<template v-if="link.avatar" #leading>
<UAvatar v-bind="link.avatar" size="2xs" />
<UAvatar v-bind="link.avatar" size="2xs" :alt="`${link.label} avatar`" />
</template>
</UButton>
</template>

View File

@@ -169,6 +169,7 @@ onMounted(() => {
:loading="index >= 4 ? 'lazy' : 'eager'"
width="640"
height="360"
:alt="`${component.name} preview`"
/>
</div>
</UPageCard>

View File

@@ -106,6 +106,9 @@ useIntersectionObserver(contributorsRef, ([entry]) => {
:light="`${component.path.replace('/components/', '/components/light/')}.png`"
:dark="`${component.path.replace('/components/', '/components/dark/')}.png`"
:alt="`${component.title} preview`"
width="290"
height="163"
format="webp"
class="hover:scale-105 lg:hover:scale-110 transition-transform aspect-video w-full border-x lg:border-x-0 lg:border-y border-(--ui-border) 2xl:border-y-0"
loading="lazy"
/>
@@ -132,6 +135,9 @@ useIntersectionObserver(contributorsRef, ([entry]) => {
:light="`${component.path.replace('/components/', '/components/light/')}.png`"
:dark="`${component.path.replace('/components/', '/components/dark/')}.png`"
:alt="`${component.title} preview`"
width="290"
height="163"
format="webp"
class="hover:scale-105 lg:hover:scale-110 transition-transform aspect-video w-full border-x lg:border-x-0 lg:border-y border-(--ui-border) 2xl:border-y-0"
loading="lazy"
/>
@@ -155,7 +161,9 @@ useIntersectionObserver(contributorsRef, ([entry]) => {
:in-view-options="{ once: true }"
class="flex items-start gap-x-3 relative group"
>
<NuxtLink v-if="feature.to" :to="feature.to" class="absolute inset-0 z-10" />
<NuxtLink v-if="feature.to" :to="feature.to" class="absolute inset-0 z-10">
<span class="sr-only">Go to {{ feature.title }}</span>
</NuxtLink>
<div class="relative p-3">
<svg class="absolute inset-0" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -221,26 +229,32 @@ useIntersectionObserver(contributorsRef, ([entry]) => {
class="border-b border-(--ui-border)"
>
<template #features>
<NuxtLink to="https://npm.chart.dev/@nuxt/ui" target="_blank" class="min-w-0">
<p class="text-4xl font-semibold text-(--ui-text-highlighted) truncate">
{{ format(module?.stats?.downloads ?? 0) }}+
</p>
<p class="text-(--ui-text-muted) text-sm truncate">monthly downloads</p>
</NuxtLink>
<li>
<NuxtLink to="https://npm.chart.dev/@nuxt/ui" target="_blank" class="min-w-0">
<p class="text-4xl font-semibold text-(--ui-text-highlighted) truncate">
{{ format(module?.stats?.downloads ?? 0) }}+
</p>
<p class="text-(--ui-text-muted) text-sm truncate">monthly downloads</p>
</NuxtLink>
</li>
<NuxtLink to="https://github.com/nuxt/ui" target="_blank" class="min-w-0">
<p class="text-4xl font-semibold text-(--ui-text-highlighted) truncate">
{{ format(module?.stats?.stars ?? 0) }}+
</p>
<p class="text-(--ui-text-muted) text-sm truncate">GitHub stars</p>
</NuxtLink>
<li>
<NuxtLink to="https://github.com/nuxt/ui" target="_blank" class="min-w-0">
<p class="text-4xl font-semibold text-(--ui-text-highlighted) truncate">
{{ format(module?.stats?.stars ?? 0) }}+
</p>
<p class="text-(--ui-text-muted) text-sm truncate">GitHub stars</p>
</NuxtLink>
</li>
<NuxtLink to="https://github.com/nuxt/ui/graphs/contributors" target="_blank" class="min-w-0">
<p class="text-4xl font-semibold text-(--ui-text-highlighted) truncate">
175+
</p>
<p class="text-(--ui-text-muted) text-sm truncate">Contributors</p>
</NuxtLink>
<li>
<NuxtLink to="https://github.com/nuxt/ui/graphs/contributors" target="_blank" class="min-w-0">
<p class="text-4xl font-semibold text-(--ui-text-highlighted) truncate">
175+
</p>
<p class="text-(--ui-text-muted) text-sm truncate">Contributors</p>
</NuxtLink>
</li>
</template>
<div ref="contributorsRef" class="p-4 sm:px-6 md:px-8 lg:px-12 xl:px-14 overflow-hidden flex relative">

View File

@@ -56,6 +56,7 @@ useSeoMeta({
v-if="template.thumbnail"
v-bind="template.thumbnail"
class="w-full h-auto border lg:border-y lg:border-x-0 border-(--ui-border) rounded-(--ui-radius) lg:rounded-none"
:alt="`Template ${index} thumbnail`"
width="656"
height="369"
loading="lazy"

View File

@@ -6,7 +6,7 @@ navigation.icon: i-lucide-house
<iframe width="100%" height="100%" src="https://www.youtube-nocookie.com/embed/_eQxomah-nA?si=pDSzchUBDKb2NQu7" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen style="aspect-ratio: 16/9;" class="rounded-[calc(var(--ui-radius)*1.5)]"></iframe>
### Reka UI
## Reka UI
We've transitioned from [Headless UI](https://headlessui.com/) to [Reka UI](https://reka-ui.com/) as our core component foundation. This shift brings several key advantages:
@@ -17,7 +17,7 @@ We've transitioned from [Headless UI](https://headlessui.com/) to [Reka UI](http
This transition empowers Nuxt UI to become a more comprehensive and flexible UI library, offering developers greater power and customization options.
### Tailwind CSS v4
## Tailwind CSS v4
Nuxt UI integrates the latest Tailwind CSS v4, bringing significant improvements:
@@ -30,7 +30,7 @@ Nuxt UI integrates the latest Tailwind CSS v4, bringing significant improvements
Learn about all the breaking changes in Tailwind CSS v4.
::
### Tailwind Variants
## Tailwind Variants
We've adopted [Tailwind Variants](https://www.tailwind-variants.org/) to manage our design system, offering:
@@ -40,7 +40,7 @@ We've adopted [Tailwind Variants](https://www.tailwind-variants.org/) to manage
This integration unifies the styling of components, ensuring consistency and code maintainability.
### TypeScript Integration
## TypeScript Integration
Nuxt UI offers significantly improved TypeScript integration, providing a superior developer experience:
@@ -60,7 +60,7 @@ Nuxt UI offers significantly improved TypeScript integration, providing a superi
Check out an example of the Accordion component with auto-completion for props and slots.
::
### Vue compatibility
## Vue compatibility
You can now use Nuxt UI in any Vue project without Nuxt by adding the Vite and Vue plugins to your configuration. This provides:
@@ -72,7 +72,7 @@ You can now use Nuxt UI in any Vue project without Nuxt by adding the Vite and V
Learn how to install and configure Nuxt UI in a Vue project in the **Vue installation guide**.
::
### Nuxt DevTools Integration
## Nuxt DevTools Integration
You can play with Nuxt UI components as well as your app components directly from Nuxt Devtools with the [compodium](https://github.com/romhml/compodium) module, providing a powerful development experience:

View File

@@ -23,7 +23,7 @@ class: '!p-0'
---
::
::callout{icon="i-simple-icons-github" to="https://github.com/nuxt/ui/tree/v3/docs/app/components/content/examples/table/TableExample.vue"}
::callout{icon="i-simple-icons-github" to="https://github.com/nuxt/ui/tree/v3/docs/app/components/content/examples/table/TableExample.vue" aria-label="View source code"}
This example demonstrates the most common use case of the `Table` component. Check out the source code on GitHub.
::
@@ -85,7 +85,7 @@ Use the `columns` prop as an array of [ColumnDef](https://tanstack.com/table/lat
In order to render components or other HTML elements, you will need to use the Vue [`h` function](https://vuejs.org/api/render-function.html#h) inside the `header` and `cell` props. This is different from other components that use slots but allows for more flexibility.
::tip{to="#with-slots"}
::tip{to="#with-slots" aria-label="Table columns with slots"}
You can also use slots to customize the header and data cells of the table.
::

View File

@@ -225,6 +225,7 @@ export default defineNuxtConfig({
},
image: {
format: ['webp', 'jpeg', 'jpg', 'png', 'svg'],
provider: 'ipx'
},

View File

@@ -148,12 +148,12 @@ const columns: TableColumn<Payment>[] = [{
header: ({ table }) => h(UCheckbox, {
'modelValue': table.getIsSomePageRowsSelected() ? 'indeterminate' : table.getIsAllPageRowsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all'
'aria-label': 'Select all'
}),
cell: ({ row }) => h(UCheckbox, {
'modelValue': row.getIsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
'ariaLabel': 'Select row'
'aria-label': 'Select row'
}),
enableSorting: false,
enableHiding: false
@@ -251,15 +251,17 @@ const columns: TableColumn<Payment>[] = [{
}]
return h('div', { class: 'text-right' }, h(UDropdownMenu, {
content: {
'content': {
align: 'end'
},
items
items,
'aria-label': 'Actions dropdown'
}, () => h(UButton, {
icon: 'i-lucide-ellipsis-vertical',
color: 'neutral',
variant: 'ghost',
class: 'ms-auto'
'icon': 'i-lucide-ellipsis-vertical',
'color': 'neutral',
'variant': 'ghost',
'class': 'ms-auto',
'aria-label': 'Actions dropdown'
})))
}
}]