FAQ Components
Beautiful, production-ready FAQ and accordion components built with TailwindCSS. Preview with Alpine.js, and copy matching Vue or React code for your stack.
1) Minimal FAQ
Simple, clean accordion with rotating chevron.
UI snippets for Laravel/Tailwind with matching React/Vue code.
Yes, follow each component’s license note (MIT for these demos).
Tailwind utilities make it easy—add
dark:
variants to your classes.<div x-data="{ open: null }" class="max-w-2xl mx-auto space-y-2">
<div class="border border-gray-200 rounded-lg">
<button @click="open = open === 1 ? null : 1" class="w-full px-5 py-4 text-left font-medium flex justify-between items-center hover:bg-gray-50">
<span>What is Templateight?</span>
<svg class="w-5 h-5 transform transition-transform" :class="{'rotate-180': open === 1}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /></svg>
</button>
<div x-show="open === 1" x-collapse>
<div class="px-5 pb-4 border-top border-gray-200 text-gray-700">UI snippets for Laravel/Tailwind with matching React/Vue code.</div>
</div>
</div>
</div>
<template>
<div class="max-w-2xl mx-auto space-y-2">
<div v-for="item in items" :key="item.id" class="border border-gray-200 rounded-lg">
<button @click="open = open === item.id ? null : item.id" class="w-full px-5 py-4 text-left font-medium flex justify-between items-center hover:bg-gray-50">
<span>{{ item.q }}</span>
<svg :class="['w-5 h-5 transform transition-transform', open===item.id ? 'rotate-180' : '']" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div v-show="open===item.id" class="px-5 pb-4 border-t border-gray-200 text-gray-700">{{ item.a }}</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const open = ref(null)
const items = [
{id:1,q:'What is Templateight?',a:'UI snippets for Laravel/Tailwind with matching React/Vue code.'},
{id:2,q:'Can I use them commercially?',a:'Yes, MIT for these demos.'},
{id:3,q:'Does it support dark mode?',a:'Use Tailwind dark: variants.'},
]
</script>
import React, { useState } from 'react'
export default function MinimalFaq(){
const [open, setOpen] = useState(null)
const items = [
{id:1,q:'What is Templateight?',a:'UI snippets for Laravel/Tailwind with matching React/Vue code.'},
{id:2,q:'Can I use them commercially?',a:'Yes, MIT for these demos.'},
{id:3,q:'Does it support dark mode?',a:'Use Tailwind dark: variants.'},
]
return (<div className="max-w-2xl mx-auto space-y-2">
{items.map(it => (
<div key={it.id} className="border border-gray-200 rounded-lg">
<button onClick={()=>setOpen(open===it.id?null:it.id)} className="w-full px-5 py-4 text-left font-medium flex justify-between items-center hover:bg-gray-50">
<span>{it.q}</span>
<svg className={`w-5 h-5 transform transition-transform ${open===it.id?'rotate-180':''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"/></svg>
</button>
{open===it.id && (<div className="px-5 pb-4 border-t border-gray-200 text-gray-700">{it.a}</div>)}
</div>
))}
</div>)
}
2) Plus / Minus (Heroicons)
Uses Heroicons plus/minus. Smooth state swap.
<div x-data="{ open: null }" class="max-w-2xl mx-auto space-y-3">
<div class="border border-gray-300 rounded-lg">
<button @click="open = open === 1 ? null : 1" class="w-full px-6 py-4 flex items-center justify-between">
<span class="font-medium">Do you support React/Vue?</span>
<span class="text-gray-500">
<svg x-show="open !== 1" class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
<svg x-show="open === 1" class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14"/></svg>
</span>
</button>
<div x-show="open === 1" x-collapse>
<div class="px-6 pb-4 border-t border-gray-200 text-gray-700">Yes, both with matching code samples.</div>
</div>
</div>
</div>
<template>
<div class="max-w-2xl mx-auto space-y-3">
<div v-for="item in items" :key="item.id" class="border border-gray-300 rounded-lg">
<button @click="open = open===item.id ? null : item.id" class="w-full px-6 py-4 flex items-center justify-between">
<span class="font-medium">{{ item.q }}</span>
<span class="text-gray-500">
<PlusIcon v-show="open!==item.id" class="w-5 h-5" />
<MinusIcon v-show="open===item.id" class="w-5 h-5" />
</span>
</button>
<div v-show="open===item.id" class="px-6 pb-4 border-t border-gray-200 text-gray-700">{{ item.a }}</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { PlusIcon, MinusIcon } from '@heroicons/vue/24/outline'
const open = ref(null)
const items = [
{id:1,q:'Do you support React/Vue?',a:'Yes, both with matching code samples.'},
{id:2,q:'How to install?',a:'Install Tailwind and copy the snippets.'},
{id:3,q:'Is there support?',a:'Open GitHub issues or check docs.'}
]
</script>
import React, { useState } from 'react'
import { PlusIcon, MinusIcon } from '@heroicons/react/24/outline'
export default function PlusMinusFaq(){
const [open,setOpen]=useState(null)
const items=[
{id:1,q:'Do you support React/Vue?',a:'Yes, both with matching code samples.'},
{id:2,q:'How to install?',a:'Install Tailwind and copy the snippets.'},
{id:3,q:'Is there support?',a:'Open GitHub issues or check docs.'},
]
return (<div className="max-w-2xl mx-auto space-y-3">
{items.map(it=>(
<div key={it.id} className="border border-gray-300 rounded-lg">
<button onClick={()=>setOpen(open===it.id?null:it.id)} className="w-full px-6 py-4 flex items-center justify-between">
<span className="font-medium">{it.q}</span>
<span className="text-gray-500">{open!==it.id ? <PlusIcon className="w-5 h-5"/> : <MinusIcon className="w-5 h-5"/>}</span>
</button>
{open===it.id && (<div className="px-6 pb-4 border-t border-gray-200 text-gray-700">{it.a}</div>)}
</div>
))}
</div>)
}
3) FAQ Grid + Expand/Collapse All
Two-column on desktop with “Expand all / Collapse all” controls.
General Questions
<div x-data="faqGrid()" class="max-w-4xl mx-auto">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold">General Questions</h3>
<div class="flex gap-2">
<button @click="expandAll()" class="px-3 py-1.5 text-sm rounded-lg border border-black/10 hover:bg-gray-50">Expand all</button>
<button @click="collapseAll()" class="px-3 py-1.5 text-sm rounded-lg border border-black/10 hover:bg-gray-50">Collapse all</button>
</div>
</div>
<div class="grid md:grid-cols-2 gap-3">
<div class="rounded-lg border border-gray-200" x-data>
<button @click="toggle(1)" class="w-full px-5 py-4 flex justify-between items-start text-left hover:bg-gray-50">
<span class="font-medium">Where are components stored?</span>
<svg :class="{'rotate-180': isOpen(1)}" class="w-5 h-5 text-gray-500 transform transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="isOpen(1)" x-collapse>
<div class="px-5 pb-4 border-t border-gray-200 text-gray-700">In resources/views/pages/components.</div>
</div>
</div>
</div>
</div>
<script>
function faqGrid(){
return {
items:[{id:1},{id:2},{id:3},{id:4}],
openIds:[],
isOpen(id){return this.openIds.includes(id)},
toggle(id){this.isOpen(id) ? this.openIds=this.openIds.filter(x=>x!==id) : this.openIds.push(id)},
expandAll(){this.openIds=this.items.map(i=>i.id)},
collapseAll(){this.openIds=[]}
}
}
</script>
<template>
<div class="max-w-4xl mx-auto">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold">General Questions</h3>
<div class="flex gap-2">
<button @click="expandAll" class="px-3 py-1.5 text-sm rounded-lg border border-black/10 hover:bg-gray-50">Expand all</button>
<button @click="collapseAll" class="px-3 py-1.5 text-sm rounded-lg border border-black/10 hover:bg-gray-50">Collapse all</button>
</div>
</div>
<div class="grid md:grid-cols-2 gap-3">
<div v-for="it in items" :key="it.id" class="rounded-lg border border-gray-200">
<button @click="toggle(it.id)" class="w-full px-5 py-4 flex justify-between items-start text-left hover:bg-gray-50">
<span class="font-medium">{{ it.q }}</span>
<svg :class="['w-5 h-5 text-gray-500 transform transition-transform', openIds.includes(it.id)?'rotate-180':'']" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div v-show="openIds.includes(it.id)" class="px-5 pb-4 border-t border-gray-200 text-gray-700">{{ it.a }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({
items: [
{id:1,q:'Where are components stored?',a:'In resources/views/pages/components.'},
{id:2,q:'How do I add a new example?',a:'Create a controller method, route, and Blade file.'},
{id:3,q:'Do these work without Alpine?',a:'Use the Vue/React versions.'},
{id:4,q:'Can I customize colors?',a:'Yes—Tailwind theme.extend.'},
],
openIds:[]
})
const toggle = id => state.openIds.includes(id) ? state.openIds = state.openIds.filter(x=>x!==id) : state.openIds.push(id)
const expandAll = () => state.openIds = state.items.map(i=>i.id)
const collapseAll = () => state.openIds = []
const { items, openIds } = state
</script>
import React, { useState } from 'react'
export default function FaqGrid(){
const items = [
{id:1,q:'Where are components stored?',a:'In resources/views/pages/components.'},
{id:2,q:'How do I add a new example?',a:'Create a controller method, route, and Blade file.'},
{id:3,q:'Do these work without Alpine?',a:'Use the Vue/React versions.'},
{id:4,q:'Can I customize colors?',a:'Yes—Tailwind theme.extend.'},
]
const [openIds,setOpenIds]=useState([])
const isOpen = id => openIds.includes(id)
const toggle = id => setOpenIds(isOpen(id)?openIds.filter(x=>x!==id):[...openIds,id])
const expandAll = () => setOpenIds(items.map(i=>i.id))
const collapseAll = () => setOpenIds([])
return (<div className="max-w-4xl mx-auto">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">General Questions</h3>
<div className="flex gap-2">
<button onClick={expandAll} className="px-3 py-1.5 text-sm rounded-lg border border-black/10 hover:bg-gray-50">Expand all</button>
<button onClick={collapseAll} className="px-3 py-1.5 text-sm rounded-lg border border-black/10 hover:bg-gray-50">Collapse all</button>
</div>
</div>
<div className="grid md:grid-cols-2 gap-3">
{items.map(it=>(
<div key={it.id} className="rounded-lg border border-gray-200">
<button onClick={()=>toggle(it.id)} className="w-full px-5 py-4 flex justify-between items-start text-left hover:bg-gray-50">
<span className="font-medium">{it.q}</span>
<svg className={`w-5 h-5 text-gray-500 transform transition-transform ${isOpen(it.id)?'rotate-180':''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"/></svg>
</button>
{isOpen(it.id) && (<div className="px-5 pb-4 border-t border-gray-200 text-gray-700">{it.a}</div>)}
</div>
))}
</div>
</div>)
}
4) Icon Accent FAQ
Cards with leading icons and a subtle accent ring on open.
Every example ships with Alpine (preview) and matching Vue/React snippets so your stack stays in sync.
We follow good semantics and keep interactions simple. You can extend ARIA roles as needed.
Change colors, spacing, and radiuses with straight Tailwind classes or theme extension.
<div x-data="{ open:null }" class="max-w-2xl mx-auto space-y-3">
<div class="rounded-xl border border-gray-200" :class="{'ring-2 ring-violet/30': open===1}">
<button @click="open = open===1 ? null : 1" class="w-full px-5 py-4 flex items-center gap-4 text-left hover:bg-gray-50">
<span class="flex h-10 w-10 items-center justify-center rounded-lg bg-violet/10 text-violet">...icon...</span>
<span class="flex-1"><div class="font-semibold">What makes these FAQs different?</div><div class="text-sm text-gray-600">Design-first, copy-paste friendly, Vue/React parity.</div></span>
<svg class="w-5 h-5 text-gray-500 transform transition-transform" :class="{'rotate-180': open===1}" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open===1" x-collapse><div class="px-5 pb-5 pt-2 text-gray-700 border-t border-gray-200">...</div></div>
</div>
</div>
<template>
<div class="max-w-2xl mx-auto space-y-3">
<div v-for="it in items" :key="it.id" class="rounded-xl border border-gray-200" :class="open===it.id ? 'ring-2 ring-violet/30' : ''">
<button @click="open = open===it.id ? null : it.id" class="w-full px-5 py-4 flex items-center gap-4 text-left hover:bg-gray-50">
<span class="flex h-10 w-10 items-center justify-center rounded-lg" :class="it.bg"><component :is="it.icon" class="h-5 w-5" /></span>
<span class="flex-1"><div class="font-semibold">{{ it.q }}</div><div class="text-sm text-gray-600">{{ it.sub }}</div></span>
<svg :class="['w-5 h-5 text-gray-500 transform transition-transform', open===it.id?'rotate-180':'']" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div v-show="open===it.id" class="px-5 pb-5 pt-2 text-gray-700 border-t border-gray-200">{{ it.a }}</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { SparklesIcon, ShieldCheckIcon, Cog6ToothIcon } from '@heroicons/vue/24/outline'
const open = ref(null)
const items = [
{id:1,q:'What makes these FAQs different?',sub:'Design-first, copy-paste friendly, Vue/React parity.',a:'Every example ships with Alpine and matching Vue/React.',icon:SparklesIcon,bg:'bg-violet/10 text-violet'},
{id:2,q:'Are they accessible?',sub:'Keyboard close (Esc) + semantic roles.',a:'Good semantics and simple interactions.',icon:ShieldCheckIcon,bg:'bg-emerald/10 text-emerald-600'},
{id:3,q:'How customizable?',sub:'Utility-first = instant themes.',a:'Change colors/spacing via Tailwind.',icon:Cog6ToothIcon,bg:'bg-amber/10 text-amber-600'},
]
</script>
import React, { useState } from 'react'
import { SparklesIcon, ShieldCheckIcon, Cog6ToothIcon } from '@heroicons/react/24/outline'
export default function IconAccentFaq(){
const [open,setOpen]=useState(null)
const items=[
{id:1,q:'What makes these FAQs different?',sub:'Design-first, copy-paste friendly, Vue/React parity.',a:'Every example ships with Alpine and matching Vue/React.',Icon:SparklesIcon,bg:'bg-violet/10 text-violet'},
{id:2,q:'Are they accessible?',sub:'Keyboard close (Esc) + semantic roles.',a:'Good semantics and simple interactions.',Icon:ShieldCheckIcon,bg:'bg-emerald/10 text-emerald-600'},
{id:3,q:'How customizable?',sub:'Utility-first = instant themes.',a:'Change colors/spacing via Tailwind.',Icon:Cog6ToothIcon,bg:'bg-amber/10 text-amber-600'},
]
return (<div className="max-w-2xl mx-auto space-y-3">
{items.map(it=>(
<div key={it.id} className={`rounded-xl border border-gray-200 ${open===it.id?'ring-2 ring-violet/30':''}`}>
<button onClick={()=>setOpen(open===it.id?null:it.id)} className="w-full px-5 py-4 flex items-center gap-4 text-left hover:bg-gray-50">
<span className={`flex h-10 w-10 items-center justify-center rounded-lg ${it.bg}`}><it.Icon className="h-5 w-5"/></span>
<span className="flex-1"><div className="font-semibold">{it.q}</div><div className="text-sm text-gray-600">{it.sub}</div></span>
<svg className={`w-5 h-5 text-gray-500 transform transition-transform ${open===it.id?'rotate-180':''}`} viewBox="0 0 24 24" fill="none" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"/></svg>
</button>
{open===it.id && (<div className="px-5 pb-5 pt-2 text-gray-700 border-t border-gray-200">{it.a}</div>)}
</div>
))}
</div>)
}
Notes
- HTML previews use Alpine. Make sure Alpine (and
@alpinejs/collapse
if you usex-collapse
) is loaded in yourapp.js
. - Vue/React versions are 1:1 visually with the previews.
- Adjust spacing/rounded/colored borders via Tailwind utilities to match your brand.