React-Bootstrap Accordion: Choosing Between defaultActiveKey and activeKey for Dynamic FAQs
React-Bootstrap's Accordion offers two state models. Choose defaultActiveKey for static lists and activeKey when FAQs must sync with URLs or external filters.
26 Aug 2025, 03:04 UTC

You are rendering an FAQ list from a CMS. The first question should be open by default, a “Share” button needs to deep-link into a specific question, and a sidebar filter must be able to close every panel at once. You drop in React-Bootstrap’s Accordion, set defaultActiveKey="0", and the first panel opens. Then the sidebar filter fires, the URL hash changes, and nothing happens. The component ignores you.
The useful takeaway: defaultActiveKey is a mount-time seed, not a live binding. If any part of your application outside the Accordion needs to decide which panel is open, you must switch to a controlled activeKey prop and own that state yourself.
defaultActiveKey is a starting gun, not a steering wheel
When you pass defaultActiveKey, React-Bootstrap’s Accordion manages its own open/close state internally. The prop is read once during the initial render. After that, the component tracks clicks on its own. This is convenient for a static list of questions that never changes and never needs to respond to routing or global search.
The limitation appears the moment your FAQ content becomes dynamic. If the URL hash changes to #shipping, or a parent component decides to collapse everything, the uncontrolled Accordion has no way to hear about it. You are left reaching into the DOM or remounting the component, both of which are fragile.
activeKey plus onSelect is a contract
Controlled mode inverts the relationship. You pass activeKey to tell the Accordion which panel is open, and you pass an onSelect handler so the component can report user clicks back to you. The Accordion becomes a pure view: it renders what you tell it and asks before changing anything.
This gives you a single source of truth. The open panel can live in React state, a URL parameter, or a global store, and every part of your UI can read from the same place. The cost is boilerplate: you must manage the state and write the handler.
One caution from the React-Bootstrap API: if you supply activeKey but omit onSelect, the component will freeze in its initial state. React-Bootstrap will not update the prop for you, and the accordion will stop responding to clicks.
If you are embedding the Accordion inside a card or sidebar, the flush prop removes the default borders and rounded corners so the component blends into the surrounding container.
A URL-synced FAQ accordion
The pattern below keeps the open panel in React state and mirrors it to the URL hash. It assumes React-Bootstrap v2.x with Bootstrap 5; confirm your installed version before copying the prop names.
import { Accordion } from 'react-bootstrap';
import { useState, useEffect } from 'react';
function FaqAccordion({ items }) {
const [activeKey, setActiveKey] = useState(null);
// Read the URL hash once on mount
useEffect(() => {
const fromHash = window.location.hash.replace('#', '');
if (fromHash) setActiveKey(fromHash);
}, []);
const handleSelect = (key) => {
const next = key === activeKey ? null : key;
setActiveKey(next);
if (next) {
window.location.hash = next;
}
};
return (
<Accordion activeKey={activeKey} onSelect={handleSelect} flush>
{items.map((item) => (
<Accordion.Item eventKey={item.id} key={item.id}>
<Accordion.Header>{item.question}</Accordion.Header>
<Accordion.Body>{item.answer}</Accordion.Body>
</Accordion.Item>
))}
</Accordion>
);
}
Notice that item.id is used directly as eventKey. React-Bootstrap accepts strings or numbers for eventKey, but the value stored in activeKey must match the type exactly. A URL hash is always a string, so if your CMS returns numeric IDs, coerce them before comparing. A type mismatch will not throw an error; the panel simply will not expand.
What the DOM gives you for free
React-Bootstrap renders each Accordion header as a semantic <button> inside an <h2> element by default. That structure preserves keyboard navigation and screen-reader announcements without extra ARIA wiring. You can tab to a header, press Enter or Space to toggle it, and the assistive technology will announce the expanded state.
To verify this in your own build, open browser DevTools and inspect a header button. You should see aria-expanded reflecting the current state and aria-controls pointing to the ID of the collapsible panel. If those attributes are missing, you may be on an older major version or a custom header override.
Before trusting any prop name, check the installed version in your project root:
npm ls react-bootstrap
Expected check: a single resolved version line, for example react-bootstrap@2.x.y. If the output shows UNMET DEPENDENCY or multiple conflicting versions, your local type definitions may not match the runtime code, and the Accordion API could differ from the examples above.
Trade-offs and version drift
Controlled mode is the right choice when the open panel is shared state. It is the wrong choice for a read-only FAQ that never interacts with routing, because you are writing state management code for no benefit. Uncontrolled mode is simpler but isolated.
React-Bootstrap’s Accordion behavior and prop names have evolved across major Bootstrap versions. The flush prop and the compound Accordion.Item pattern described here assume a v2.x line. If your lockfile shows v1.x or an older v0.x release, verify the exact API against your local type definitions before refactoring.
Do not mix controlled and uncontrolled props on the same instance. Passing both defaultActiveKey and activeKey will trigger a React warning and produce unpredictable behavior.
How to decide and verify
- If the FAQ is static and self-contained, use
defaultActiveKeyand move on. - If the open panel must respond to a URL, a filter, or a parent component, use
activeKeywith anonSelecthandler. - Ensure every
eventKeyand the value inactiveKeyshare the same JavaScript type. - Run
npm ls react-bootstrapand confirm the version supports the props you are using. - Inspect the rendered DOM for
aria-expandedandaria-controlson the header buttons.
Start with the state model that matches where the truth lives. If the truth lives outside the Accordion, the Accordion should not own it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.