Accordion
Organizes content into collapsible sections, allowing users to focus on one section at a time.
<script lang="ts">
import { Accordion } from "bits-ui";
import CaretDown from "phosphor-svelte/lib/CaretDown";
const items = [
{
title: "What is the meaning of life?",
content:
"To become a better person, to help others, and to leave the world a better place than you found it."
},
{
title: "How do I become a better person?",
content:
"Read books, listen to podcasts, and surround yourself with people who inspire you."
},
{
title: "What is the best way to help others?",
content: "Give them your time, attention, and love."
}
];
let value = $state<string[]>([]);
</script>
<Accordion.Root class="w-full sm:max-w-[70%]" type="multiple" bind:value>
{#each items as item, i}
<Accordion.Item value={`${i}`} class="group border-b border-dark-10 px-1.5">
<Accordion.Header>
<Accordion.Trigger
class="flex w-full flex-1 items-center justify-between py-5 text-[15px] font-medium transition-all [&[data-state=open]>span>svg]:rotate-180"
>
{item.title}
<span
class="inline-flex size-8 items-center justify-center rounded-[7px] bg-transparent transition-all hover:bg-dark-10"
>
<CaretDown class="size-[18px] transition-all duration-200" />
</span>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content
class="overflow-hidden text-sm tracking-[-0.01em] data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
>
<div class="pb-[25px]">
{item.content}
</div>
</Accordion.Content>
</Accordion.Item>
{/each}
</Accordion.Root>
import typography from "@tailwindcss/typography";
import animate from "tailwindcss-animate";
import { fontFamily } from "tailwindcss/defaultTheme";
/** @type {import('tailwindcss').Config} */
export default {
darkMode: "class",
content: ["./src/**/*.{html,js,svelte,ts}"],
theme: {
container: {
center: true,
screens: {
"2xl": "1440px",
},
},
extend: {
colors: {
border: {
DEFAULT: "hsl(var(--border-card))",
input: "hsl(var(--border-input))",
"input-hover": "hsl(var(--border-input-hover))",
},
background: {
DEFAULT: "hsl(var(--background) / <alpha-value>)",
alt: "hsl(var(--background-alt) / <alpha-value>)",
},
foreground: {
DEFAULT: "hsl(var(--foreground) / <alpha-value>)",
alt: "hsl(var(--foreground-alt) / <alpha-value>)",
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground))",
},
dark: {
DEFAULT: "hsl(var(--dark) / <alpha-value>)",
4: "hsl(var(--dark-04))",
10: "hsl(var(--dark-10))",
40: "hsl(var(--dark-40))",
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
},
contrast: {
DEFAULT: "hsl(var(--contrast) / <alpha-value>)",
},
},
fontFamily: {
sans: ["Inter", ...fontFamily.sans],
mono: ["Source Code Pro", ...fontFamily.mono],
alt: ["Courier", ...fontFamily.sans],
},
fontSize: {
xxs: "10px",
},
borderWidth: {
6: "6px",
},
borderRadius: {
card: "16px",
"card-lg": "20px",
"card-sm": "10px",
input: "9px",
button: "5px",
"5px": "5px",
"9px": "9px",
"10px": "10px",
"15px": "15px",
},
height: {
input: "3rem",
"input-sm": "2.5rem",
},
boxShadow: {
mini: "var(--shadow-mini)",
"mini-inset": "var(--shadow-mini-inset)",
popover: "var(--shadow-popover)",
kbd: "var(--shadow-kbd)",
btn: "var(--shadow-btn)",
card: "var(--shadow-card)",
"date-field-focus": "var(--shadow-date-field-focus)",
},
opacity: {
8: "0.08",
},
scale: {
80: ".80",
98: ".98",
99: ".99",
},
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--bits-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--bits-accordion-content-height)" },
to: { height: "0" },
},
"caret-blink": {
"0%,70%,100%": { opacity: "1" },
"20%,50%": { opacity: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"caret-blink": "caret-blink 1.25s ease-out infinite",
},
},
plugins: [typography, animate],
};
@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Colors */
--background: 0 0% 100%;
--background-alt: 0 0% 100%;
--foreground: 0 0% 9%;
--foreground-alt: 0 0% 32%;
--muted: 240 5% 96%;
--muted-foreground: 0 0% 9% / 0.4;
--border: 240 6% 10%;
--border-input: 240 6% 10% / 0.17;
--border-input-hover: 240 6% 10% / 0.4;
--border-card: 240 6% 10% / 0.1;
--dark: 240 6% 10%;
--dark-10: 240 6% 10% / 0.1;
--dark-40: 240 6% 10% / 0.4;
--dark-04: 240 6% 10% / 0.04;
--accent: 204 94% 94%;
--accent-foreground: 204 80% 16%;
--destructive: 347 77% 50%;
/* black */
--constrast: 0 0% 0%;
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.04) inset;
--shadow-popover: 0px 7px 12px 3px hsla(var(--dark-10));
--shadow-kbd: 0px 2px 0px 0px rgba(0, 0, 0, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.03);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(24, 24, 27, 0.17);
}
.dark {
/* Colors */
--background: 0 0% 5%;
--background-alt: 0 0% 8%;
--foreground: 0 0% 95%;
--foreground-alt: 0 0% 70%;
--muted: 240 4% 16%;
--muted-foreground: 0 0% 100% / 0.4;
--border: 0 0% 96%;
--border-input: 0 0% 96% / 0.17;
--border-input-hover: 0 0% 96% / 0.4;
--border-card: 0 0% 96% / 0.1;
--dark: 0 0% 96%;
--dark-40: 0 0% 96% / 0.4;
--dark-10: 0 0% 96% / 0.1;
--dark-04: 0 0% 96% / 0.04;
--accent: 204 90 90%;
--accent-foreground: 204 94% 94%;
--destructive: 350 89% 60%;
/* white */
--constrast: 0 0% 100%;
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.3);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.5) inset;
--shadow-popover: 0px 7px 12px 3px hsla(0deg 0% 0% / 30%);
--shadow-kbd: 0px 2px 0px 0px rgba(255, 255, 255, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.2);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.4);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(244, 244, 245, 0.1);
}
}
@layer base {
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
/* Mobile tap highlight */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-tap-highlight-color */
html {
-webkit-tap-highlight-color: rgba(128, 128, 128, 0.5);
}
::selection {
background: #fdffa4;
color: black;
}
/* === Scrollbars === */
::-webkit-scrollbar {
@apply w-2;
@apply h-2;
}
::-webkit-scrollbar-track {
@apply !bg-transparent;
}
::-webkit-scrollbar-thumb {
@apply rounded-card-lg !bg-dark-10;
}
::-webkit-scrollbar-corner {
background: rgba(0, 0, 0, 0);
}
/* Firefox */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color#browser_compatibility */
html {
scrollbar-color: var(--bg-muted);
}
.antialised {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer utilities {
.step {
counter-increment: step;
}
.step:before {
@apply absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 border-background bg-muted text-center -indent-px font-mono text-base font-medium;
@apply ml-[-50px] mt-[-4px];
content: counter(step);
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
}
.link {
@apply inline-flex items-center gap-1 rounded-sm font-medium underline underline-offset-4 hover:text-foreground/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
}
}
Structure
<script lang="ts">
import { Accordion } from "bits-ui";
</script>
<Accordion.Root>
<Accordion.Item>
<Accordion.Header>
<Accordion.Trigger />
</Accordion.Header>
<Accordion.Content />
</Accordion.Item>
</Accordion.Root>
Usage
Single
Set the type
prop to "single"
to allow only one accordion item to be open at a time.
<Accordion.Root type="single">
<!-- ... -->
</Accordion.Root>
Multiple
Set the type
prop to "multiple"
to allow multiple accordion items to be open at the same time.
<Accordion.Root type="multiple">
<!-- ... -->
</Accordion.Root>
Disable Items
To disable an individual accordion item, set the disabled
prop to true
. This will prevent users from interacting with the item.
<Accordion.Root type="single">
<Accordion.Item value="item-1" disabled>
<!-- ... -->
</Accordion.Item>
</Accordion.Root>
Controlled Value
You can programmatically control the active of the accordion item(s) using the value
prop.
<script lang="ts">
let value = $state("item-1");
</script>
<button onclick={() => (value = "item-2")}>Change value</button>
<Accordion.Root bind:value>
<!-- ... -->
</Accordion.Root>
Value Change Side Effects
You can use the onValueChange
prop to handle side effects when the value of the accordion changes.
<Accordion.Root
onValueChange={(value) => {
doSomething(value);
}}
>
<!-- ... -->
</Accordion.Root>
Alternatively, you can use bind:value
with an $effect
block to handle side effects when the value of the accordion changes.
<script lang="ts">
import { Accordion } from "bits-ui";
let value = $state("item-1")
$effect(() => {
doSomething(value);
})
</script>
<Accordion.Root bind:value>
<!-- ... -->
</Accordion.Item>
Reusable Wrappers
Entire Component
If you're going to be using the same accordion component multiple places throughout your app, you can create a reusable wrapper to reduce the amount of code you need to write each time.
<script lang="ts">
import { Accordion, type WithoutChildren } from "bits-ui";
type Props = WithoutChildren<Accordion.RootProps> & {
items: Array<{
value: string;
disabled?: boolean;
title: string;
content: string;
}>;
};
let { items, value = $bindable(""), ...restProps }: Props = $props();
</script>
<Accordion.Root bind:value {...restProps}>
{#each items as item}
<Accordion.Item value={item.value} disabled={item.disabled}>
<Accordion.Header>
<Accordion.Trigger>{item.title}</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>{item.content}</Accordion.Content>
</Accordion.Item>
{/each}
</Accordion.Root>
Individual Item
For each invidual item, you need an Accordion.Item
, Accordion.Header
, Accordion.Trigger
and Accordion.Content
component. You can make a reusable wrapper to reduce the amount of code you need to write each time.
<script lang="ts">
import { Accordion, type WithoutChildren } from "bits-ui";
type Props = WithoutChildren<Accordion.ItemProps> & {
title: string;
content: string;
};
let { title, content, ...restProps }: Props = $props();
</script>
<Accordion.Item {...restProps}>
<Accordion.Header>
<Accordion.Trigger>
{title}
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>
{content}
</Accordion.Content>
</Accordion.Item>
<script lang="ts">
import { Accordion } from "bits-ui";
import CustomAccordionItem from "$lib/components/CustomAccordionItem.svelte";
</script>
<Accordion.Root type="single">
<CustomAccordionItem title="Item 1" content="Content 1" />
<CustomAccordionItem title="Item 2" content="Content 2" />
<CustomAccordionItem title="Item 3" content="Content 3" />
</Accordion.Root>
API Reference
The root accordion component used to set and manage the state of the accordion.
Property | Type | Description |
---|---|---|
multiple | boolean | Whether or not multiple accordion items can be active at the same time. Default: false |
disabled | boolean | Whether or not the accordion is disabled. Default: false |
value | union | The active accordion item value. Default: undefined |
onValueChange | function | A callback function called when the active accordion item value changes. Default: undefined |
asChild | boolean | Whether to use render delegation with this component or not. Default: false |
el | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to programatically interact with the element. Default: undefined |
Slot Property | Type | Description |
---|---|---|
builder | object | The builder attributes and actions to apply to the element if using the |
Data Attribute | Value | Description |
---|---|---|
data-orientation | enum | The orientation of the accordion. |
data-accordion-root |
| Present on the root element. |
An accordion item.
Property | Type | Description |
---|---|---|
value * Required | string | The value of the accordion item. Default: undefined |
disabled | boolean | Whether or not the accordion item is disabled. Default: false |
asChild | boolean | Whether to use render delegation with this component or not. Default: false |
el | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to programatically interact with the element. Default: undefined |
Slot Property | Type | Description |
---|---|---|
builder | object | The builder attributes and actions to apply to the element if using the |
Data Attribute | Value | Description |
---|---|---|
data-state | enum | The state of the accordion item. |
data-disabled |
| Present when the accordion item is disabled. |
data-accordion-item | —— | Present on the item element. |
The accordion item header, which wraps the trigger and makes it more accessible.
Property | Type | Description |
---|---|---|
level | enum | The heading level to use for the header. This will be set as the Default: undefined |
asChild | boolean | Whether to use render delegation with this component or not. Default: false |
el | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to programatically interact with the element. Default: undefined |
Slot Property | Type | Description |
---|---|---|
builder | object | The builder attributes and actions to apply to the element if using the |
Data Attribute | Value | Description |
---|---|---|
data-heading-level | enum | The heading level of the header. |
data-accordion-header | —— | Present on the header element. |
The accordion item trigger, which opens and closes the accordion item.
Property | Type | Description |
---|---|---|
asChild | boolean | Whether to use render delegation with this component or not. Default: false |
el | HTMLButtonElement | The underlying DOM element being rendered. You can bind to this to programatically interact with the element. Default: undefined |
Slot Property | Type | Description |
---|---|---|
builder | object | The builder attributes and actions to apply to the element if using the |
Data Attribute | Value | Description |
---|---|---|
data-state | enum | The state of the accordion item. |
data-disabled | —— | Present when the accordion item is disabled. |
data-value | —— | The value of the accordion item. |
data-accordion-trigger | —— | Present on the trigger element. |
The accordion item content, which is displayed when the item is open.
Property | Type | Description |
---|---|---|
transition | function | A Svelte transition function to use when transitioning the content in and out. Default: undefined |
transitionConfig | TransitionConfig | The configuration to apply to the transition. Default: undefined |
inTransition | function | A Svelte transition function to use when transitioning the content in and out. Default: undefined |
inTransitionConfig | TransitionConfig | The configuration to apply to the transition. Default: undefined |
outTransition | function | A Svelte transition function to use when transitioning the content in and out. Default: undefined |
outTransitionConfig | TransitionConfig | The configuration to apply to the transition. Default: undefined |
asChild | boolean | Whether to use render delegation with this component or not. Default: false |
el | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to programatically interact with the element. Default: undefined |
Slot Property | Type | Description |
---|---|---|
builder | object | The builder attributes and actions to apply to the element if using the |
Data Attribute | Value | Description |
---|---|---|
data-state | enum | The state of the accordion item. |
data-disabled | —— | Present when the accordion item is disabled. |
data-value | —— | The value of the accordion item. |
data-accordion-content | —— | Present on the content element. |