COMPONENTS/DATA DISPLAY
Accordion.
A little less, until you need more.
In practice
Warm neutrals, precise details and components you own.
Absolutely. Every component comes with its source code.
Yes. Every color is backed by a light and dark theme token.
Compose Item, Header, Trigger and Content. Supports multiple expansion and arrow-key navigation.
Installation
Install Coal once. Components and TypeScript declarations are included. React 19 and React DOM 19 are peer dependencies.
Terminal · local package
npm install http://localhost:3100/downloads/chlohal-coal-ui-0.5.0.tgzThis installs the local package. It is not yet published on npm. See installation for stylesheet setup and integration.
Usage
example.tsx
"use client";
import * as React from "react";
import "@chlohal/coal-ui/styles.css";
import { Plus } from "lucide-react";
import { Accordion, AccordionItem, AccordionHeader, AccordionTrigger, AccordionContent } from "@chlohal/coal-ui";
export default function Example() {
return (<Accordion>
{[
[
"What makes coal different?",
"Warm neutrals, precise details and components you own.",
],
[
"Can I make it my own?",
"Absolutely. Every component comes with its source code.",
],
[
"Does it support dark mode?",
"Yes. Every color is backed by a light and dark theme token.",
],
].map(([q, a]) => (<AccordionItem key={q}>
<AccordionHeader>
<AccordionTrigger>
{q}
<Plus size={14}/>
</AccordionTrigger>
</AccordionHeader>
<AccordionContent>{a}</AccordionContent>
</AccordionItem>))}
</Accordion>);
}
Source
The original Coal implementation. Use the package export to integrate it.
packages/react/src/accordion.tsx
"use client";
import * as React from "react";
import {
Action,
cn,
useRequired,
useValue,
moveFocus,
type ActionProps,
} from "./internal.js";
const Root = React.createContext<
| { values: string[]; set: (v: string[]) => void; multiple: boolean }
| undefined
>(undefined);
const Item = React.createContext<
| { open: boolean; toggle: () => void; id: string; disabled?: boolean }
| undefined
>(undefined);
export type AccordionProps = Omit<
React.ComponentPropsWithRef<"div">,
"defaultValue"
> & {
value?: string[];
defaultValue?: string[];
onValueChange?: (value: string[]) => void;
multiple?: boolean;
};
export function Accordion({
value,
defaultValue = [],
onValueChange,
multiple = false,
children,
className,
onKeyDown,
...props
}: AccordionProps) {
const [values, set] = useValue(value, defaultValue, onValueChange);
return (
<Root.Provider value={{ values, set, multiple }}>
<div
className={cn("coal-accordion", className)}
{...props}
onKeyDown={(e) => {
onKeyDown?.(e);
if (!e.defaultPrevented)
moveFocus(e, "[data-coal-accordion-trigger]");
}}
>
{children}
</div>
</Root.Provider>
);
}
export function AccordionItem({
value,
disabled,
children,
className,
...props
}: React.ComponentPropsWithRef<"div"> & {
value?: string;
disabled?: boolean;
}) {
const r = useRequired(Root, "AccordionItem");
const id = React.useId();
const key = value ?? id;
const open = r.values.includes(key);
return (
<Item.Provider
value={{
id,
open,
disabled,
toggle: () =>
r.set(
open
? r.values.filter((v) => v !== key)
: r.multiple
? [...r.values, key]
: [key],
),
}}
>
<div className={cn("coal-accordion-item", className)} {...props}>
{children}
</div>
</Item.Provider>
);
}
export function AccordionHeader(props: React.ComponentPropsWithRef<"h3">) {
return <h3 {...props} />;
}
export function AccordionTrigger({
className,
onClick,
...props
}: ActionProps) {
const c = useRequired(Item, "AccordionTrigger");
return (
<Action
{...props}
id={`${c.id}-trigger`}
data-coal-accordion-trigger=""
disabled={c.disabled || props.disabled}
aria-expanded={c.open}
aria-controls={`${c.id}-panel`}
className={cn("coal-accordion-trigger", className)}
onClick={(e) => {
onClick?.(e);
if (!e.defaultPrevented) c.toggle();
}}
/>
);
}
export function AccordionContent({
className,
...props
}: React.ComponentPropsWithRef<"div">) {
const c = useRequired(Item, "AccordionContent");
return (
<div
{...props}
id={`${c.id}-panel`}
role="region"
aria-labelledby={`${c.id}-trigger`}
hidden={!c.open}
className={cn("coal-accordion-content", className)}
/>
);
}
Accessibility checklist
- Provide an accessible name for controls, including icon-only buttons.
- Check keyboard navigation, visible focus and disabled states in your application.
- Do not rely on color alone to communicate status.