Accordion
When discussing the system design of your accordion component in an interview, focus on the following structured approach to showcase your technical depth and architectural thinking:
1. Component Architecture
-
Class-Based Design:
- Explain why a class was chosen (encapsulation, reusability, method organization).
- Example:
"The class structure allows multiple independent accordion instances without state collisions. Methods likecreateAccordion
anddestroy
encapsulate specific responsibilities."
-
Separation of Concerns:
- Highlight DOM creation (
createAccordionItem
), event handling (addEvent
), and state management (openCurrentAccordion
) as distinct modules. - Example:
"Separating DOM manipulation from business logic ensures maintainability and ease of debugging."
- Highlight DOM creation (
2. State Management
-
Internal State:
- Discuss tracking
currentActiveIndex
to manage expanded/collapsed items. - Example:
"A single source of truth for the active index simplifies state updates and avoids race conditions."
- Discuss tracking
-
Stateless vs. Stateful:
- Justify keeping state internal for simplicity vs. using external state management (e.g., Redux) for complex apps.
- Example:
"For a standalone component, internal state is sufficient. In a larger app, we might lift state to a global store."
3. Event Handling & Performance
-
Event Delegation:
- Explain attaching a single listener to the container vs. individual item listeners.
- Example:
"Using event delegation on the parent container reduces memory overhead and handles dynamically added items."
-
Cleanup:
- Highlight the
destroy
method’s role in removing listeners and preventing memory leaks. - Example:
"Explicit cleanup ensures no orphaned listeners after component removal."
- Highlight the
4. Accessibility (A11y)
-
ARIA Attributes:
- Discuss
aria-expanded
,aria-hidden
, and keyboard navigation. - Example:
"ARIA roles make the component screen-reader friendly, whileEnter
/Arrow
keys ensure keyboard usability."
- Discuss
-
Focus Management:
- Suggest improvements like
focus
traps for nested accordions.
- Suggest improvements like
5. Performance Optimizations
-
DOM Efficiency:
- Use of
DocumentFragment
for batched updates. - Example:
"Batching DOM writes with fragments minimizes reflows, improving render performance."
- Use of
-
CSS vs. JS Animations:
- Propose CSS transitions for smooth expand/collapse (e.g.,
max-height
).
- Propose CSS transitions for smooth expand/collapse (e.g.,
6. Extensibility & Scalability
-
Dynamic Data:
- Discuss reinitializing the accordion when data changes (e.g.,
createAccordion
clears old items).
- Discuss reinitializing the accordion when data changes (e.g.,
-
Nested Accordions:
- Explain recursive rendering or event-bubbling adjustments.
-
Theming & Customization:
- Suggest CSS variables for easy styling overrides.
7. Error Handling & Validation
-
Input Validation:
- Mention constructor checks for
target
anddata
. - Example:
"Validating inputs upfront prevents silent failures and improves debuggability."
- Mention constructor checks for
-
Graceful Degradation:
- Plan for scenarios like empty data (e.g., showing a placeholder).
8. Testing Strategy
-
Unit Tests:
- Validate state changes (e.g.,
currentActiveIndex
updates correctly). - Example:
"Tests would simulate clicks/keyboard events and assert ARIA attribute changes."
- Validate state changes (e.g.,
-
Integration Tests:
- Verify DOM updates and accessibility compliance (e.g., using Jest + Testing Library).
9. Integration with Larger Systems
-
Framework Compatibility:
- Discuss wrapping the vanilla JS component in React/Vue (e.g., using refs and lifecycle methods).
-
API Design:
- Expose methods like
expand(index)
orcollapseAll()
for external control.
- Expose methods like
10. Trade-Offs
-
Vanilla JS vs. Frameworks:
- Pros: No dependencies, smaller bundle.
- Cons: More boilerplate for complex features.
-
CSS Transitions:
- Trade-off: Smooth animations require careful CSS/JS coordination (e.g.,
transitionend
events).
- Trade-off: Smooth animations require careful CSS/JS coordination (e.g.,
Example Discussion Flow
-
Start with Requirements:
"The accordion must be reusable, accessible, and performant. Let’s break down how each part addresses these goals..." -
Highlight Key Decisions:
"Using a class encapsulates logic, while event delegation ensures scalability. Thedestroy
method prevents memory leaks." -
Address Edge Cases:
"For empty data, we could add a fallback UI. Dynamic data would require arefresh
method to re-render items." -
Future-Proofing:
"To support animations, we’d add CSS transitions and JS hooks fortransitionend
. For nested accordions, we’d recursively instantiate child components."
Why This Approach Wins
- Depth: Covers architecture, performance, accessibility, and scalability.
- Clarity: Structured flow from requirements to implementation details.
- Proactiveness: Anticipates edge cases and future needs.
This discussion demonstrates SDE 2/3-level system design skills by balancing practicality with foresight. 🚀
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: plum;
}
.ac-container {
max-width: 100%;
width: 500px;
/* background-color: white; */
display: flex;
flex-direction: column;
gap: 1rem;
}
.ac-container .ac-item {
border-radius: 16px;
padding: 16px;
border: 2px solid black;
}
.ac-container .ac-title {
font-size: 18px;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
}
.ac-container .ac-content {
display: none;
}
.ac-container .ac-content.active {
display: block;
}
</style>
</head>
<body>
<div class="ac-container"></div>
<script src="./index.js"></script>
</body>
</html>
const accordionContainer = document.querySelector(".ac-container");
const data = [
{ title: "hi 1", content: "hello" },
{ title: "hi 2", content: "hello" },
{ title: "hi 3", content: "hello" },
];
class Accordion {
constructor(data, target, onToggle) {
if (!target) throw new Error("Target element is required");
if (!Array.isArray(data)) throw new Error("Data must be an array");
this.data = data;
this.target = target;
this.currentActiveIndex = -1;
this.currentData = {};
this.handleClick = this.handleClick.bind(this);
this.handleKeyup = this.handleKeyup.bind(this);
this.onToggle = onToggle;
this.createAccordion();
this.addEvent();
}
createAccordionItem({ title, content, index }) {
const accordionItem = document.createElement("div");
const accordionTitle = document.createElement("div");
const accordionContent = document.createElement("div");
// item
accordionItem.classList.add("ac-item");
accordionItem.setAttribute("data-index", index);
// title
accordionTitle.classList.add("ac-title");
accordionTitle.innerHTML = title;
accordionTitle.setAttribute("aria-expanded", "false");
accordionTitle.setAttribute("tabindex", "0");
// content
accordionContent.classList.add("ac-content");
accordionContent.innerHTML = content;
accordionContent.setAttribute("aria-hidden", "true");
accordionItem.appendChild(accordionTitle);
accordionItem.appendChild(accordionContent);
return accordionItem;
}
createAccordion() {
this.target.innerHTML = "";
const fragment = document.createDocumentFragment();
this.data.forEach((each, index) => {
fragment.appendChild(this.createAccordionItem({ ...each, index: index }));
});
this.target?.appendChild(fragment);
}
openCurrentAccordion(currentItem) {
this.currentActiveIndex = currentItem.getAttribute("data-index");
this.currentData = this.data[this.currentActiveIndex];
const active = document.querySelector(".ac-content.active");
const currentTitle = currentItem.firstChild;
const currentContent = currentItem.lastElementChild;
if (active && active !== currentContent) {
active.classList.remove("active");
active.setAttribute("aria-hidden", "true");
active.previousElementSibling.setAttribute("aria-expanded", "false");
}
const isActive = currentContent.classList.contains("active");
currentContent.setAttribute("aria-hidden", isActive ? "true" : "false");
currentTitle.setAttribute("aria-expanded", isActive ? "false" : "true");
currentContent.classList.toggle("active");
this.onClicked(this.currentActiveIndex, this.data[this.currentActiveIndex]);
this.onToggle(
this.currentActiveIndex,
!isActive,
this.data[this.currentActiveIndex]
);
}
handleClick(e) {
if (!e.target.matches(".ac-title")) return;
this.openCurrentAccordion(e.target.parentElement);
}
handleKeyup(e) {
if (!e.target.matches(".ac-title")) return;
const keyToCheck = ["Enter", "ArrowDown", "ArrowUp"];
if (keyToCheck.includes(e.key)) {
this.openCurrentAccordion(e.target.parentElement);
}
}
addEvent() {
this.target.addEventListener("click", this.handleClick);
this.target.addEventListener("keyup", this.handleKeyup);
}
destroy() {
this.target.removeEventListener("click", this.handleClick);
this.target.removeEventListener("keyup", this.handleKeyup);
this.target.innerHTML = "";
}
onClicked(currentIndex, currentData) {
return { currentIndex, currentData };
}
}
function onToggle(index, isOpen, data) {
console.log(`Item ${index} ${data.title} is ${isOpen ? "opened" : "closed"}`);
}
new Accordion(data, accordionContainer, onToggle);