Accordion
Organizes content into collapsible sections, allowing users to focus on one or more sections at a time.
<script lang="ts">
import { Accordion } from "bits-ui";
import CaretDown from "phosphor-svelte/lib/CaretDown";
const items = [
{
value: "1",
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."
},
{
value: "2",
title: "How do I become a better person?",
content:
"Read books, listen to podcasts, and surround yourself with people who inspire you."
},
{
value: "3",
title: "What is the best way to help others?",
content: "Give them your time, attention, and love."
}
];
</script>
<Accordion.Root class="w-full sm:max-w-[70%]" type="multiple">
{#each items as item (item.value)}
<Accordion.Item
value={item.value}
class="border-dark-10 group border-b px-1.5"
>
<Accordion.Header>
<Accordion.Trigger
class="flex w-full flex-1 select-none items-center justify-between py-5 text-[15px] font-medium transition-all [&[data-state=open]>span>svg]:rotate-180"
>
<span class="w-full text-left">
{item.title}
</span>
<span
class="hover:bg-dark-10 inline-flex size-8 items-center justify-center rounded-[7px] bg-transparent"
>
<CaretDown class="size-[18px] transition-transform duration-200" />
</span>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm tracking-[-0.01em]"
>
<div class="pb-[25px]">
{item.content}
</div>
</Accordion.Content>
</Accordion.Item>
{/each}
</Accordion.Root>
@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");
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@font-face {
font-family: "Cal Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/CalSans-SemiBold.woff2") format("woff2");
}
:root {
/* Colors */
--background: hsl(0 0% 100%);
--background-alt: hsl(0 0% 100%);
--foreground: hsl(0 0% 9%);
--foreground-alt: hsl(0 0% 32%);
--muted: hsl(240 5% 96%);
--muted-foreground: hsla(0 0% 9% / 0.4);
--border: hsl(240 6% 10%);
--border-input: hsla(240 6% 10% / 0.17);
--border-input-hover: hsla(240 6% 10% / 0.4);
--border-card: hsla(240 6% 10% / 0.1);
--dark: hsl(240 6% 10%);
--dark-10: hsla(240 6% 10% / 0.1);
--dark-40: hsla(240 6% 10% / 0.4);
--dark-04: hsla(240 6% 10% / 0.04);
--accent: hsl(204 94% 94%);
--accent-foreground: hsl(204 80% 16%);
--destructive: hsl(347 77% 50%);
--tertiary: hsl(37.7 92.1% 50.2%);
--line: hsl(0 0% 100%);
/* black */
--contrast: hsl(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: hsl(0 0% 5%);
--background-alt: hsl(0 0% 8%);
--foreground: hsl(0 0% 95%);
--foreground-alt: hsl(0 0% 70%);
--muted: hsl(240 4% 16%);
--muted-foreground: hsla(0 0% 100% / 0.4);
--border: hsl(0 0% 96%);
--border-input: hsla(0 0% 96% / 0.17);
--border-input-hover: hsla(0 0% 96% / 0.4);
--border-card: hsla(0 0% 96% / 0.1);
--dark: hsl(0 0% 96%);
--dark-40: hsl(0 0% 96% / 0.4);
--dark-10: hsl(0 0% 96% / 0.1);
--dark-04: hsl(0 0% 96% / 0.04);
--accent: hsl(204 90% 90%);
--accent-foreground: hsl(204 94% 94%);
--destructive: hsl(350 89% 60%);
--line: hsl(0 0% 9.02%);
--tertiary: hsl(61.3 100% 82.2%);
/* white */
--contrast: hsl(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);
}
@theme inline {
--color-background: var(--background);
--color-background-alt: var(--background-alt);
--color-foreground: var(--foreground);
--color-foreground-alt: var(--foreground-alt);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border-card);
--color-border-input: var(--border-input);
--color-border-input-hover: var(--border-input-hover);
--color-border-card: var(--border-card);
--color-dark: var(--dark);
--color-dark-10: var(--dark-10);
--color-dark-40: var(--dark-40);
--color-dark-04: var(--dark-04);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-tertiary: var(--tertiary);
--color-line: var(--line);
--color-contrast: var(--contrast);
--shadow-mini: var(--shadow-mini);
--shadow-mini-inset: var(--shadow-mini-inset);
--shadow-popover: var(--shadow-popover);
--shadow-kbd: var(--shadow-kbd);
--shadow-btn: var(--shadow-btn);
--shadow-card: var(--shadow-card);
--shadow-date-field-focus: var(--shadow-date-field-focus);
--text-xxs: 10px;
--radius-card: 16px;
--radius-card-lg: 20px;
--radius-card-sm: 10px;
--radius-input: 9px;
--radius-button: 5px;
--radius-5px: 5px;
--radius-9px: 9px;
--radius-10px: 10px;
--radius-15px: 15px;
--spacing-input: 3rem;
--spacing-input-sm: 2.5rem;
--breakpoint-desktop: 1440px;
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-caret-blink: caret-blink 1s ease-out infinite;
--animate-scale-in: scale-in 0.2s ease;
--animate-scale-out: scale-out 0.15s ease;
--animate-fade-in: fade-in 0.2s ease;
--animate-fade-out: fade-out 0.15s ease;
--animate-enter-from-left: enter-from-left 0.2s ease;
--animate-enter-from-right: enter-from-right 0.2s ease;
--animate-exit-to-left: exit-to-left 0.2s ease;
--animate-exit-to-right: exit-to-right 0.2s ease;
--font-sans: "Inter", "sans-serif";
--font-mono: "Source Code Pro", "monospace";
--font-alt: "Courier", "sans-serif";
--font-display: "Cal Sans", "sans-serif";
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--bits-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--bits-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
@keyframes enter-from-right {
from {
opacity: 0;
transform: translateX(200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes enter-from-left {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes exit-to-right {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes exit-to-left {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: rotateX(-10deg) scale(0.9);
}
to {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
}
@keyframes scale-out {
from {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
to {
opacity: 0;
transform: rotateX(-10deg) scale(0.95);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-border-card, currentColor);
}
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
scrollbar-color: var(--bg-muted);
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
::selection {
background: #fdffa4;
color: black;
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:ring-foreground focus-visible:ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1;
}
}
.link {
@apply hover:text-foreground/80 focus-visible:ring-foreground focus-visible:ring-offset-background rounded-xs focus-visible:outline-hidden inline-flex items-center gap-1 font-medium underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-offset-2;
}
}
Overview
The Accordion component is a versatile UI element designed to organize content into collapsible sections, helping users focus on specific information without being overwhelmed by visual clutter.
Quick Start
<script lang="ts">
import { Accordion } from "bits-ui";
</script>
<Accordion.Root type="single">
<Accordion.Item value="item-1">
<Accordion.Header>
<Accordion.Trigger>Item 1 Title</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>This is the collapsible content for this section.</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-2">
<Accordion.Header>
<Accordion.Trigger>Item 2 Title</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>This is the collapsible content for this section.</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
Key Features
- Single or Multiple Mode: Toggle between allowing one open section or multiple sections at once.
- Accessible by Default: Built-in ARIA attributes and keyboard navigation support.
- Smooth Transitions: Leverage CSS variables or Svelte transitions for animated open/close effects.
- Flexible State: Use uncontrolled defaults or take full control with bound values.
Structure
The Accordion is a compound component made up of several parts:
Accordion.Root
: Container that manages overall stateAccordion.Item
: Individual collapsible sectionAccordion.Header
: Contains the visible headingAccordion.Trigger
: The clickable element that toggles content visibilityAccordion.Content
: The collapsible body content
Reusable Components
To streamline usage in larger applications, create custom wrapper components for repeated patterns. Below is an example of a reusable MyAccordionItem
and MyAccordion
.
Item Wrapper
Combines Item
, Header
, Trigger
, and Content
into a single component:
<script lang="ts">
import { Accordion, type WithoutChildrenOrChild } from "bits-ui";
type Props = WithoutChildrenOrChild<Accordion.ItemProps> & {
title: string;
content: string;
};
let { title, content, ...restProps }: Props = $props();
</script>
<Accordion.Item {...restProps}>
<Accordion.Header>
<Accordion.Trigger>{item.title}</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>
{content}
</Accordion.Content>
</Accordion.Item>
Accordion Wrapper
Wraps Root
and renders multiple MyAccordionItem
components:
<script lang="ts">
import { Accordion, type WithoutChildrenOrChild } from "bits-ui";
import MyAccordionItem from "$lib/components/MyAccordionItem.svelte";
type Item = {
value?: string;
title: string;
content: string;
disabled?: boolean;
};
let {
value = $bindable(),
ref = $bindable(null),
...restProps
}: WithoutChildrenOrChild<Accordion.RootProps> & {
items: Item[];
} = $props();
</script>
<!--
Since we have to destructure the `value` to make it `$bindable`, we need to use `as any` here to avoid
type errors from the discriminated union of `"single" | "multiple"`.
(an unfortunate consequence of having to destructure bindable values)
-->
<Accordion.Root bind:value bind:ref {...restProps as any}>
{#each items as item, i (item.title + i)}
<MyAccordionItem {...item} />
{/each}
</Accordion.Root>
Usage Example
<script lang="ts">
import MyAccordion from "$lib/components/MyAccordion.svelte";
const items = [
{ title: "Item 1", content: "Content 1" },
{ title: "Item 2", content: "Content 2" },
];
</script>
<MyAccordion type="single" {items} />
Tip
Use unique value
props for each Item
if you plan to control the state programatically.
Managing Value State
This section covers how to manage the value
state of the Accordion.
Two-Way Binding
Use bind:value
for simple, automatic state synchronization:
<script lang="ts">
import { Accordion } from "bits-ui";
let myValue = $state<string[]>([]);
const numberOfItemsOpen = $derived(myValue.length);
</script>
<button
onclick={() => {
myValue = ["item-1", "item-2"];
}}
>
Open Items 1 and 2
</button>
<Accordion.Root type="multiple" bind:value={myValue}>
<Accordion.Item value="item-1">
<!-- ... -->
</Accordion.Item>
<Accordion.Item value="item-2">
<!-- ... -->
</Accordion.Item>
<Accordion.Item value="item-3">
<!-- ... -->
</Accordion.Item>
</Accordion.Root>
Fully Controlled
Use a Function Binding for complete control over the state's reads and writes.
<script lang="ts">
import { Accordion } from "bits-ui";
let myValue = $state("");
function getValue() {
return myValue;
}
function setValue(newValue: string) {
myValue = newValue;
}
</script>
<Accordion.Root type="single" bind:value={getValue, setValue}>
<!-- ... -->
</Accordion.Root>
See the State Management documentation for more information.
Customization
Single vs. Multiple
Set the type
prop to "single"
to allow only one accordion item to be open at a time.
<MyAccordion
type="single"
items={[
{ title: "Title A", content: "Content A" },
{ title: "Title B", content: "Content B" },
{ title: "Title C", content: "Content C" },
]}
/>
Set the type
prop to "multiple"
to allow multiple accordion items to be open at the same time.
<MyAccordion
type="multiple"
items={[
{ title: "Title A", content: "Content A" },
{ title: "Title B", content: "Content B" },
{ title: "Title C", content: "Content C" },
]}
/>
Default Open Items
Set the value
prop to pre-open items:
<MyAccordion value={["A", "C"]} type="multiple" />
Disable Items
Disable specific items with the disabled
prop:
<Accordion.Root type="single">
<Accordion.Item value="item-1" disabled>
<!-- ... -->
</Accordion.Item>
</Accordion.Root>
Svelte Transitions
The Accordion component can be enhanced with Svelte's built-in transition effects or other animation libraries.
Using forceMount
and child
Snippets
To apply Svelte transitions to Accordion components, use the forceMount
prop in combination with the child
snippet. This approach gives you full control over the mounting behavior and animation of the Accordion.Content
.
<Accordion.Content forceMount={true}>
{#snippet child({ props, open })}
{#if open}
<div {...props} transition:slide={{ duration: 1000 }}>
This is the accordion content that will transition in and out.
</div>
{/if}
{/snippet}
</Accordion.Content>
In this example:
- The
forceMount
prop ensures the components are always in the DOM. - The
child
snippet provides access to the open state and component props. - Svelte's
#if
block controls when the content is visible. - Transition directives (
transition:fade
andtransition:fly
) apply the animations.
<script lang="ts">
import { Accordion } from "bits-ui";
import CaretDown from "phosphor-svelte/lib/CaretDown";
import { slide } from "svelte/transition";
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="border-dark-10 group border-b 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="hover:bg-dark-10 inline-flex size-8 items-center justify-center rounded-[7px] bg-transparent transition-all"
>
<CaretDown class="size-[18px] transition-all duration-200" />
</span>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content
forceMount={true}
class="overflow-hidden text-sm tracking-[-0.01em]"
>
{#snippet child({ props, open })}
{#if open}
<div {...props} transition:slide={{ duration: 1000 }}>
<div class="pb-[25px]">
{item.content}
</div>
</div>
{/if}
{/snippet}
</Accordion.Content>
</Accordion.Item>
{/each}
</Accordion.Root>
@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");
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@font-face {
font-family: "Cal Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/CalSans-SemiBold.woff2") format("woff2");
}
:root {
/* Colors */
--background: hsl(0 0% 100%);
--background-alt: hsl(0 0% 100%);
--foreground: hsl(0 0% 9%);
--foreground-alt: hsl(0 0% 32%);
--muted: hsl(240 5% 96%);
--muted-foreground: hsla(0 0% 9% / 0.4);
--border: hsl(240 6% 10%);
--border-input: hsla(240 6% 10% / 0.17);
--border-input-hover: hsla(240 6% 10% / 0.4);
--border-card: hsla(240 6% 10% / 0.1);
--dark: hsl(240 6% 10%);
--dark-10: hsla(240 6% 10% / 0.1);
--dark-40: hsla(240 6% 10% / 0.4);
--dark-04: hsla(240 6% 10% / 0.04);
--accent: hsl(204 94% 94%);
--accent-foreground: hsl(204 80% 16%);
--destructive: hsl(347 77% 50%);
--tertiary: hsl(37.7 92.1% 50.2%);
--line: hsl(0 0% 100%);
/* black */
--contrast: hsl(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: hsl(0 0% 5%);
--background-alt: hsl(0 0% 8%);
--foreground: hsl(0 0% 95%);
--foreground-alt: hsl(0 0% 70%);
--muted: hsl(240 4% 16%);
--muted-foreground: hsla(0 0% 100% / 0.4);
--border: hsl(0 0% 96%);
--border-input: hsla(0 0% 96% / 0.17);
--border-input-hover: hsla(0 0% 96% / 0.4);
--border-card: hsla(0 0% 96% / 0.1);
--dark: hsl(0 0% 96%);
--dark-40: hsl(0 0% 96% / 0.4);
--dark-10: hsl(0 0% 96% / 0.1);
--dark-04: hsl(0 0% 96% / 0.04);
--accent: hsl(204 90% 90%);
--accent-foreground: hsl(204 94% 94%);
--destructive: hsl(350 89% 60%);
--line: hsl(0 0% 9.02%);
--tertiary: hsl(61.3 100% 82.2%);
/* white */
--contrast: hsl(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);
}
@theme inline {
--color-background: var(--background);
--color-background-alt: var(--background-alt);
--color-foreground: var(--foreground);
--color-foreground-alt: var(--foreground-alt);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border-card);
--color-border-input: var(--border-input);
--color-border-input-hover: var(--border-input-hover);
--color-border-card: var(--border-card);
--color-dark: var(--dark);
--color-dark-10: var(--dark-10);
--color-dark-40: var(--dark-40);
--color-dark-04: var(--dark-04);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-tertiary: var(--tertiary);
--color-line: var(--line);
--color-contrast: var(--contrast);
--shadow-mini: var(--shadow-mini);
--shadow-mini-inset: var(--shadow-mini-inset);
--shadow-popover: var(--shadow-popover);
--shadow-kbd: var(--shadow-kbd);
--shadow-btn: var(--shadow-btn);
--shadow-card: var(--shadow-card);
--shadow-date-field-focus: var(--shadow-date-field-focus);
--text-xxs: 10px;
--radius-card: 16px;
--radius-card-lg: 20px;
--radius-card-sm: 10px;
--radius-input: 9px;
--radius-button: 5px;
--radius-5px: 5px;
--radius-9px: 9px;
--radius-10px: 10px;
--radius-15px: 15px;
--spacing-input: 3rem;
--spacing-input-sm: 2.5rem;
--breakpoint-desktop: 1440px;
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-caret-blink: caret-blink 1s ease-out infinite;
--animate-scale-in: scale-in 0.2s ease;
--animate-scale-out: scale-out 0.15s ease;
--animate-fade-in: fade-in 0.2s ease;
--animate-fade-out: fade-out 0.15s ease;
--animate-enter-from-left: enter-from-left 0.2s ease;
--animate-enter-from-right: enter-from-right 0.2s ease;
--animate-exit-to-left: exit-to-left 0.2s ease;
--animate-exit-to-right: exit-to-right 0.2s ease;
--font-sans: "Inter", "sans-serif";
--font-mono: "Source Code Pro", "monospace";
--font-alt: "Courier", "sans-serif";
--font-display: "Cal Sans", "sans-serif";
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--bits-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--bits-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
@keyframes enter-from-right {
from {
opacity: 0;
transform: translateX(200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes enter-from-left {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes exit-to-right {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes exit-to-left {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: rotateX(-10deg) scale(0.9);
}
to {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
}
@keyframes scale-out {
from {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
to {
opacity: 0;
transform: rotateX(-10deg) scale(0.95);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-border-card, currentColor);
}
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
scrollbar-color: var(--bg-muted);
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
::selection {
background: #fdffa4;
color: black;
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:ring-foreground focus-visible:ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1;
}
}
.link {
@apply hover:text-foreground/80 focus-visible:ring-foreground focus-visible:ring-offset-background rounded-xs focus-visible:outline-hidden inline-flex items-center gap-1 font-medium underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-offset-2;
}
}
Best Practices
For cleaner code and better maintainability, consider creating custom reusable components that encapsulate this transition logic.
<script lang="ts">
import { Accordion, type WithoutChildrenOrChild } from "bits-ui";
import type { Snippet } from "svelte";
import { fade } from "svelte/transition";
let {
ref = $bindable(null),
duration = 200,
children,
...restProps
}: WithoutChildrenOrChild<Accordion.ContentProps> & {
duration?: number;
children: Snippet;
} = $props();
</script>
<Accordion.Content forceMount bind:ref {...restProps}>
{#snippet child({ props, open })}
{#if open}
<div {...props} transition:fade={{ duration }}>
{@render children?.()}
</div>
{/if}
{/snippet}
</Accordion.Content>
You can then use the MyAccordionContent
component alongside the other Accordion
primitives throughout your application:
<Accordion.Root>
<Accordion.Item value="A">
<Accordion.Header>
<Accordion.Trigger>A</Accordion.Trigger>
</Accordion.Header>
<MyAccordionContent duration={300}>
<!-- ... -->
</MyAccordionContent>
</Accordion.Item>
</Accordion.Root>
Examples
The following examples demonstrate different ways to use the Accordion component.
Horizontal Cards
Use the Accordion component to create a horizontal card layout with collapsible sections.
<script lang="ts">
import { Accordion } from "bits-ui";
let value = $state("item-1");
const items = [
{
id: "item-1",
title: "Mountain Range",
image:
"https://images.unsplash.com/photo-1586589058841-f1264894a260?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.0.3",
description:
"Majestic mountain ranges with snow-capped peaks and lush valleys."
},
{
id: "item-2",
title: "Ocean Views",
image:
"https://images.unsplash.com/photo-1650300874827-7d39bc9276ea?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.0.3",
description:
"Serene ocean scenes with crashing waves, beautiful sunsets, and sandy beaches."
},
{
id: "item-3",
title: "Forest Retreats",
image:
"https://images.unsplash.com/photo-1693297490324-37ee6301f6c8?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.0.3",
description:
"Dense forests with towering trees, abundant wildlife, and peaceful streams."
}
];
</script>
<Accordion.Root
type="single"
orientation="horizontal"
class="flex h-[400px] w-full flex-col gap-2 sm:flex-row"
bind:value
>
{#each items as item (item.id)}
<Accordion.Item
value={item.id}
class="ring-primary/70 relative cursor-pointer overflow-hidden rounded-lg transition-all duration-500 ease-in-out data-[state=closed]:w-[10%] data-[state=open]:w-[100%] [&:has(:focus-visible)]:ring-2"
onclick={() => (value = item.id)}
>
<img
src={item.image}
alt={item.title}
class="h-[400px] w-full object-cover"
/>
<div
class="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-black/80 via-black/40 to-transparent p-4"
>
<div
class="transition-all duration-300 group-data-[state=closed]:translate-y-2 group-data-[state=open]:translate-y-0"
>
<Accordion.Header>
<Accordion.Trigger
class="focus-override text-left font-bold text-white transition-all duration-300 focus-visible:!outline-none data-[state=open]:mb-2 data-[state=closed]:text-sm data-[state=open]:text-xl data-[state=closed]:opacity-0 data-[state=open]:opacity-100"
>
{item.title}
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content
forceMount
class="max-h-0 overflow-hidden text-white/90 transition-all duration-700 data-[state=open]:max-h-[100px] data-[state=closed]:opacity-0 data-[state=open]:opacity-100"
>
{item.description}
</Accordion.Content>
<div
class="absolute bottom-0 left-0 h-1 w-full transition-all duration-300 group-data-[state=closed]:opacity-0 group-data-[state=open]:opacity-100"
></div>
</div>
</div>
</Accordion.Item>
{/each}
</Accordion.Root>
@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");
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@font-face {
font-family: "Cal Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/CalSans-SemiBold.woff2") format("woff2");
}
:root {
/* Colors */
--background: hsl(0 0% 100%);
--background-alt: hsl(0 0% 100%);
--foreground: hsl(0 0% 9%);
--foreground-alt: hsl(0 0% 32%);
--muted: hsl(240 5% 96%);
--muted-foreground: hsla(0 0% 9% / 0.4);
--border: hsl(240 6% 10%);
--border-input: hsla(240 6% 10% / 0.17);
--border-input-hover: hsla(240 6% 10% / 0.4);
--border-card: hsla(240 6% 10% / 0.1);
--dark: hsl(240 6% 10%);
--dark-10: hsla(240 6% 10% / 0.1);
--dark-40: hsla(240 6% 10% / 0.4);
--dark-04: hsla(240 6% 10% / 0.04);
--accent: hsl(204 94% 94%);
--accent-foreground: hsl(204 80% 16%);
--destructive: hsl(347 77% 50%);
--tertiary: hsl(37.7 92.1% 50.2%);
--line: hsl(0 0% 100%);
/* black */
--contrast: hsl(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: hsl(0 0% 5%);
--background-alt: hsl(0 0% 8%);
--foreground: hsl(0 0% 95%);
--foreground-alt: hsl(0 0% 70%);
--muted: hsl(240 4% 16%);
--muted-foreground: hsla(0 0% 100% / 0.4);
--border: hsl(0 0% 96%);
--border-input: hsla(0 0% 96% / 0.17);
--border-input-hover: hsla(0 0% 96% / 0.4);
--border-card: hsla(0 0% 96% / 0.1);
--dark: hsl(0 0% 96%);
--dark-40: hsl(0 0% 96% / 0.4);
--dark-10: hsl(0 0% 96% / 0.1);
--dark-04: hsl(0 0% 96% / 0.04);
--accent: hsl(204 90% 90%);
--accent-foreground: hsl(204 94% 94%);
--destructive: hsl(350 89% 60%);
--line: hsl(0 0% 9.02%);
--tertiary: hsl(61.3 100% 82.2%);
/* white */
--contrast: hsl(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);
}
@theme inline {
--color-background: var(--background);
--color-background-alt: var(--background-alt);
--color-foreground: var(--foreground);
--color-foreground-alt: var(--foreground-alt);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border-card);
--color-border-input: var(--border-input);
--color-border-input-hover: var(--border-input-hover);
--color-border-card: var(--border-card);
--color-dark: var(--dark);
--color-dark-10: var(--dark-10);
--color-dark-40: var(--dark-40);
--color-dark-04: var(--dark-04);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-tertiary: var(--tertiary);
--color-line: var(--line);
--color-contrast: var(--contrast);
--shadow-mini: var(--shadow-mini);
--shadow-mini-inset: var(--shadow-mini-inset);
--shadow-popover: var(--shadow-popover);
--shadow-kbd: var(--shadow-kbd);
--shadow-btn: var(--shadow-btn);
--shadow-card: var(--shadow-card);
--shadow-date-field-focus: var(--shadow-date-field-focus);
--text-xxs: 10px;
--radius-card: 16px;
--radius-card-lg: 20px;
--radius-card-sm: 10px;
--radius-input: 9px;
--radius-button: 5px;
--radius-5px: 5px;
--radius-9px: 9px;
--radius-10px: 10px;
--radius-15px: 15px;
--spacing-input: 3rem;
--spacing-input-sm: 2.5rem;
--breakpoint-desktop: 1440px;
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-caret-blink: caret-blink 1s ease-out infinite;
--animate-scale-in: scale-in 0.2s ease;
--animate-scale-out: scale-out 0.15s ease;
--animate-fade-in: fade-in 0.2s ease;
--animate-fade-out: fade-out 0.15s ease;
--animate-enter-from-left: enter-from-left 0.2s ease;
--animate-enter-from-right: enter-from-right 0.2s ease;
--animate-exit-to-left: exit-to-left 0.2s ease;
--animate-exit-to-right: exit-to-right 0.2s ease;
--font-sans: "Inter", "sans-serif";
--font-mono: "Source Code Pro", "monospace";
--font-alt: "Courier", "sans-serif";
--font-display: "Cal Sans", "sans-serif";
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--bits-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--bits-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
@keyframes enter-from-right {
from {
opacity: 0;
transform: translateX(200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes enter-from-left {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes exit-to-right {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes exit-to-left {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: rotateX(-10deg) scale(0.9);
}
to {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
}
@keyframes scale-out {
from {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
to {
opacity: 0;
transform: rotateX(-10deg) scale(0.95);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-border-card, currentColor);
}
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
scrollbar-color: var(--bg-muted);
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
::selection {
background: #fdffa4;
color: black;
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:ring-foreground focus-visible:ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1;
}
}
.link {
@apply hover:text-foreground/80 focus-visible:ring-foreground focus-visible:ring-offset-background rounded-xs focus-visible:outline-hidden inline-flex items-center gap-1 font-medium underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-offset-2;
}
}
Checkout Steps
Use the Accordion component to create a multi-step checkout process.
Order Summary
<script lang="ts">
import cn from "clsx";
import { Accordion, useId, Button } from "bits-ui";
import CaretDown from "phosphor-svelte/lib/CaretDown";
import { SvelteSet } from "svelte/reactivity";
let activeStep = $state("");
let completedSteps = new SvelteSet<string>();
</script>
{#snippet MyAccordionHeader({ value, title }: { value: string; title: string })}
{@const isCompleted = completedSteps.has(value)}
<Accordion.Header>
<Accordion.Trigger
class="flex w-full flex-1 select-none items-center justify-between gap-3 py-5 text-[15px] font-medium transition-all [&[data-state=open]>span>svg]:rotate-180"
>
<div
class={cn(
"border-foreground/30 flex size-6 shrink-0 items-center justify-center rounded-full border text-xs font-medium",
isCompleted ? "text-foreground" : "text-muted-foreground"
)}
>
{isCompleted ? "✓" : value}
</div>
<span class="w-full text-left">
{title}
</span>
<span
class="hover:bg-dark-10 inline-flex size-8 items-center justify-center rounded-[7px] bg-transparent"
>
<CaretDown class="size-[18px] transition-transform duration-200" />
</span>
</Accordion.Trigger>
</Accordion.Header>
{/snippet}
{#snippet InputField({
label,
placeholder,
type = "text"
}: {
label: string;
placeholder: string;
type?: string;
})}
{@const id = useId()}
<div class="flex flex-col gap-1">
<label class="select-none text-sm font-medium" for={id}>{label}</label>
<input
{type}
{id}
name={label}
{placeholder}
class="rounded-card-sm border-border-input bg-background placeholder:text-foreground-alt/50 hover:border-dark-40 focus-override inline-flex h-10 w-full items-center border px-4 text-base sm:text-sm"
/>
</div>
{/snippet}
<Accordion.Root
bind:value={activeStep}
class="w-full sm:max-w-[70%]"
type="single"
>
<Accordion.Item value="1" class="border-dark-10 group border-b px-1.5">
{@render MyAccordionHeader({ title: "Shipping Information", value: "1" })}
<Accordion.Content
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm tracking-[-0.01em]"
>
<div class="flex flex-col gap-4 pb-6 pt-2">
<div class="grid grid-cols-2 gap-4">
{@render InputField({ label: "First Name", placeholder: "John" })}
{@render InputField({ label: "Last Name", placeholder: "Doe" })}
<div class="col-span-2">
{@render InputField({
label: "Address",
placeholder: "1234 Elm Street"
})}
</div>
{@render InputField({ label: "City", placeholder: "Tampa" })}
{@render InputField({ label: "ZIP", placeholder: "123456" })}
</div>
<div class="pt-2">
<Button.Root
class="rounded-input bg-dark text-background shadow-mini hover:bg-dark/95 inline-flex h-10 select-none items-center justify-center whitespace-nowrap px-[21px] text-sm font-medium transition-all hover:cursor-pointer active:scale-[0.98]"
onclick={() => {
completedSteps.add("1");
activeStep = "2";
}}
>
Continue to Payment
</Button.Root>
</div>
</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="2" class="border-dark-10 group border-b px-1.5">
{@render MyAccordionHeader({ title: "Payment Method", value: "2" })}
<Accordion.Content
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm tracking-[-0.01em]"
>
<div class="flex flex-col gap-4 pb-6 pt-2">
<div class="grid grid-cols-3 gap-4">
<div class="col-span-3">
{@render InputField({
label: "Card Number",
placeholder: "4242 4242 4242 4242"
})}
</div>
{@render InputField({ label: "Exp. Month", placeholder: "MM" })}
{@render InputField({ label: "Exp. Year", placeholder: "YY" })}
{@render InputField({ label: "CVC", placeholder: "123" })}
</div>
<div class="pt-2">
<Button.Root
class="rounded-input bg-dark text-background shadow-mini hover:bg-dark/95 inline-flex h-10 select-none items-center justify-center whitespace-nowrap px-[21px] text-sm font-medium transition-all hover:cursor-pointer active:scale-[0.98]"
onclick={() => {
completedSteps.add("2");
activeStep = "3";
}}
>
Continue to Review
</Button.Root>
</div>
</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="3" class="border-dark-10 group border-b px-1.5">
{@render MyAccordionHeader({ title: "Payment Method", value: "3" })}
<Accordion.Content
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden pb-6 text-sm tracking-[-0.01em]"
>
<div class="flex flex-col gap-4 pt-2">
<div class="rounded-lg border p-4">
<h4 class="mb-2 font-medium">Order Summary</h4>
<div class="flex flex-col gap-2">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">Product 1</span>
<span>$29.99</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">Product 2</span>
<span>$49.99</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">Shipping</span>
<span>$4.99</span>
</div>
<div class="mt-2 flex justify-between border-t pt-2 font-medium">
<span>Total</span>
<span>$84.97</span>
</div>
</div>
</div>
<div class="pt-2">
<Button.Root
class="rounded-input bg-dark text-background shadow-mini hover:bg-dark/95 inline-flex h-10 select-none items-center justify-center whitespace-nowrap px-[21px] text-sm font-medium transition-all hover:cursor-pointer active:scale-[0.98]"
onclick={() => {
completedSteps.add("3");
activeStep = "";
}}
>
Place Order
</Button.Root>
</div>
</div>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
@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");
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@font-face {
font-family: "Cal Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/CalSans-SemiBold.woff2") format("woff2");
}
:root {
/* Colors */
--background: hsl(0 0% 100%);
--background-alt: hsl(0 0% 100%);
--foreground: hsl(0 0% 9%);
--foreground-alt: hsl(0 0% 32%);
--muted: hsl(240 5% 96%);
--muted-foreground: hsla(0 0% 9% / 0.4);
--border: hsl(240 6% 10%);
--border-input: hsla(240 6% 10% / 0.17);
--border-input-hover: hsla(240 6% 10% / 0.4);
--border-card: hsla(240 6% 10% / 0.1);
--dark: hsl(240 6% 10%);
--dark-10: hsla(240 6% 10% / 0.1);
--dark-40: hsla(240 6% 10% / 0.4);
--dark-04: hsla(240 6% 10% / 0.04);
--accent: hsl(204 94% 94%);
--accent-foreground: hsl(204 80% 16%);
--destructive: hsl(347 77% 50%);
--tertiary: hsl(37.7 92.1% 50.2%);
--line: hsl(0 0% 100%);
/* black */
--contrast: hsl(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: hsl(0 0% 5%);
--background-alt: hsl(0 0% 8%);
--foreground: hsl(0 0% 95%);
--foreground-alt: hsl(0 0% 70%);
--muted: hsl(240 4% 16%);
--muted-foreground: hsla(0 0% 100% / 0.4);
--border: hsl(0 0% 96%);
--border-input: hsla(0 0% 96% / 0.17);
--border-input-hover: hsla(0 0% 96% / 0.4);
--border-card: hsla(0 0% 96% / 0.1);
--dark: hsl(0 0% 96%);
--dark-40: hsl(0 0% 96% / 0.4);
--dark-10: hsl(0 0% 96% / 0.1);
--dark-04: hsl(0 0% 96% / 0.04);
--accent: hsl(204 90% 90%);
--accent-foreground: hsl(204 94% 94%);
--destructive: hsl(350 89% 60%);
--line: hsl(0 0% 9.02%);
--tertiary: hsl(61.3 100% 82.2%);
/* white */
--contrast: hsl(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);
}
@theme inline {
--color-background: var(--background);
--color-background-alt: var(--background-alt);
--color-foreground: var(--foreground);
--color-foreground-alt: var(--foreground-alt);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border-card);
--color-border-input: var(--border-input);
--color-border-input-hover: var(--border-input-hover);
--color-border-card: var(--border-card);
--color-dark: var(--dark);
--color-dark-10: var(--dark-10);
--color-dark-40: var(--dark-40);
--color-dark-04: var(--dark-04);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-tertiary: var(--tertiary);
--color-line: var(--line);
--color-contrast: var(--contrast);
--shadow-mini: var(--shadow-mini);
--shadow-mini-inset: var(--shadow-mini-inset);
--shadow-popover: var(--shadow-popover);
--shadow-kbd: var(--shadow-kbd);
--shadow-btn: var(--shadow-btn);
--shadow-card: var(--shadow-card);
--shadow-date-field-focus: var(--shadow-date-field-focus);
--text-xxs: 10px;
--radius-card: 16px;
--radius-card-lg: 20px;
--radius-card-sm: 10px;
--radius-input: 9px;
--radius-button: 5px;
--radius-5px: 5px;
--radius-9px: 9px;
--radius-10px: 10px;
--radius-15px: 15px;
--spacing-input: 3rem;
--spacing-input-sm: 2.5rem;
--breakpoint-desktop: 1440px;
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-caret-blink: caret-blink 1s ease-out infinite;
--animate-scale-in: scale-in 0.2s ease;
--animate-scale-out: scale-out 0.15s ease;
--animate-fade-in: fade-in 0.2s ease;
--animate-fade-out: fade-out 0.15s ease;
--animate-enter-from-left: enter-from-left 0.2s ease;
--animate-enter-from-right: enter-from-right 0.2s ease;
--animate-exit-to-left: exit-to-left 0.2s ease;
--animate-exit-to-right: exit-to-right 0.2s ease;
--font-sans: "Inter", "sans-serif";
--font-mono: "Source Code Pro", "monospace";
--font-alt: "Courier", "sans-serif";
--font-display: "Cal Sans", "sans-serif";
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--bits-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--bits-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
@keyframes enter-from-right {
from {
opacity: 0;
transform: translateX(200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes enter-from-left {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes exit-to-right {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes exit-to-left {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: rotateX(-10deg) scale(0.9);
}
to {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
}
@keyframes scale-out {
from {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
to {
opacity: 0;
transform: rotateX(-10deg) scale(0.95);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-border-card, currentColor);
}
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
scrollbar-color: var(--bg-muted);
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
::selection {
background: #fdffa4;
color: black;
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:ring-foreground focus-visible:ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1;
}
}
.link {
@apply hover:text-foreground/80 focus-visible:ring-foreground focus-visible:ring-offset-background rounded-xs focus-visible:outline-hidden inline-flex items-center gap-1 font-medium underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-offset-2;
}
}
API Reference
The root accordion component used to set and manage the state of the accordion.
Property | Type | Description |
---|---|---|
type required | enum | The type of accordion. If set to Default: undefined |
value $bindable | union | The value of the currently active accordion item. If Default: undefined |
onValueChange | function | A callback function called when the active accordion item value changes. If the Default: undefined |
disabled | boolean | Whether or not the accordion is disabled. When disabled, the accordion cannot be interacted with. Default: false |
loop | boolean | Whether or not the accordion should loop through items when reaching the end. Default: false |
orientation | enum | The orientation of the accordion. Default: vertical |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-orientation | enum | The orientation of the component. |
data-disabled | '' | Present when the component is disabled. |
data-accordion-root | '' | Present on the root element. |
An accordion item.
Property | Type | Description |
---|---|---|
disabled | boolean | Whether or not the accordion item is disabled. Default: false |
value | string | The value of the accordion item. This is used to identify when the item is open or closed. If not provided, a unique ID will be generated for this value. Default: A random unique ID |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-state | enum | Whether the accordion item is open or closed. |
data-disabled | '' | Present when the component is disabled. |
data-orientation | enum | The orientation of the component. |
data-accordion-item | '' | Present on the item element. |
The header of the accordion item.
Property | Type | Description |
---|---|---|
level | union | The heading level of the header. This will be set as the Default: 3 |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-orientation | enum | The orientation of the component. |
data-disabled | '' | Present when the component is disabled. |
data-heading-level | enum | The heading level of the element. |
data-accordion-header | '' | Present on the header element. |
The button responsible for toggling the accordion item.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLButtonElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-orientation | enum | The orientation of the component. |
data-disabled | '' | Present when the component is disabled. |
data-accordion-trigger | '' | Present on the trigger element. |
The accordion item content, which is displayed when the item is open.
Property | Type | Description |
---|---|---|
forceMount | boolean | Whether or not to forcefully mount the content. This is useful if you want to use Svelte transitions or another animation library for the content. Default: false |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-orientation | enum | The orientation of the component. |
data-disabled | '' | Present when the component is disabled. |
data-accordion-content | '' | Present on the content element. |
CSS Variable | Description |
---|---|
--bits-accordion-content-height | The height of the accordion content element. |
--bits-accordion-content-width | The width of the accordion content element. |