Vanilla JS Components for Frontend Interviews
Build production-quality UI components from scratch using only HTML, CSS, and vanilla JavaScript — image carousel, modal dialog, star rating, and more. Interview-ready implementations with accessibility.
Vanilla JS Components for Frontend Interviews
Frontend interviews love asking you to build UI components from scratch — no React, no frameworks, just vanilla HTML, CSS, and JavaScript. This post covers the most commonly asked components with clean, interview-ready implementations.
Each component follows these principles:
- Accessible — keyboard navigation, ARIA attributes, screen reader support
- Clean separation — HTML structure, CSS styling, JS behavior
- Edge cases handled — the details interviewers look for
1. Image Carousel / Slideshow
A classic interview question. Build a carousel that cycles through images with previous/next buttons, dot indicators, and keyboard support.
What Interviewers Look For
- Handling wrap-around (last slide → first slide)
- Keyboard accessibility (arrow keys)
- Smooth transitions
- Auto-play with pause on hover (bonus)
HTML
<div class="carousel" role="region" aria-label="Image carousel" aria-roledescription="carousel">
<div class="carousel-viewport">
<div class="carousel-track">
<div class="carousel-slide active" role="group" aria-roledescription="slide" aria-label="Slide 1 of 4">
<img src="https://picsum.photos/id/10/600/300" alt="Mountain landscape" />
</div>
<div class="carousel-slide" role="group" aria-roledescription="slide" aria-label="Slide 2 of 4">
<img src="https://picsum.photos/id/20/600/300" alt="Ocean view" />
</div>
<div class="carousel-slide" role="group" aria-roledescription="slide" aria-label="Slide 3 of 4">
<img src="https://picsum.photos/id/30/600/300" alt="Forest path" />
</div>
<div class="carousel-slide" role="group" aria-roledescription="slide" aria-label="Slide 4 of 4">
<img src="https://picsum.photos/id/40/600/300" alt="City skyline" />
</div>
</div>
</div>
<button class="carousel-btn carousel-btn--prev" aria-label="Previous slide">←</button>
<button class="carousel-btn carousel-btn--next" aria-label="Next slide">→</button>
<div class="carousel-dots" role="tablist" aria-label="Slide navigation">
<button class="carousel-dot active" role="tab" aria-selected="true" aria-label="Go to slide 1"></button>
<button class="carousel-dot" role="tab" aria-selected="false" aria-label="Go to slide 2"></button>
<button class="carousel-dot" role="tab" aria-selected="false" aria-label="Go to slide 3"></button>
<button class="carousel-dot" role="tab" aria-selected="false" aria-label="Go to slide 4"></button>
</div>
</div>CSS
.carousel {
position: relative;
max-width: 600px;
margin: 0 auto;
overflow: hidden;
border-radius: 8px;
}
.carousel-viewport {
overflow: hidden;
}
.carousel-track {
display: flex;
transition: transform 0.4s ease-in-out;
}
.carousel-slide {
min-width: 100%;
flex-shrink: 0;
}
.carousel-slide img {
width: 100%;
height: 300px;
object-fit: cover;
display: block;
}
/* Navigation buttons */
.carousel-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.5);
color: white;
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.carousel-btn:hover {
background: rgba(0, 0, 0, 0.8);
}
.carousel-btn--prev { left: 10px; }
.carousel-btn--next { right: 10px; }
/* Dot indicators */
.carousel-dots {
display: flex;
justify-content: center;
gap: 8px;
padding: 12px 0;
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
.carousel-dot {
width: 10px;
height: 10px;
border-radius: 50%;
border: 2px solid white;
background: transparent;
cursor: pointer;
padding: 0;
}
.carousel-dot.active {
background: white;
}JavaScript
class Carousel {
constructor(element) {
this.el = element;
this.track = element.querySelector('.carousel-track');
this.slides = [...element.querySelectorAll('.carousel-slide')];
this.dots = [...element.querySelectorAll('.carousel-dot')];
this.prevBtn = element.querySelector('.carousel-btn--prev');
this.nextBtn = element.querySelector('.carousel-btn--next');
this.currentIndex = 0;
this.totalSlides = this.slides.length;
this.autoPlayInterval = null;
this.bindEvents();
this.startAutoPlay();
}
bindEvents() {
this.prevBtn.addEventListener('click', () => this.goTo(this.currentIndex - 1));
this.nextBtn.addEventListener('click', () => this.goTo(this.currentIndex + 1));
this.dots.forEach((dot, i) => {
dot.addEventListener('click', () => this.goTo(i));
});
// Keyboard navigation
this.el.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') this.goTo(this.currentIndex - 1);
if (e.key === 'ArrowRight') this.goTo(this.currentIndex + 1);
});
// Pause auto-play on hover
this.el.addEventListener('mouseenter', () => this.stopAutoPlay());
this.el.addEventListener('mouseleave', () => this.startAutoPlay());
// Pause on focus for accessibility
this.el.addEventListener('focusin', () => this.stopAutoPlay());
this.el.addEventListener('focusout', () => this.startAutoPlay());
}
goTo(index) {
// Wrap around
if (index < 0) index = this.totalSlides - 1;
if (index >= this.totalSlides) index = 0;
this.currentIndex = index;
this.track.style.transform = `translateX(-$\{index * 100\}%)`;
// Update active states
this.slides.forEach((slide, i) => {
slide.classList.toggle('active', i === index);
slide.setAttribute('aria-label', `Slide $\{i + 1\} of $\{this.totalSlides\}`);
});
this.dots.forEach((dot, i) => {
dot.classList.toggle('active', i === index);
dot.setAttribute('aria-selected', i === index);
});
}
startAutoPlay(interval = 4000) {
this.stopAutoPlay();
this.autoPlayInterval = setInterval(() => {
this.goTo(this.currentIndex + 1);
}, interval);
}
stopAutoPlay() {
if (this.autoPlayInterval) {
clearInterval(this.autoPlayInterval);
this.autoPlayInterval = null;
}
}
}
// Initialize
const carousel = new Carousel(document.querySelector('.carousel'));Key Interview Points
- Wrap-around logic:
index < 0goes to last slide,index >= totalgoes to first - CSS transform vs. display toggle: Transform with
translateXis more performant — it triggers GPU compositing instead of layout recalculations - Auto-play pause: Must pause on both hover and keyboard focus (accessibility)
- ARIA roles:
aria-roledescription="carousel"andaria-roledescription="slide"give screen readers proper context
2. Modal Dialog
Modals are deceptively complex. The tricky parts are focus trapping, scroll locking, and closing behavior.
What Interviewers Look For
- Focus trap (Tab cycles within modal only)
- Escape key closes modal
- Clicking backdrop closes modal
- Scroll lock on body
- Return focus to trigger element on close
HTML
<button id="open-modal" class="btn">Open Modal</button>
<div class="modal-overlay" id="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-hidden="true">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-title">Modal Title</h2>
<button class="modal-close" aria-label="Close modal">×</button>
</div>
<div class="modal-body">
<p>This is the modal content. Tab through the focusable elements — focus stays trapped inside.</p>
<input type="text" placeholder="Try tabbing here..." />
<textarea placeholder="And here..."></textarea>
</div>
<div class="modal-footer">
<button class="btn btn--secondary modal-cancel">Cancel</button>
<button class="btn btn--primary">Confirm</button>
</div>
</div>
</div>CSS
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease, visibility 0.2s ease;
}
.modal-overlay.open {
opacity: 1;
visibility: visible;
}
.modal-content {
background: white;
border-radius: 12px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
transform: scale(0.95) translateY(10px);
transition: transform 0.2s ease;
}
.modal-overlay.open .modal-content {
transform: scale(1) translateY(0);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #e5e7eb;
}
.modal-header h2 {
margin: 0;
font-size: 18px;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
color: #6b7280;
}
.modal-close:hover {
background: #f3f4f6;
color: #111;
}
.modal-body {
padding: 20px 24px;
display: flex;
flex-direction: column;
gap: 12px;
}
.modal-footer {
padding: 16px 24px;
border-top: 1px solid #e5e7eb;
display: flex;
justify-content: flex-end;
gap: 8px;
}
/* Scroll lock */
body.modal-open {
overflow: hidden;
}JavaScript
class Modal {
constructor(overlay) {
this.overlay = overlay;
this.content = overlay.querySelector('.modal-content');
this.closeBtn = overlay.querySelector('.modal-close');
this.cancelBtn = overlay.querySelector('.modal-cancel');
this.triggerElement = null;
this.bindEvents();
}
bindEvents() {
this.closeBtn.addEventListener('click', () => this.close());
if (this.cancelBtn) {
this.cancelBtn.addEventListener('click', () => this.close());
}
// Close on backdrop click (but not content click)
this.overlay.addEventListener('click', (e) => {
if (e.target === this.overlay) this.close();
});
// Close on Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isOpen()) this.close();
});
}
open(triggerElement) {
this.triggerElement = triggerElement;
this.overlay.classList.add('open');
this.overlay.setAttribute('aria-hidden', 'false');
document.body.classList.add('modal-open');
// Focus the first focusable element
const firstFocusable = this.getFocusableElements()[0];
if (firstFocusable) firstFocusable.focus();
// Trap focus
this.overlay.addEventListener('keydown', this.handleTab);
}
close() {
this.overlay.classList.remove('open');
this.overlay.setAttribute('aria-hidden', 'true');
document.body.classList.remove('modal-open');
// Return focus to the trigger element
if (this.triggerElement) {
this.triggerElement.focus();
}
this.overlay.removeEventListener('keydown', this.handleTab);
}
isOpen() {
return this.overlay.classList.contains('open');
}
getFocusableElements() {
const selector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
return [...this.content.querySelectorAll(selector)].filter(
(el) => !el.disabled && el.offsetParent !== null
);
}
handleTab = (e) => {
if (e.key !== 'Tab') return;
const focusable = this.getFocusableElements();
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey) {
// Shift+Tab: if on first element, wrap to last
if (document.activeElement === first) {
e.preventDefault();
last.focus();
}
} else {
// Tab: if on last element, wrap to first
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
};
}
// Initialize
const modal = new Modal(document.getElementById('modal'));
const openBtn = document.getElementById('open-modal');
openBtn.addEventListener('click', () => modal.open(openBtn));Key Interview Points
- Focus trap: The
handleTabmethod wraps focus between first and last focusable elements — this is the most important accessibility detail aria-modal="true": Tells assistive technologies that content behind the modal is inert- Return focus: When the modal closes, focus returns to the button that opened it — essential for keyboard users
- Backdrop click detection:
e.target === this.overlayensures clicks on the content don't close the modal - Scroll lock:
overflow: hiddenon body prevents background scrolling while the modal is open
3. Star Rating
A seemingly simple component with subtle UX details — hover preview, click to select, keyboard support, and half-star support.
What Interviewers Look For
- Hover preview vs. selected state
- Keyboard accessibility (arrow keys to change rating)
- Screen reader support
- Handling half-stars (bonus)
HTML
<div class="star-rating" role="radiogroup" aria-label="Rating">
<button class="star" data-value="1" role="radio" aria-checked="false" aria-label="1 star">
<svg viewBox="0 0 24 24" width="32" height="32">
<polygon points="12,2 15,9 22,9 17,14 18,21 12,17 6,21 7,14 2,9 9,9" />
</svg>
</button>
<button class="star" data-value="2" role="radio" aria-checked="false" aria-label="2 stars">
<svg viewBox="0 0 24 24" width="32" height="32">
<polygon points="12,2 15,9 22,9 17,14 18,21 12,17 6,21 7,14 2,9 9,9" />
</svg>
</button>
<button class="star" data-value="3" role="radio" aria-checked="false" aria-label="3 stars">
<svg viewBox="0 0 24 24" width="32" height="32">
<polygon points="12,2 15,9 22,9 17,14 18,21 12,17 6,21 7,14 2,9 9,9" />
</svg>
</button>
<button class="star" data-value="4" role="radio" aria-checked="false" aria-label="4 stars">
<svg viewBox="0 0 24 24" width="32" height="32">
<polygon points="12,2 15,9 22,9 17,14 18,21 12,17 6,21 7,14 2,9 9,9" />
</svg>
</button>
<button class="star" data-value="5" role="radio" aria-checked="false" aria-label="5 stars">
<svg viewBox="0 0 24 24" width="32" height="32">
<polygon points="12,2 15,9 22,9 17,14 18,21 12,17 6,21 7,14 2,9 9,9" />
</svg>
</button>
<span class="star-rating-text" aria-live="polite">No rating</span>
</div>CSS
.star-rating {
display: inline-flex;
align-items: center;
gap: 4px;
}
.star {
background: none;
border: none;
cursor: pointer;
padding: 2px;
border-radius: 4px;
transition: transform 0.1s ease;
}
.star:hover {
transform: scale(1.15);
}
.star:focus-visible {
outline: 2px solid #f59e0b;
outline-offset: 2px;
}
.star svg {
fill: #d1d5db;
stroke: #d1d5db;
stroke-width: 1;
transition: fill 0.15s ease, stroke 0.15s ease;
}
/* Hover preview — all stars up to hovered one turn yellow */
.star-rating:hover .star svg {
fill: #d1d5db;
stroke: #d1d5db;
}
.star-rating .star.hovered svg {
fill: #fbbf24;
stroke: #f59e0b;
}
/* Selected state */
.star.selected svg {
fill: #f59e0b;
stroke: #d97706;
}
.star-rating-text {
margin-left: 8px;
font-size: 14px;
color: #6b7280;
min-width: 80px;
}JavaScript
class StarRating {
constructor(element) {
this.el = element;
this.stars = [...element.querySelectorAll('.star')];
this.text = element.querySelector('.star-rating-text');
this.currentRating = 0;
this.labels = ['No rating', 'Poor', 'Fair', 'Good', 'Very Good', 'Excellent'];
this.bindEvents();
}
bindEvents() {
this.stars.forEach((star, index) => {
// Hover preview
star.addEventListener('mouseenter', () => this.preview(index + 1));
// Click to select
star.addEventListener('click', () => this.select(index + 1));
});
// Reset preview on mouse leave
this.el.addEventListener('mouseleave', () => this.preview(0));
// Keyboard support
this.el.addEventListener('keydown', (e) => {
const focused = this.stars.indexOf(document.activeElement);
if (focused === -1) return;
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
e.preventDefault();
const next = Math.min(focused + 1, this.stars.length - 1);
this.stars[next].focus();
this.select(next + 1);
}
if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
e.preventDefault();
const prev = Math.max(focused - 1, 0);
this.stars[prev].focus();
this.select(prev + 1);
}
});
}
preview(rating) {
this.stars.forEach((star, i) => {
star.classList.toggle('hovered', rating > 0 && i < rating);
});
// Show preview text while hovering
if (rating > 0) {
this.text.textContent = this.labels[rating];
} else {
this.text.textContent = this.labels[this.currentRating];
}
}
select(rating) {
// Toggle off if clicking same rating
if (this.currentRating === rating) {
this.currentRating = 0;
} else {
this.currentRating = rating;
}
this.stars.forEach((star, i) => {
const isSelected = i < this.currentRating;
star.classList.toggle('selected', isSelected);
star.setAttribute('aria-checked', isSelected);
});
this.text.textContent = this.labels[this.currentRating];
// Dispatch custom event
this.el.dispatchEvent(new CustomEvent('rating-change', {
detail: { rating: this.currentRating }
}));
}
}
// Initialize
const rating = new StarRating(document.querySelector('.star-rating'));
// Listen for changes
rating.el.addEventListener('rating-change', (e) => {
console.log('Rating:', e.detail.rating);
});Key Interview Points
- Hover vs. selected: Two separate visual states —
hoveredclass for preview,selectedclass for committed choice - Mouse leave resets preview: When the mouse leaves the component, it reverts to showing the selected state
- Toggle behavior: Clicking the same star deselects it (rating = 0)
aria-live="polite": The text label announces changes to screen readers without interrupting them- Custom event:
rating-changeevent makes the component reusable — parent code listens for the event instead of reaching into internals
4. Accordion / Collapsible Sections
Commonly asked as a follow-up to simpler components. Key details: only one section open at a time (optional), smooth height animation, and keyboard navigation.
HTML
<div class="accordion" role="presentation">
<div class="accordion-item">
<button class="accordion-header" aria-expanded="false" aria-controls="panel-1" id="header-1">
<span>What is JavaScript?</span>
<span class="accordion-icon" aria-hidden="true">+</span>
</button>
<div class="accordion-panel" id="panel-1" role="region" aria-labelledby="header-1">
<div class="accordion-content">
<p>JavaScript is a high-level, interpreted programming language. It is one of the core technologies of the World Wide Web, alongside HTML and CSS.</p>
</div>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header" aria-expanded="false" aria-controls="panel-2" id="header-2">
<span>What is the DOM?</span>
<span class="accordion-icon" aria-hidden="true">+</span>
</button>
<div class="accordion-panel" id="panel-2" role="region" aria-labelledby="header-2">
<div class="accordion-content">
<p>The Document Object Model (DOM) is a programming interface for web documents. It represents the page so that programs can change the document structure, style, and content.</p>
</div>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header" aria-expanded="false" aria-controls="panel-3" id="header-3">
<span>What is event delegation?</span>
<span class="accordion-icon" aria-hidden="true">+</span>
</button>
<div class="accordion-panel" id="panel-3" role="region" aria-labelledby="header-3">
<div class="accordion-content">
<p>Event delegation is a technique where you attach a single event listener to a parent element to handle events for all its child elements. It works because of event bubbling in the DOM.</p>
</div>
</div>
</div>
</div>CSS
.accordion {
max-width: 600px;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
}
.accordion-item + .accordion-item {
border-top: 1px solid #e5e7eb;
}
.accordion-header {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: white;
border: none;
cursor: pointer;
font-size: 16px;
font-weight: 500;
text-align: left;
color: #111;
transition: background 0.15s ease;
}
.accordion-header:hover {
background: #f9fafb;
}
.accordion-header:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: -2px;
}
.accordion-icon {
font-size: 20px;
transition: transform 0.3s ease;
color: #6b7280;
}
.accordion-header[aria-expanded="true"] .accordion-icon {
transform: rotate(45deg);
}
.accordion-panel {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.accordion-content {
padding: 0 20px 16px;
color: #4b5563;
line-height: 1.6;
}JavaScript
class Accordion {
constructor(element, options = {}) {
this.el = element;
this.allowMultiple = options.allowMultiple || false;
this.items = [...element.querySelectorAll('.accordion-item')];
this.headers = [...element.querySelectorAll('.accordion-header')];
this.bindEvents();
}
bindEvents() {
this.headers.forEach((header) => {
header.addEventListener('click', () => this.toggle(header));
});
// Keyboard navigation
this.el.addEventListener('keydown', (e) => {
const index = this.headers.indexOf(document.activeElement);
if (index === -1) return;
let target;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
target = this.headers[(index + 1) % this.headers.length];
break;
case 'ArrowUp':
e.preventDefault();
target = this.headers[(index - 1 + this.headers.length) % this.headers.length];
break;
case 'Home':
e.preventDefault();
target = this.headers[0];
break;
case 'End':
e.preventDefault();
target = this.headers[this.headers.length - 1];
break;
}
if (target) target.focus();
});
}
toggle(header) {
const isExpanded = header.getAttribute('aria-expanded') === 'true';
const panel = document.getElementById(header.getAttribute('aria-controls'));
if (!this.allowMultiple && !isExpanded) {
// Close all other panels first
this.headers.forEach((h) => {
if (h !== header) this.collapse(h);
});
}
if (isExpanded) {
this.collapse(header);
} else {
this.expand(header);
}
}
expand(header) {
const panel = document.getElementById(header.getAttribute('aria-controls'));
header.setAttribute('aria-expanded', 'true');
// Animate to full height
panel.style.maxHeight = panel.scrollHeight + 'px';
}
collapse(header) {
const panel = document.getElementById(header.getAttribute('aria-controls'));
header.setAttribute('aria-expanded', 'false');
panel.style.maxHeight = '0';
}
}
// Single-open mode (default)
const accordion = new Accordion(document.querySelector('.accordion'));
// Multi-open mode
// const accordion = new Accordion(document.querySelector('.accordion'), { allowMultiple: true });Key Interview Points
max-heightanimation trick: CSS can't animateheight: auto. UsescrollHeightto get the actual content height and animatemax-heightto that valueallowMultipleoption: Shows you think about configurability- Keyboard nav: Arrow keys move between headers, Home/End jump to first/last — follows WAI-ARIA accordion pattern
aria-expanded+aria-controls: Links each button to its panel for screen readers
5. Dropdown Menu
A dropdown with keyboard navigation, sub-menus awareness, and click-outside-to-close behavior.
HTML
<div class="dropdown">
<button class="dropdown-trigger" aria-haspopup="true" aria-expanded="false">
Options
<span class="dropdown-arrow" aria-hidden="true">▾</span>
</button>
<ul class="dropdown-menu" role="menu">
<li role="menuitem" tabindex="-1">Edit</li>
<li role="menuitem" tabindex="-1">Duplicate</li>
<li role="menuitem" tabindex="-1">Archive</li>
<li class="dropdown-divider" role="separator"></li>
<li role="menuitem" tabindex="-1" class="dropdown-danger">Delete</li>
</ul>
</div>CSS
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-trigger {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: white;
border: 1px solid #d1d5db;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
color: #111;
}
.dropdown-trigger:hover {
background: #f9fafb;
}
.dropdown-arrow {
font-size: 10px;
transition: transform 0.2s ease;
}
.dropdown-trigger[aria-expanded="true"] .dropdown-arrow {
transform: rotate(180deg);
}
.dropdown-menu {
position: absolute;
top: calc(100% + 4px);
left: 0;
min-width: 160px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
list-style: none;
padding: 4px;
margin: 0;
opacity: 0;
visibility: hidden;
transform: translateY(-4px);
transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s ease;
z-index: 100;
}
.dropdown-menu.open {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.dropdown-menu li[role="menuitem"] {
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
color: #374151;
}
.dropdown-menu li[role="menuitem"]:hover,
.dropdown-menu li[role="menuitem"]:focus {
background: #f3f4f6;
outline: none;
}
.dropdown-divider {
height: 1px;
background: #e5e7eb;
margin: 4px 0;
}
.dropdown-danger {
color: #ef4444 !important;
}JavaScript
class Dropdown {
constructor(element) {
this.el = element;
this.trigger = element.querySelector('.dropdown-trigger');
this.menu = element.querySelector('.dropdown-menu');
this.items = [...element.querySelectorAll('[role="menuitem"]')];
this.focusedIndex = -1;
this.bindEvents();
}
bindEvents() {
this.trigger.addEventListener('click', () => this.toggle());
// Keyboard on trigger
this.trigger.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.open();
this.focusItem(0);
}
});
// Keyboard navigation within menu
this.menu.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
this.focusItem(this.focusedIndex + 1);
break;
case 'ArrowUp':
e.preventDefault();
this.focusItem(this.focusedIndex - 1);
break;
case 'Home':
e.preventDefault();
this.focusItem(0);
break;
case 'End':
e.preventDefault();
this.focusItem(this.items.length - 1);
break;
case 'Escape':
this.close();
this.trigger.focus();
break;
case 'Enter':
case ' ':
e.preventDefault();
if (this.focusedIndex >= 0) {
this.items[this.focusedIndex].click();
this.close();
}
break;
case 'Tab':
this.close();
break;
}
});
// Click item
this.items.forEach((item, i) => {
item.addEventListener('click', () => {
console.log('Selected:', item.textContent);
this.close();
});
});
// Click outside to close
document.addEventListener('click', (e) => {
if (!this.el.contains(e.target)) {
this.close();
}
});
}
toggle() {
if (this.menu.classList.contains('open')) {
this.close();
} else {
this.open();
}
}
open() {
this.menu.classList.add('open');
this.trigger.setAttribute('aria-expanded', 'true');
}
close() {
this.menu.classList.remove('open');
this.trigger.setAttribute('aria-expanded', 'false');
this.focusedIndex = -1;
}
focusItem(index) {
if (index < 0) index = this.items.length - 1;
if (index >= this.items.length) index = 0;
this.focusedIndex = index;
this.items[index].focus();
}
}
// Initialize
const dropdown = new Dropdown(document.querySelector('.dropdown'));Key Interview Points
- Click outside:
document.addEventListener('click')withthis.el.contains(e.target)— a pattern used everywhere aria-haspopup+aria-expanded: Tells screen readers a popup is available and its current state- Focus management: Arrow keys cycle through items, Escape returns focus to trigger
tabindex="-1"on menu items: Makes them focusable via JS but not in the natural tab order
6. Tooltip
Position-aware tooltips that flip when they'd go off-screen.
HTML
<button class="tooltip-trigger" data-tooltip="This is a helpful tooltip!" data-position="top">
Hover me (top)
</button>
<button class="tooltip-trigger" data-tooltip="I appear on the right side" data-position="right">
Hover me (right)
</button>
<button class="tooltip-trigger" data-tooltip="Bottom tooltip here" data-position="bottom">
Hover me (bottom)
</button>CSS
.tooltip-trigger {
position: relative;
padding: 8px 16px;
background: white;
border: 1px solid #d1d5db;
border-radius: 6px;
cursor: pointer;
}
.tooltip {
position: absolute;
background: #1f2937;
color: white;
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s ease;
z-index: 1000;
}
.tooltip.visible {
opacity: 1;
}
/* Arrow */
.tooltip::after {
content: '';
position: absolute;
border: 5px solid transparent;
}
.tooltip[data-position="top"] {
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
}
.tooltip[data-position="top"]::after {
top: 100%;
left: 50%;
transform: translateX(-50%);
border-top-color: #1f2937;
}
.tooltip[data-position="bottom"] {
top: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
}
.tooltip[data-position="bottom"]::after {
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border-bottom-color: #1f2937;
}
.tooltip[data-position="right"] {
left: calc(100% + 8px);
top: 50%;
transform: translateY(-50%);
}
.tooltip[data-position="right"]::after {
right: 100%;
top: 50%;
transform: translateY(-50%);
border-right-color: #1f2937;
}
.tooltip[data-position="left"] {
right: calc(100% + 8px);
top: 50%;
transform: translateY(-50%);
}
.tooltip[data-position="left"]::after {
left: 100%;
top: 50%;
transform: translateY(-50%);
border-left-color: #1f2937;
}JavaScript
class Tooltip {
constructor() {
this.triggers = document.querySelectorAll('.tooltip-trigger');
this.init();
}
init() {
this.triggers.forEach((trigger) => {
const text = trigger.getAttribute('data-tooltip');
const position = trigger.getAttribute('data-position') || 'top';
// Create tooltip element
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.textContent = text;
tooltip.setAttribute('role', 'tooltip');
tooltip.setAttribute('data-position', position);
// Generate unique ID for aria
const id = 'tooltip-' + Math.random().toString(36).substring(2, 9);
tooltip.id = id;
trigger.setAttribute('aria-describedby', id);
trigger.style.position = 'relative';
trigger.appendChild(tooltip);
// Show/hide
trigger.addEventListener('mouseenter', () => this.show(tooltip, trigger));
trigger.addEventListener('mouseleave', () => this.hide(tooltip));
trigger.addEventListener('focus', () => this.show(tooltip, trigger));
trigger.addEventListener('blur', () => this.hide(tooltip));
});
}
show(tooltip, trigger) {
tooltip.classList.add('visible');
// Check if tooltip overflows viewport and flip if needed
requestAnimationFrame(() => {
const rect = tooltip.getBoundingClientRect();
const position = tooltip.getAttribute('data-position');
if (position === 'top' && rect.top < 0) {
tooltip.setAttribute('data-position', 'bottom');
} else if (position === 'bottom' && rect.bottom > window.innerHeight) {
tooltip.setAttribute('data-position', 'top');
} else if (position === 'right' && rect.right > window.innerWidth) {
tooltip.setAttribute('data-position', 'left');
} else if (position === 'left' && rect.left < 0) {
tooltip.setAttribute('data-position', 'right');
}
});
}
hide(tooltip) {
tooltip.classList.remove('visible');
// Reset position
const originalPosition = tooltip.closest('.tooltip-trigger').getAttribute('data-position');
tooltip.setAttribute('data-position', originalPosition);
}
}
// Initialize
const tooltips = new Tooltip();Key Interview Points
- Position flipping: Check
getBoundingClientRect()against viewport bounds and swap position aria-describedby: Links the trigger to the tooltip text for screen readers- Focus support: Tooltips should also appear on focus, not just hover — keyboard users need them too
pointer-events: none: Prevents the tooltip from blocking clicks or triggering mouse events
7. Tabs
Tab panels with proper ARIA roles and keyboard navigation.
HTML
<div class="tabs">
<div class="tab-list" role="tablist" aria-label="Content sections">
<button class="tab active" role="tab" aria-selected="true" aria-controls="tab-panel-1" id="tab-1">HTML</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="tab-panel-2" id="tab-2" tabindex="-1">CSS</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="tab-panel-3" id="tab-3" tabindex="-1">JavaScript</button>
</div>
<div class="tab-panel active" role="tabpanel" id="tab-panel-1" aria-labelledby="tab-1">
<p>HTML (HyperText Markup Language) is the standard markup language for documents designed to be displayed in a web browser.</p>
</div>
<div class="tab-panel" role="tabpanel" id="tab-panel-2" aria-labelledby="tab-2" hidden>
<p>CSS (Cascading Style Sheets) is a style sheet language used for describing the presentation of a document written in a markup language.</p>
</div>
<div class="tab-panel" role="tabpanel" id="tab-panel-3" aria-labelledby="tab-3" hidden>
<p>JavaScript is a programming language that is one of the core technologies of the World Wide Web, alongside HTML and CSS.</p>
</div>
</div>CSS
.tabs {
max-width: 600px;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
}
.tab-list {
display: flex;
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.tab {
flex: 1;
padding: 12px 20px;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: #6b7280;
transition: color 0.15s ease, border-color 0.15s ease;
}
.tab:hover {
color: #111;
}
.tab.active {
color: #3b82f6;
border-bottom-color: #3b82f6;
background: white;
}
.tab:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: -2px;
}
.tab-panel {
padding: 20px;
}
.tab-panel[hidden] {
display: none;
}JavaScript
class Tabs {
constructor(element) {
this.el = element;
this.tabs = [...element.querySelectorAll('[role="tab"]')];
this.panels = [...element.querySelectorAll('[role="tabpanel"]')];
this.bindEvents();
}
bindEvents() {
this.tabs.forEach((tab) => {
tab.addEventListener('click', () => this.activate(tab));
});
// Keyboard navigation
this.el.querySelector('[role="tablist"]').addEventListener('keydown', (e) => {
const index = this.tabs.indexOf(document.activeElement);
if (index === -1) return;
let target;
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
target = this.tabs[(index + 1) % this.tabs.length];
break;
case 'ArrowLeft':
e.preventDefault();
target = this.tabs[(index - 1 + this.tabs.length) % this.tabs.length];
break;
case 'Home':
e.preventDefault();
target = this.tabs[0];
break;
case 'End':
e.preventDefault();
target = this.tabs[this.tabs.length - 1];
break;
}
if (target) {
this.activate(target);
target.focus();
}
});
}
activate(selectedTab) {
// Deactivate all
this.tabs.forEach((tab) => {
tab.classList.remove('active');
tab.setAttribute('aria-selected', 'false');
tab.setAttribute('tabindex', '-1');
});
this.panels.forEach((panel) => {
panel.classList.remove('active');
panel.hidden = true;
});
// Activate selected
selectedTab.classList.add('active');
selectedTab.setAttribute('aria-selected', 'true');
selectedTab.removeAttribute('tabindex');
const panelId = selectedTab.getAttribute('aria-controls');
const panel = document.getElementById(panelId);
panel.classList.add('active');
panel.hidden = false;
}
}
// Initialize
const tabs = new Tabs(document.querySelector('.tabs'));Key Interview Points
- Roving tabindex: Only the active tab is in the tab order (
tabindexremoved). Inactive tabs havetabindex="-1". Arrow keys navigate between them. hiddenattribute: Semantically hides inactive panels — better thandisplay: nonein CSS because it communicates to assistive techaria-controls+aria-labelledby: Creates a relationship between each tab and its panel
8. Toast / Notification System
A notification system that stacks, auto-dismisses, and supports different types.
HTML
<div class="toast-container" aria-live="polite" aria-atomic="false"></div>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<button onclick="toast.show('File saved successfully', 'success')">Success Toast</button>
<button onclick="toast.show('Something went wrong', 'error')">Error Toast</button>
<button onclick="toast.show('Please check your input', 'warning')">Warning Toast</button>
<button onclick="toast.show('New update available', 'info')">Info Toast</button>
</div>CSS
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 8px;
max-width: 360px;
}
.toast {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
border-radius: 8px;
background: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border-left: 4px solid;
font-size: 14px;
animation: toast-in 0.3s ease forwards;
cursor: pointer;
}
.toast.removing {
animation: toast-out 0.3s ease forwards;
}
.toast-success { border-left-color: #10b981; }
.toast-error { border-left-color: #ef4444; }
.toast-warning { border-left-color: #f59e0b; }
.toast-info { border-left-color: #3b82f6; }
.toast-icon {
font-size: 18px;
flex-shrink: 0;
}
.toast-message {
flex: 1;
color: #374151;
}
.toast-close {
background: none;
border: none;
font-size: 16px;
color: #9ca3af;
cursor: pointer;
padding: 0 2px;
flex-shrink: 0;
}
.toast-close:hover {
color: #374151;
}
.toast-progress {
position: absolute;
bottom: 0;
left: 4px;
right: 0;
height: 3px;
background: rgba(0, 0, 0, 0.1);
border-radius: 0 0 8px 0;
}
.toast-progress-bar {
height: 100%;
border-radius: inherit;
transition: width linear;
}
.toast-success .toast-progress-bar { background: #10b981; }
.toast-error .toast-progress-bar { background: #ef4444; }
.toast-warning .toast-progress-bar { background: #f59e0b; }
.toast-info .toast-progress-bar { background: #3b82f6; }
@keyframes toast-in {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes toast-out {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(100%);
}
}JavaScript
class ToastManager {
constructor(containerSelector = '.toast-container') {
this.container = document.querySelector(containerSelector);
}
show(message, type = 'info', duration = 4000) {
const icons = {
success: '✓',
error: '✗',
warning: '⚠',
info: 'ℹ',
};
const toast = document.createElement('div');
toast.className = `toast toast-$\{type\}`;
toast.style.position = 'relative';
toast.setAttribute('role', 'alert');
toast.innerHTML = `
<span class="toast-icon">${icons[type]}</span>
<span class="toast-message">${this.escapeHtml(message)}</span>
<button class="toast-close" aria-label="Dismiss">×</button>
<div class="toast-progress">
<div class="toast-progress-bar" style="width: 100%"></div>
</div>
`;
// Close button
toast.querySelector('.toast-close').addEventListener('click', () => {
this.dismiss(toast);
});
// Click to dismiss
toast.addEventListener('click', (e) => {
if (!e.target.closest('.toast-close')) {
this.dismiss(toast);
}
});
this.container.appendChild(toast);
// Animate progress bar
const progressBar = toast.querySelector('.toast-progress-bar');
requestAnimationFrame(() => {
progressBar.style.transitionDuration = duration + 'ms';
progressBar.style.width = '0%';
});
// Auto-dismiss
const timer = setTimeout(() => this.dismiss(toast), duration);
// Pause on hover
toast.addEventListener('mouseenter', () => {
clearTimeout(timer);
progressBar.style.transitionPlayState = 'paused';
});
toast.addEventListener('mouseleave', () => {
const remaining = (parseFloat(getComputedStyle(progressBar).width) /
parseFloat(getComputedStyle(progressBar.parentElement).width)) * duration;
progressBar.style.transitionPlayState = 'running';
setTimeout(() => this.dismiss(toast), remaining || 1000);
});
return toast;
}
dismiss(toast) {
if (toast.classList.contains('removing')) return;
toast.classList.add('removing');
toast.addEventListener('animationend', () => toast.remove());
}
escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
}
// Initialize
const toast = new ToastManager();Key Interview Points
- XSS prevention:
escapeHtml()prevents injection through toast messages — interviewers love this detail aria-live="polite": The container announces new toasts to screen readers without interrupting- Progress bar animation: CSS
transitiononwidthwith JS control — shows CSS/JS coordination - Pause on hover: Clearing the timeout and pausing the CSS transition shows attention to UX detail
- Stacking: Flexbox column layout with gap handles multiple toasts naturally
9. Infinite Scroll
Load more content as the user scrolls to the bottom — using IntersectionObserver instead of scroll event listeners.
HTML
<div class="feed">
<div class="feed-list" id="feed-list"></div>
<div class="feed-sentinel" id="sentinel"></div>
<div class="feed-loading" id="loading" hidden>
<div class="spinner"></div>
Loading more...
</div>
<div class="feed-end" id="end-message" hidden>
You've reached the end!
</div>
</div>CSS
.feed {
max-width: 600px;
margin: 0 auto;
}
.feed-item {
padding: 16px 20px;
border: 1px solid #e5e7eb;
border-radius: 8px;
margin-bottom: 12px;
background: white;
animation: fade-in 0.3s ease;
}
.feed-item h3 {
margin: 0 0 4px;
font-size: 16px;
}
.feed-item p {
margin: 0;
color: #6b7280;
font-size: 14px;
}
.feed-loading, .feed-end {
text-align: center;
padding: 20px;
color: #6b7280;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid #e5e7eb;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes fade-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}JavaScript
class InfiniteScroll {
constructor(options) {
this.list = document.getElementById(options.listId);
this.sentinel = document.getElementById(options.sentinelId);
this.loading = document.getElementById(options.loadingId);
this.endMessage = document.getElementById(options.endMessageId);
this.page = 1;
this.pageSize = options.pageSize || 10;
this.maxItems = options.maxItems || 50;
this.isLoading = false;
this.hasMore = true;
this.initObserver();
this.loadMore(); // Initial load
}
initObserver() {
this.observer = new IntersectionObserver(
(entries) => {
// When sentinel becomes visible, load more
if (entries[0].isIntersecting && this.hasMore && !this.isLoading) {
this.loadMore();
}
},
{
rootMargin: '200px', // Start loading 200px before reaching bottom
}
);
this.observer.observe(this.sentinel);
}
async loadMore() {
this.isLoading = true;
this.loading.hidden = false;
// Simulate API call
const items = await this.fetchItems(this.page, this.pageSize);
// Render items
items.forEach((item) => {
const el = document.createElement('div');
el.className = 'feed-item';
el.innerHTML = `<h3>${item.title}</h3><p>${item.body}</p>`;
this.list.appendChild(el);
});
this.page++;
this.isLoading = false;
this.loading.hidden = true;
// Check if we've loaded everything
const totalLoaded = this.list.children.length;
if (totalLoaded >= this.maxItems) {
this.hasMore = false;
this.observer.disconnect();
this.endMessage.hidden = false;
}
}
// Mock API — replace with real fetch
fetchItems(page, pageSize) {
return new Promise((resolve) => {
setTimeout(() => {
const start = (page - 1) * pageSize;
const items = Array.from({ length: pageSize }, (_, i) => ({
title: `Post #$\{start + i + 1\}`,
body: `This is the content for post $\{start + i + 1\}. It contains interesting information.`,
}));
resolve(items);
}, 800);
});
}
}
// Initialize
const feed = new InfiniteScroll({
listId: 'feed-list',
sentinelId: 'sentinel',
loadingId: 'loading',
endMessageId: 'end-message',
pageSize: 10,
maxItems: 50,
});Key Interview Points
IntersectionObservervs. scroll events: Observer is far more performant — no throttling/debouncing needed, no layout thrashing- Sentinel element: A hidden div at the bottom that triggers loading when it enters the viewport — cleaner than measuring scroll position
rootMargin: '200px': Pre-fetches before the user reaches the bottom for a seamless experience- Loading guard:
isLoadingflag prevents duplicate requests during slow network conditions - Disconnect on completion: Stop observing when all content is loaded to prevent unnecessary callbacks
10. Drag and Drop Sortable List
Reorderable list using the native HTML5 Drag and Drop API.
HTML
<ul class="sortable-list" id="sortable">
<li class="sortable-item" draggable="true">
<span class="drag-handle" aria-hidden="true">☰</span>
<span>Learn HTML</span>
</li>
<li class="sortable-item" draggable="true">
<span class="drag-handle" aria-hidden="true">☰</span>
<span>Learn CSS</span>
</li>
<li class="sortable-item" draggable="true">
<span class="drag-handle" aria-hidden="true">☰</span>
<span>Learn JavaScript</span>
</li>
<li class="sortable-item" draggable="true">
<span class="drag-handle" aria-hidden="true">☰</span>
<span>Build a project</span>
</li>
<li class="sortable-item" draggable="true">
<span class="drag-handle" aria-hidden="true">☰</span>
<span>Get a job</span>
</li>
</ul>CSS
.sortable-list {
list-style: none;
padding: 0;
max-width: 400px;
margin: 0 auto;
}
.sortable-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
margin-bottom: 8px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
cursor: grab;
user-select: none;
transition: box-shadow 0.15s ease, transform 0.15s ease;
}
.sortable-item:active {
cursor: grabbing;
}
.sortable-item.dragging {
opacity: 0.4;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.sortable-item.drag-over {
border-color: #3b82f6;
background: #eff6ff;
}
.drag-handle {
color: #9ca3af;
font-size: 14px;
cursor: grab;
}JavaScript
class SortableList {
constructor(listElement) {
this.list = listElement;
this.items = [...listElement.querySelectorAll('.sortable-item')];
this.draggedItem = null;
this.bindEvents();
}
bindEvents() {
this.list.addEventListener('dragstart', (e) => {
const item = e.target.closest('.sortable-item');
if (!item) return;
this.draggedItem = item;
item.classList.add('dragging');
// Required for Firefox
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', '');
// Slight delay so the dragging class applies after the drag image is captured
requestAnimationFrame(() => {
item.classList.add('dragging');
});
});
this.list.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const target = e.target.closest('.sortable-item');
if (!target || target === this.draggedItem) return;
// Determine if we should insert before or after
const rect = target.getBoundingClientRect();
const midY = rect.top + rect.height / 2;
// Remove all drag-over states
this.list.querySelectorAll('.drag-over').forEach((el) => {
el.classList.remove('drag-over');
});
target.classList.add('drag-over');
if (e.clientY < midY) {
this.list.insertBefore(this.draggedItem, target);
} else {
this.list.insertBefore(this.draggedItem, target.nextSibling);
}
});
this.list.addEventListener('dragend', (e) => {
if (this.draggedItem) {
this.draggedItem.classList.remove('dragging');
}
this.list.querySelectorAll('.drag-over').forEach((el) => {
el.classList.remove('drag-over');
});
this.draggedItem = null;
// Get new order
const newOrder = [...this.list.querySelectorAll('.sortable-item')].map(
(item) => item.querySelector('span:last-child').textContent
);
console.log('New order:', newOrder);
});
this.list.addEventListener('drop', (e) => {
e.preventDefault();
});
}
}
// Initialize
const sortable = new SortableList(document.getElementById('sortable'));Key Interview Points
midYcalculation: Determines whether to insert before or after the target element — gives a natural reordering feel- Event delegation: All events attached to the parent list, not individual items — performant and handles dynamic items
- Firefox quirk:
e.dataTransfer.setData('text/plain', '')is required for drag to work in Firefox requestAnimationFrame: Ensures the visual dragging state applies after the browser captures the drag ghost imageuser-select: none: Prevents text selection during drag
Interview Tips
Before You Start Coding
- Clarify requirements. Ask: "Should this support keyboard navigation? Should it be accessible? Should it handle edge cases like empty states?"
- Talk through your approach. Explain: HTML structure first, then CSS layout, then JS behavior.
- Mention accessibility upfront. Even if the interviewer doesn't ask, mentioning ARIA roles and keyboard support shows seniority.
While Coding
- Start with the HTML structure. Get the semantic markup right first — it's the foundation.
- Use CSS for everything you can. Transitions, hover states, show/hide — don't reach for JS when CSS can do it.
- Use event delegation. Attaching listeners to parent elements is more performant and handles dynamic content.
- Name things clearly.
handleTab,goToSlide,dismiss— the interviewer is reading your code in real time.
Common Follow-Up Questions
| Question | What They're Testing |
|---|---|
| "How would you make this accessible?" | ARIA roles, keyboard navigation, screen reader support |
| "What if there are 10,000 items?" | Virtualization, pagination, IntersectionObserver |
| "How would you test this?" | Unit tests for logic, integration tests for DOM, accessibility audits |
| "How would you make this a reusable component?" | Constructor options, events API, no global state |
| "What about mobile?" | Touch events, responsive design, swipe gestures |
You might also like
Build Connect Four from Scratch with Vanilla JS
A complete guide to building Connect Four using only HTML, CSS, and vanilla JavaScript. Covers gravity-based piece dropping, win detection across rows/columns/diagonals, and AI opponent. Interview-ready implementation.
FrontendBuild a Hierarchical Checkbox from Scratch with Vanilla JS
A complete guide to building a hierarchical (tri-state) checkbox tree using only HTML, CSS, and vanilla JavaScript. Covers parent-child propagation, indeterminate state, dynamic trees, and accessibility. Interview-ready implementation.
FrontendBuild Snake from Scratch with Vanilla JS
A complete guide to building the classic Snake game using only HTML, CSS, and vanilla JavaScript. Covers game loops, collision detection, growing mechanics, and responsive canvas rendering. Interview-ready implementation.