shadimage
moduix-react
itemaccordionsource: ok · refreshed

Accordion

moduix React Accordion component.

npx shadcn add "https://moduix.dev/r/react/accordion.json"
free

Interaction

accordion

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
ui-primitives@ark-ui/react

Registry dependencies

@moduix-react/foundation@moduix-react/icons

Files3 · source cached 10/8/2026, 8:49:53 AM

src/components/accordion/Accordion.tsx
'use client';

import {
  Accordion as AccordionPrimitive,
  useAccordion,
  useAccordionContext,
  useAccordionItemContext,
} from '@ark-ui/react/accordion';
import type { HTMLArkProps } from '@ark-ui/react/factory';
import { ark } from '@ark-ui/react/factory';
import { clsx } from 'clsx';
import type { ComponentProps, ComponentRef } from 'react';
import { forwardRef } from 'react';
import { PlusIcon } from '@/lib/moduix/icons/ui';
import styles from './Accordion.module.css';

const Accordion = forwardRef<
  ComponentRef<typeof AccordionPrimitive.Root>,
  ComponentProps<typeof AccordionPrimitive.Root>
>(function Accordion({ className, ...props }, ref) {
  return (
    <AccordionPrimitive.Root
      ref={ref}
      className={clsx(styles.root, className)}
      {...props}
      data-slot="accordion-root"
    />
  );
});

const AccordionRootProvider = forwardRef<
  ComponentRef<typeof AccordionPrimitive.RootProvider>,
  ComponentProps<typeof AccordionPrimitive.RootProvider>
>(function AccordionRootProvider({ className, ...props }, ref) {
  return (
    <AccordionPrimitive.RootProvider
      ref={ref}
      className={clsx(styles.root, className)}
      {...props}
      data-slot="accordion-root-provider"
    />
  );
});

const AccordionItem = forwardRef<
  ComponentRef<typeof AccordionPrimitive.Item>,
  ComponentProps<typeof AccordionPrimitive.Item>
>(function AccordionItem({ className, ...props }, ref) {
  return (
    <AccordionPrimitive.Item
      ref={ref}
      className={clsx(styles.item, className)}
      {...props}
      data-slot="accordion-item"
    />
  );
});

const AccordionItemTrigger = forwardRef<
  ComponentRef<typeof AccordionPrimitive.ItemTrigger>,
  ComponentProps<typeof AccordionPrimitive.ItemTrigger>
>(function AccordionItemTrigger({ className, ...props }, ref) {
  return (
    <AccordionPrimitive.ItemTrigger
      ref={ref}
      className={clsx(styles.itemTrigger, className)}
      {...props}
      data-slot="accordion-item-trigger"
    />
  );
});

const AccordionItemIndicator = forwardRef<
  ComponentRef<typeof AccordionPrimitive.ItemIndicator>,
  ComponentProps<typeof AccordionPrimitive.ItemIndicator>
>(function AccordionItemIndicator({ className, children, ...props }, ref) {
  return (
    <AccordionPrimitive.ItemIndicator
      ref={ref}
      className={clsx(styles.itemIndicator, className)}
      {...props}
      data-slot="accordion-item-indicator"
    >
      {children ?? <PlusIcon />}
    </AccordionPrimitive.ItemIndicator>
  );
});

const AccordionItemContent = forwardRef<
  ComponentRef<typeof AccordionPrimitive.ItemContent>,
  ComponentProps<typeof AccordionPrimitive.ItemContent>
>(function AccordionItemContent({ className, ...props }, ref) {
  return (
    <AccordionPrimitive.ItemContent
      ref={ref}
      className={clsx(styles.itemContent, className)}
      {...props}
      data-slot="accordion-item-content"
    />
  );
});

const AccordionItemBody = forwardRef<ComponentRef<typeof ark.div>, HTMLArkProps<'div'>>(
  function AccordionItemBody({ className, ...props }, ref) {
    return (
      <ark.div
        ref={ref}
        className={clsx(styles.itemBody, className)}
        {...props}
        data-scope="accordion"
        data-part="item-body"
        data-slot="accordion-item-body"
      />
    );
  },
);

const AccordionContext = AccordionPrimitive.Context;
const AccordionItemContext = AccordionPrimitive.ItemContext;

export {
  Accordion,
  AccordionContext,
  AccordionItem,
  AccordionItemBody,
  AccordionItemContent,
  AccordionItemContext,
  AccordionItemIndicator,
  AccordionItemTrigger,
  AccordionRootProvider,
  useAccordion,
  useAccordionContext,
  useAccordionItemContext,
};
src/components/accordion/Accordion.module.css
@layer moduix.components {
  @keyframes accordion-item-content-open {
    from {
      height: 0;
      opacity: var(--moduix-accordion-item-content-closed-opacity, 0.01);
    }

    to {
      height: var(--height);
      opacity: var(--moduix-accordion-item-content-open-opacity, 1);
    }
  }

  @keyframes accordion-item-content-closed {
    from {
      height: var(--height);
      opacity: var(--moduix-accordion-item-content-open-opacity, 1);
    }

    to {
      height: 0;
      opacity: var(--moduix-accordion-item-content-closed-opacity, 0.01);
    }
  }

  @keyframes accordion-item-content-open-horizontal {
    from {
      width: 0;
      opacity: var(--moduix-accordion-item-content-closed-opacity, 0.01);
    }

    to {
      width: var(--width);
      opacity: var(--moduix-accordion-item-content-open-opacity, 1);
    }
  }

  @keyframes accordion-item-content-closed-horizontal {
    from {
      width: var(--width);
      opacity: var(--moduix-accordion-item-content-open-opacity, 1);
    }

    to {
      width: 0;
      opacity: var(--moduix-accordion-item-content-closed-opacity, 0.01);
    }
  }

  .root {
    box-sizing: border-box;
    display: flex;
    width: var(--moduix-accordion-width, 100%);
    max-width: var(--moduix-accordion-max-width, 100%);
    min-width: 0;
    flex-direction: column;
    color: var(--moduix-accordion-color, var(--moduix-color-foreground));

    &[data-orientation='horizontal'] {
      width: var(--moduix-accordion-horizontal-width, auto);
      height: var(--moduix-accordion-horizontal-height, 20rem);
      max-height: var(--moduix-accordion-horizontal-max-height, 100%);
      flex-direction: row;
    }
  }

  .item {
    overflow-anchor: none;

    &[data-orientation='vertical']:not(:last-child) {
      border-bottom: var(--moduix-accordion-item-border-width, var(--moduix-border-width-sm)) solid
        var(--moduix-accordion-item-border-color, var(--moduix-color-border));
    }

    &[data-orientation='horizontal'] {
      display: flex;

      &:not(:last-child) {
        border-inline-end: var(--moduix-accordion-item-border-width, var(--moduix-border-width-sm))
          solid var(--moduix-accordion-item-border-color, var(--moduix-color-border));
      }
    }
  }

  .itemTrigger {
    box-sizing: border-box;
    display: flex;
    width: 100%;
    align-items: center;
    justify-content: space-between;
    gap: var(--moduix-accordion-trigger-gap, var(--moduix-spacing-4));
    margin: 0;
    padding-block: var(--moduix-accordion-trigger-padding-y, var(--moduix-spacing-2));
    padding-inline: var(--moduix-accordion-trigger-padding-x, var(--moduix-spacing-3));
    border: 0;
    background-color: var(--moduix-accordion-trigger-bg, var(--moduix-color-muted));
    color: inherit;
    font: inherit;
    font-size: var(--moduix-accordion-trigger-font-size, var(--moduix-text-md));
    line-height: var(--moduix-accordion-trigger-line-height, var(--moduix-line-height-text-md));
    text-align: start;
    user-select: none;
    outline: 0;
    cursor: pointer;
    transition:
      background-color var(--moduix-transition-default),
      color var(--moduix-transition-default);

    &:not(:is([disabled], [data-disabled])) {
      @media (hover: hover) {
        &:hover {
          background-color: var(--moduix-accordion-trigger-bg-hover, var(--moduix-color-accent));
        }
      }

      &:active {
        background-color: var(
          --moduix-accordion-trigger-bg-active,
          var(--moduix-accordion-trigger-bg-hover, var(--moduix-color-accent))
        );
      }
    }

    &:focus-visible {
      position: relative;
      z-index: 1;
      outline: var(
          --moduix-accordion-focus-ring-width,
          var(--moduix-focus-ring-width, var(--moduix-border-width-md))
        )
        solid var(--moduix-accordion-focus-ring-color, var(--moduix-color-ring));
      outline-offset: calc(
        var(--moduix-accordion-focus-ring-offset, var(--moduix-border-width-sm)) * -1
      );
    }

    &:is([disabled], [data-disabled]) {
      cursor: default;
      opacity: var(--moduix-accordion-disabled-opacity, var(--moduix-opacity-disabled));
    }

    &[data-orientation='horizontal'] {
      width: var(--moduix-accordion-horizontal-trigger-width, 2.5rem);
      height: 100%;
      justify-content: center;
      writing-mode: sideways-lr;
    }
  }

  .itemIndicator {
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--moduix-accordion-icon-size, var(--moduix-spacing-3));
    height: var(--moduix-accordion-icon-size, var(--moduix-spacing-3));
    flex-shrink: 0;
    transition: transform var(--moduix-accordion-icon-transition, var(--moduix-transition-default));

    & svg {
      width: 100%;
      height: 100%;
    }

    &[data-state='open'] {
      transform: var(--moduix-accordion-icon-open-transform, rotate(45deg) scale(1.1));
    }
  }

  .itemContent {
    box-sizing: border-box;
    overflow: hidden;
    color: var(--moduix-accordion-item-content-color, var(--moduix-color-muted-foreground));
    font-size: var(--moduix-accordion-item-content-font-size, var(--moduix-text-md));
    line-height: var(
      --moduix-accordion-item-content-line-height,
      var(--moduix-line-height-text-md)
    );

    &[hidden]:not([hidden='until-found']) {
      display: none;
    }

    &[data-state='open'][data-orientation='vertical'] {
      animation: accordion-item-content-open
        var(--moduix-accordion-item-content-transition, var(--moduix-transition-default));
    }

    &[data-state='closed'][data-orientation='vertical'] {
      animation: accordion-item-content-closed
        var(--moduix-accordion-item-content-transition, var(--moduix-transition-default));
    }

    &[data-state='open'][data-orientation='horizontal'] {
      animation: accordion-item-content-open-horizontal
        var(--moduix-accordion-item-content-transition, var(--moduix-transition-default));
    }

    &[data-state='closed'][data-orientation='horizontal'] {
      animation: accordion-item-content-closed-horizontal
        var(--moduix-accordion-item-content-transition, var(--moduix-transition-default));
    }

    &[data-orientation='horizontal'] > * {
      width: var(--moduix-accordion-horizontal-content-width, 16rem);
    }
  }

  .itemBody {
    display: grid;
    gap: var(--moduix-accordion-item-body-gap, var(--moduix-spacing-3));
    min-width: 0;
    padding: var(--moduix-accordion-item-body-padding, var(--moduix-spacing-3));
  }

  @media (prefers-reduced-motion: reduce) {
    .itemTrigger,
    .itemIndicator {
      transition: none;
    }

    .itemContent[data-state][data-orientation] {
      animation-duration: 1ms;
      animation-delay: 0ms;
    }
  }
}
src/components/accordion/index.ts
export * from './Accordion.js';

More from moduix-react

Browse all components in this library →