I have this component that is producing a pill check group. I am achieving it by hiding the checkboxes themselves but showing the labels as well-formatted boxes with checked and unchecked states.
Here is the component InputCheckGroupPill.vue
<div class="grid grid-cols-3 sm:grid-cols-4">
<div v-for="(option, index) in options" :key="option[valueAttribute]">
<div class="text-sm ">
<input :id="'check-option-' + option[valueAttribute] + index"
:aria-describedby="'check-label-' + option[valueAttribute] + index"
:aria-activedescendant="'check-label-' + option[valueAttribute] + index"
:aria-labelledby="'check-label-' + option[valueAttribute] + index"
:value="option[valueAttribute]"
v-model="selectedOptions"
class="hidden"
type="checkbox"/>
<label :id="'check-label-' + option[valueAttribute] + index"
:for="'check-option-' + option[valueAttribute] + index"
:class="[ selectedOptions.includes(option[valueAttribute]) ? 'bg-primary-600 text-white hover:bg-primary-500 border border-r-1 border-gray-100' : 'ring-1 ring-inset ring-gray-300 bg-white text-gray-900 hover:bg-gray-50', index === 0 ? 'rounded-l-md': '', index === options.length-1 ? 'rounded-r-md': '', 'flex items-center justify-center py-3 px-3 text-sm font-semibold uppercase sm:flex-1 focus-visible:ring-2 focus-visible:ring-primary-600 focus-visible:ring-offset-2 cursor-pointer']">
{{option[labelAttribute]}}
</label>
</div>
</div>
</div>
Here is the produced visible component
The problem is when I use the tab key to navigate, the checkboxes are skipped like they are not there.
I have exhausted all the possible ARIA descriptors from MDN to no avail, how can you help?
Note: The Component works just fine when am using the mouse, but I wish for it to work with the keyboard.
I am using VueJs, and Tailwind CSS for styling
>Solution :
Instead of using display: none to hide the checkbox, use sr-only to hide the element visually, but have it still accessible for screen readers and the like. This also means that we can use peer styling when it is focused, like:
<input … class="… peer"/>
<label … class="… peer-focus-visible:ring-2 peer-focus-visible:ring-primary-600 peer-focus-visible:ring-offset-2"/>
Vue.createApp({
data() {
return {
selectedOptions: [],
options: {
foo: {
valueAttribute: 'foo',
labelAttribute: 'Foo',
},
bar: {
valueAttribute: 'bar',
labelAttribute: 'Bar',
},
},
};
},
}).mount('#app');
tailwind.config = {
theme: {
extend: {
colors: {
primary: tailwind.colors.blue,
}
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.3.4/vue.global.prod.min.js" integrity="sha512-39BSQXI5q1XlvVhLfFRidKG8KM6Tr6VS/XSnNo6N/A0ZXexHCeoUI/s+ulujQy3UREjoLNrMnFat8VI0mMugWA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.tailwindcss.com/3.3.2"></script>
<div id="app">
<div class="grid grid-cols-3 sm:grid-cols-4">
<div v-for="(option, index) in options" :key="option.valueAttribute">
<div class="text-sm ">
<input
:id="'check-option-' + option.valueAttribute + index"
:aria-describedby="'check-label-' + option.valueAttribute + index"
:aria-activedescendant="'check-label-' + option.valueAttribute + index"
:aria-labelledby="'check-label-' + option.valueAttribute + index"
:value="option.valueAttribute"
v-model="selectedOptions"
class="sr-only peer"
type="checkbox"
/>
<label
:id="'check-label-' + option.valueAttribute + index"
:for="'check-option-' + option.valueAttribute + index"
:class="[
selectedOptions.includes(option.valueAttribute)
? 'bg-primary-600 text-white hover:bg-primary-500 border border-r-1 border-gray-100'
: 'ring-1 ring-inset ring-gray-300 bg-white text-gray-900 hover:bg-gray-50',
index === 0
? 'rounded-l-md'
: '',
index === options.length-1 ?
'rounded-r-md'
: '',
'flex items-center justify-center py-3 px-3 text-sm font-semibold uppercase sm:flex-1 peer-focus-visible:ring-2 peer-focus-visible:ring-primary-600 peer-focus-visible:ring-offset-2 cursor-pointer'
]"
>
{{ option.labelAttribute }}
</label>
</div>
</div>
</div>
</div>
