Accessibility Without the Extra Work
Building accessible front-end applications typically means juggling WAI-ARIA specifications, Section 508, WCAG 2.0 guidelines, and other standards — all while managing project deadlines. Chakra UI was built to take much of that burden off developers. Created by Segun Adebayo and ported to Vue by Jonathan Bakebwa, this design system and UI framework ships with accessibility baked into every component.
Chakra UI components come ready with keyboard navigation, focus management, appropriate aria-* attributes, and focus trapping/restoration for modal dialogs. The framework follows WAI-ARIA guidelines specifications and includes an accessibility.md report for each component — for example, see the report for the CAccordion component on GitHub.
Beyond accessibility, Chakra UI emphasizes:
- Style props: Components can be styled or overridden via props, reducing reliance on stylesheets or inline styles. This is powered by Styled Systems under the hood.
- Composition: Components are broken down into smaller pieces with minimal props so you can compose them together. For instance,
CBoxandCPseudoBoxcan be used to create new components. - Theming: A default theme object provides colors, fonts, type scales, breakpoints, and border-radius values that you can extend or replace.
- Dark mode: Most components are dark-mode compatible out of the box.
Note that while Chakra UI relies on CSS-in-JS under the hood, you don't need to know it to use the library.
Setting Up Chakra UI with Nuxt
For this walkthrough, we'll build "Chakra-ui explorer" — a single-page app to search Chakra UI components. Start by creating a new Nuxt application:
$ npx create-nuxt-app chakra-ui-explorer
Or with yarn:
$ yarn create nuxt-app chakra-ui-explorer
Follow the prompt to finish setup. Next, install Chakra UI with the official Nuxt modules for Chakra UI and Emotion (its styling engine). The @nuxtjs/emotion module ensures component styles are generated and injected during the server build:
npm i @chakra-ui/nuxt @nuxtjs/emotion
Register both modules in the modules array of nuxt.config.js:
// nuxt.config.js
modules: ['@chakra-ui/nuxt', '@nuxtjs/emotion'],
To complete setup, update the default layout component in layouts/default.vue. Add CThemeProvider, CColorModeProvider, and CReset in the template:
<!-- layouts/default.vue -->
<template>
<div class="container">
<c-theme-provider>
<c-color-mode-provider>
<c-box as="section">
<c-reset />
<nuxt />
</c-box>
</c-color-mode-provider>
</c-theme-provider>
</div>
</template>
Then import and register them in the script section:
<script>
import { CThemeProvider, CColorModeProvider, CReset, CBox } from '@chakra-ui/vue'
export default {
name: 'DefaultLayout',
components: {
CThemeProvider,
CColorModeProvider,
CReset,
CBox
}
}
</script>
Your full default.vue should look like this:
<template>
<div class="container">
<c-theme-provider>
<c-color-mode-provider>
<c-box as="section">
<c-reset />
<nuxt />
</c-box>
</c-color-mode-provider>
</c-theme-provider>
</div>
</template>
<script>
import { CThemeProvider, CColorModeProvider, CReset, CBox } from '@chakra-ui/vue'
export default {
name: 'DefaultLayout',
components: {
CThemeProvider,
CColorModeProvider,
CReset,
CBox
}
}
</script>
CThemeProvider makes the theme available throughout the app, CColorModeProvider manages light/dark color modes, and CReset removes browser default styles. It's recommended to use CReset to ensure all Chakra UI components work correctly. Note how both <c-reset /> and <nuxt /> are wrapped in a c-box component.
Extending the Default Theme
The Chakra UI Nuxt module exposes a chakra object with an extendTheme property. The object passed to extendTheme is recursively merged into the default theme. This allows you to add values like a brand color palette using keys from 50 to 900.
For our demo, we'll add the color lime as a brand color. Create a chakra folder (or any name you prefer) at the project root, then add theme.js inside it:
// ./chakra/theme.js
const customTheme = {
colors: {
brand: {
50: '#f6fcee',
100: '#e2f4c8',
200: '#cbec9e',
300: '#b2e26e',
400: '#94d736',
500: '#75c800',
600: '#68b300',
700: '#599900',
800: '#477900',
900: '#294700'
}
}
}
module.exports = customTheme
Now import this custom theme in nuxt.config.js:
import customTheme from './chakra/theme'
Then pass it through the chakra key:
chakra: {
extendTheme: customTheme
},
Your complete nuxt.config.js should look like this:
// nuxt.config.js
import customTheme from './chakra/theme'
export default {
mode: 'spa',
/*
* Headers of the page
*/
head: {
title: process.env.npm_package_name || '',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{
hid: 'description',
name: 'description',
content: process.env.npm_package_description || ''
}
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
},
/*
* Customize the progress-bar color
*/
loading: { color: '#fff' },
/*
* Global CSS
*/
css: [],
/*
* Plugins to load before mounting the App
*/
plugins: [],
/*
* Nuxt.js dev-modules
*/
buildModules: [
// Doc: https://github.com/nuxt-community/eslint-module
'@nuxtjs/eslint-module'
],
/*
* Nuxt.js modules
*/
modules: [
'@chakra-ui/nuxt',
'@nuxtjs/emotion'
],
chakra: {
extendTheme: customTheme
},
/*
* Build configuration
*/
build: {
/*
* You can extend webpack config here
*/
extend (config, ctx) {}
}
}
Run npm run dev and your homepage should appear:
Theme values like those in theme.colors become automatically available across components via props such as color, borderColor, backgroundColor, fill, stroke, and style.
Building the Navigation Bar
Now for the main navigation. Create components/NavBar.vue containing the brand name Chakra-ui explorer, links to Documentation and Repo, and a button for toggling color mode:
<template>
<c-box
as="nav"
h="60px"
px="4"
d="flex"
align-items="center"
shadow="sm"
>
<c-link
as="nuxt-link"
to="/"
color="brand.700"
font-weight="bold"
:_hover="{ color: 'brand.900' }"
>
Chakra-ui Explorer
</c-link>
<c-box
as="ul"
color="gray.500"
d="flex"
align-items="center"
list-style-type="none"
ml="auto"
>
<c-box as="li" mr="8">
<c-link
color="gray.500"
:_hover="{ color: 'brand.400' }"
is-external
href="https://vue.chakra-ui.com"
>
Documentation
</c-link>
</c-box>
<c-box as="li" mr="8">
<c-link
color="gray.500"
:_hover="{ color: 'brand.400' }"
is-external
href="https://github.com/chakra-ui/chakra-ui-vue"
>
Repo
</c-link>
</c-box>
<c-box as="li">
<c-icon-button
variant="ghost"
variant-color="gray[900]"
aria-label="Switch to dark mode"
icon="moon"
/>
</c-box>
</c-box>
</c-box>
</template>
<script>
import { CBox, CLink, CIconButton } from '@chakra-ui/vue'
export default {
name: 'NavBar',
components: {
CBox,
CLink,
CIconButton
}
}
</script>
Import NavBar into the default layout and add it to the template:
<template>
<div class="container">
<c-theme-provider>
<c-color-mode-provider>
<c-box as="section">
<c-reset />
<nav-bar />
<nuxt />
</c-box>
</c-color-mode-provider>
</c-theme-provider>
</div>
</template>
<script>
import { CThemeProvider, CColorModeProvider, CReset, CBox } from '@chakra-ui/vue'
import NavBar from '@/components/NavBar'
export default {
name: 'DefaultLayout',
components: {
CThemeProvider,
CColorModeProvider,
CReset,
CBox,
NavBar
}
}
</script>
Run the app and use the Tab key — Chakra UI handles focus management automatically, making the nav accessible without any extra work from you.
The as Prop for Semantic Markup
Notice the as prop used throughout NavBar.vue. It lets you render any HTML tag or component as the base element while keeping the component's styles and props. For example:
<c-box as="li">
<c-icon-button
variant="ghost"
variant-color="gray[900]"
aria-label="Switch to dark mode"
icon="moon"
/>
</c-box>
This tells Chakra UI to render an <li> element and place a button component inside it. Similarly, the pattern is used here:
<c-link
as="nuxt-link"
to="/"
color="brand.700"
font-weight="bold"
:_hover="{ color : 'brand.900' }">
ChakraMart
</c-link>
Here, Chakra UI renders Nuxt's
The as prop enables you to use semantic markup — so instead of a generic div for main content, you could render a main element, which properly communicates the page structure to screen readers.
When examining NavBar.vue, note how the brand color from chakra/theme.js is used just like any of Chakra UI's default colors. Also, the moon icon on our CIconButton comes from Chakra UI's built-in default icons. Check the documentation for all props exposed by Chakra UI components.
Dark Mode In Practice
Chakra UI ships with color mode support out of the box, and the Nuxt.js explorer we are building plugs into it through CColorModeProvider. Using Vue’s provide/inject mechanism, this provider exposes two functions: $chakraColorMode, which returns the current mode, and $toggleColorMode, which flips between light and dark.
To wire this into the navigation, we inject both functions into NavBar.vue:
<script>
<script>
import { CBox, CLink, CIconButton } from '@chakra-ui/vue'
export default {
name: 'NavBar',
inject: ['$chakraColorMode', '$toggleColorMode'],
components: {
CBox,
CLink,
CIconButton
},
}
</script>
Since we only need to display the active mode, a computed property keeps the template clean:
...
computed: {
colorMode () {
return this.$chakraColorMode()
}
}
With those in place, the CIconButton can be updated so its icon and label react to the current mode. The button’s visual state is driven by the computed colorMode, while the aria-label switches between meaningful descriptions for screen readers:
<c-icon-button
variant="ghost"
variant-color="gray[900]"
aria-label="Switch to dark mode"
:icon="colorMode == 'light' ? 'moon' : 'sun'"
/>
<c-icon-button
variant="ghost"
variant-color="gray[900]"
:aria-label="`Switch to ${colorMode == 'light' ? 'dark : 'light'} mode`"
:icon="colorMode == 'light' ? 'moon' : 'sun'"
/>
The click handler is straightforward — it calls the injected $toggleColorMode function:
<c-icon-button
variant="ghost"
variant-color="gray[900]"
:aria-label="`Switch to ${colorMode == 'light' ? 'dark' : 'light'} mode`"
:icon="colorMode == 'light' ? 'moon' : 'sun'"
@click="$toggleColorMode"
/>
To verify the setup, we can interpolate the current mode in the template next to the button:
<c-box as="li">
<c-icon-button
variant="ghost"
variant-color="gray[900]"
:aria-label="`Switch to ${colorMode == 'light' ? 'dark' : 'light'} mode`"
:icon="colorMode == 'light' ? 'moon' : 'sun'"
@click="$toggleColorMode"
/>
Current mode: {{ colorMode }}
</c-box>
With the provider working, the layout in default.vue can now adapt its styling. We destructure colorMode from the provider’s slot props and pass it as a key to a mainStyle object. This keeps the logic centralized rather than scattering conditional classes through the template:
<template>
<div class="container">
<c-theme-provider>
<c-color-mode-provider #default="{ colorMode }">
<c-box
v-bind="mainStyles[colorMode]"
w="100vw"
h="100vh"
as="section"
>
<c-reset />
<nav-bar />
<nuxt />
</c-box>
</c-color-mode-provider>
</c-theme-provider>
</div>
</template>
<script>
import { CThemeProvider, CColorModeProvider, CReset, CBox } from '@chakra-ui/vue'
import NavBar from '@/components/NavBar'
export default {
name: 'DefaultLayout',
components: {
CThemeProvider,
CColorModeProvider,
CReset,
CBox,
NavBar
},
data () {
return {
mainStyles: {
dark: {
bg: 'gray.900',
color: 'whiteAlpha.900'
},
light: {
bg: 'whiteAlpha.900',
color: 'gray.900'
}
}
}
}
}
</script>
At this point, the Chakra-ui explorer supports dark mode across the shell, including the navigation bar and its toggle control.
Building The Component Directory
The main page, index.vue, is where the real feature work begins. We start with a CBox container to hold the search and list:
<c-box
as="main"
d="flex"
direction="column"
align-items="center"
p="10"
>
</c-box>
Inside that container, we add a CInput element for searching. Chakra UI handles the focus outline and dark mode styling automatically — no extra props are needed for either:
<template>
<c-box
as="main"
d="flex"
align-items="center"
direction="column"
w="auto"
p="16"
>
<c-input placeholder="Search components..." size="lg" mb="5" is-full-width />
</c-box>
</template>
<script>
import { CBox, CInput } from '@chakra-ui/vue'
export default {
components: {
CBox,
CInput
}
}
</script>
For the list itself, we want to display every available Chakra UI component and link out to its official documentation. That data lives in a new directory called data at the project root, with an index.js file exporting an array of component names:
// ./data/index.js
export const components = [
{
name: 'Accordion'
},
{
name: 'Alert'
},
{
name: 'AlertDialog'
},
{
name: 'AspectRatioBox'
},
{
name: 'AspectRatioBox'
},
{
name: 'Avatar'
},
{
name: 'Badge'
},
{
name: 'Box'
},
{
name: 'Breadcrumb'
},
{
name: 'Button'
},
{
name: 'Checkbox'
},
{
name: 'CircularProgress'
},
{
name: 'CloseButton'
},
{
name: 'Code'
},
{
name: 'Collapse'
},
{
name: 'ControlBox'
},
{
name: 'Divider'
},
{
name: 'Drawer'
},
{
name: 'Editable'
},
{
name: 'Flex'
},
{
name: 'Grid'
},
{
name: 'Heading'
},
{
name: 'Icon'
},
{
name: 'IconButton'
},
{
name: 'IconButton'
},
{
name: 'Input'
},
{
name: 'Link'
},
{
name: 'List'
},
{
name: 'Menu'
},
{
name: 'Modal'
},
{
name: 'NumberInput'
},
{
name: 'Popover'
},
{
name: 'Progress'
},
{
name: 'PseudoBox'
},
{
name: 'Radio'
},
{
name: 'SimpleGrid'
},
{
name: 'Select'
},
{
name: 'Slider'
},
{
name: 'Spinner'
},
{
name: 'Stat'
},
{
name: 'Stack'
},
{
name: 'Switch'
},
{
name: 'Tabs'
},
{
name: 'Tag'
},
{
name: 'Text'
},
{
name: 'Textarea'
},
{
name: 'Toast'
},
{
name: 'Tooltip'
}
]
The page imports that array and iterates over it, rendering each item as a link. The search box filters the list as the user types. Focus management on the links is provided by default in Chakra UI, so keyboard navigation works without additional code:
// pages/index.vue
<template>
<c-box
as="main"
d="flex"
align-items="space-between"
flex-direction="column"
w="auto"
p="16"
>
<c-input v-model="search" placeholder="Search components..." size="lg" mb="10" is-full-width />
<c-grid template-columns="repeat(4, 1fr)" gap="3" p="5">
<c-box v-for="(chakraComponent, index) of filteredComponents" :key="index" h="10">
{{ chakraComponent.name }}
<c-badge>
<c-link
is-external
:href="lowercase(`https://vue.chakra-ui.com/${chakraComponent.name}`)"
>
<c-icon name="info" size="18px" />
</c-link>
</c-badge>
</c-box>
</c-grid>
</c-box>
</template>
<script>
import { CBox, CInput, CGrid, CLink, CBadge, CIcon } from '@chakra-ui/vue'
import { components as chakraComponents } from '../data'
export default {
components: {
CBox,
CInput,
CGrid,
CBadge,
CIcon,
CLink
},
data () {
return {
search: ''
}
},
computed: {
filteredComponents () {
return chakraComponents.filter((component) => {
return this.lowercase(component.name).includes(this.lowercase(this.search))
})
}
},
methods: {
lowercase (value) {
return value.toLowerCase()
}
}
}
</script>
Measuring Accessibility
To verify the work, we run Google Lighthouse’s accessibility audit on the running application. The audit’s criteria are based on the Axe user impact assessment. Running the test on the live Chakra-ui explorer returns a score of 85 — a solid starting point for a component explorer that prioritizes inclusive defaults.



