Every web developer knows the basic HTML tags: headings, paragraphs, links, images. But many of us treat them as a kind of scaffolding—something to prop up until we can bring in a framework or a library. We reach for a custom accordion component when <details> would do. We install a form validation plugin when the browser already has one built in. This guide is a practical re-examination of built-in elements: what they are, why they often outperform our custom alternatives, and when they don't. We'll use concrete analogies and real project scenarios to help you decide when to go native and when to extend.
Where Built-in Elements Show Up in Real Work
Think of built-in elements like the standard tools in a kitchen. You can chop an onion with a chef's knife, a cleaver, or a food processor. The built-in knife block—the basic HTML elements—covers most daily tasks. But many developers treat HTML as a rough draft, immediately converting everything into JavaScript-controlled components. That approach works, but it often adds complexity without a proportional benefit.
Consider the humble <details> element. It creates a disclosure widget—a clickable summary that reveals hidden content. Before it was widely supported, teams built accordions with JavaScript, managing open/close states, animations, and keyboard focus. Now, <details> handles all of that natively. One team I read about replaced a 200-line React accordion with a single <details> tag, cutting bundle size by several kilobytes and eliminating a class of focus-trap bugs. The catch? They had to style it consistently across browsers, which required a few lines of CSS. But the trade-off was overwhelmingly positive.
Another common scenario is form validation. Native HTML5 validation—using attributes like required, pattern, and type='email'—works in every modern browser. It provides instant feedback without any JavaScript. Many teams, however, disable it with novalidate and build custom validation to get consistent styling. That's often unnecessary. With a little CSS and the :invalid pseudo-class, you can achieve polished, accessible validation that works across browsers. The key is knowing when native behavior is sufficient and when you need the extra control.
Built-in elements also shine in forms: <input> types like range, color, and date provide native UI that users already know. Custom sliders and date pickers often introduce accessibility gaps and maintenance overhead. By starting with built-in elements, you get a baseline of usability for free. You can then enhance with JavaScript if needed, rather than rebuilding from scratch.
The Kitchen Analogy
Imagine you're cooking a meal. The built-in elements are the knives, pots, and pans already in your kitchen. You could use a Swiss Army knife for everything, but it's slower and less comfortable. Similarly, every custom component you build is a new tool you have to maintain. Start with the built-in set; add custom tools only when the task clearly demands it.
Foundations Readers Confuse
There are several misconceptions about built-in elements that lead developers to underestimate them. The first is that they are inflexible. Many believe that because <details> has a fixed appearance, it cannot be styled to match a brand. In reality, you can style the <summary> marker with CSS list-style and pseudo-elements, and you can animate the open/close transition using details[open]. The flexibility is there; it just requires a different approach than replacing the entire element.
Another confusion is about accessibility. Some developers assume that built-in elements are inherently accessible, which is mostly true, but they still need proper semantics. For example, using a <div> with ARIA roles to simulate a button is less reliable than using a native <button>. Native elements come with keyboard handling, screen reader announcements, and focus management built in. When you replace them, you have to re-implement all of that. The <dialog> element, for instance, automatically traps focus and manages the modal overlay. Custom modal libraries often struggle with these details, leading to inaccessible experiences.
A third confusion is about browser support. While older browsers may not support newer elements like <dialog> or <details>, the gap is shrinking. Polyfills exist for legacy support, and the native experience in modern browsers is far more consistent than it was five years ago. The real risk is not using them at all, because then you're maintaining custom code that may have its own bugs.
Finally, there's the idea that built-in elements are too simple for complex applications. This is like saying basic ingredients can't make a gourmet meal. Built-in elements are the building blocks; you can compose them with CSS and a light touch of JavaScript to create sophisticated interfaces. The trick is knowing when to stop adding layers.
Common Misstep: Overriding Native Behavior Prematurely
A typical mistake is to disable native form validation with novalidate before even trying to style it. The native validation UI is not customizable in all browsers, but you can use the Constraint Validation API to show custom messages while keeping the built-in logic. This gives you the best of both worlds: reliable validation and a branded look.
Patterns That Usually Work
Over time, certain patterns emerge where built-in elements consistently outperform custom alternatives. Here are three that we recommend as starting points.
1. Disclosure Widgets with <details>
Any interface that shows and hides content on click—FAQs, collapsible panels, tooltips—can benefit from <details>. It handles toggling, keyboard navigation (Enter/Space to open, Tab to move out), and screen reader announcements. You can style the summary marker with list-style: none and add your own icon via ::before. For animations, use details[open] with a CSS transition on max-height or a grid-template-rows trick. This pattern eliminates the need for JavaScript state management for toggles.
2. Native Form Validation
Start with HTML5 validation attributes. Use required, pattern, min, max, and appropriate type values. Then enhance with CSS: style :valid and :invalid inputs to give visual feedback. For custom error messages, use setCustomValidity() in JavaScript, but keep the native behavior for focus management and submission blocking. This approach works for most forms and reduces code significantly.
3. Modals with <dialog>
The <dialog> element provides a native modal dialog. It automatically handles focus trapping, overlay blocking, and escape-key dismissal. You can open it with .showModal() and close with .close(). Styling is straightforward: position the dialog with CSS, add a backdrop with ::backdrop, and animate with transitions. This pattern replaces heavy modal libraries and ensures accessibility out of the box.
When to Enhance
These patterns work for 80% of use cases. For the remaining 20%—complex animations, multi-step forms, or deeply customized interactions—you may need to layer on JavaScript. But start with the native element and enhance, rather than replacing it entirely. This keeps your codebase smaller and more resilient.
Anti-patterns and Why Teams Revert
Despite the benefits, many teams abandon built-in elements after trying them. The reasons often stem from how they were implemented, not from the elements themselves.
Anti-pattern 1: Ignoring Browser Inconsistencies
Built-in elements behave slightly differently across browsers. For example, <details> in Safari uses a different default marker style than Chrome. If you don't normalize these with CSS, the result looks unfinished. Teams that skip this step see a messy UI and revert to a custom solution. The fix is simple: use a CSS reset for the element and test in at least three browsers.
Anti-pattern 2: Overriding Native Behavior Too Aggressively
Some developers try to change the fundamental behavior of a built-in element. For instance, they might want <details> to close when clicking outside, or <dialog> to not close on Escape. Overriding these behaviors often requires hacky JavaScript that reintroduces the very bugs the native element avoided. Instead, accept the native interaction model or choose a different element. Users expect consistency; fighting the browser creates a poor experience.
Anti-pattern 3: Not Providing Fallbacks for Older Browsers
If your audience includes users on older browsers, a missing polyfill can break the page. Teams that deploy <dialog> without a polyfill may find that the modal doesn't appear at all in IE11. This leads to a hasty revert. The solution is to include a lightweight polyfill for the specific elements you use, or to check support with @supports and provide a fallback. This is less work than maintaining a full custom component.
Why Teams Revert: The Real Cost
When a built-in element fails due to one of these anti-patterns, the team often blames the element itself. They revert to a custom solution, which then requires ongoing maintenance for accessibility, keyboard handling, and styling. The cycle repeats. The key is to invest a small amount of time in understanding the quirks of each element before committing to it. A few extra hours of testing can save months of custom code maintenance.
Maintenance, Drift, and Long-term Costs
Every line of code you write is a liability. Built-in elements, because they are maintained by browser vendors, shift that liability to the platform. When a new browser version ships, your native element gets updates for free. Custom components, on the other hand, require you to track changes in browser behavior and update your code accordingly.
Consider the long-term cost of a custom accordion component. You have to manage state, handle edge cases (like multiple open panels), ensure keyboard navigation, and test across browsers. Every time a new framework version comes out, you may need to update the component. Over five years, that maintenance time adds up. A <details> element, once styled, requires almost no maintenance. The browser handles the rest.
Drift is another issue. Custom components tend to accumulate features over time: animations, custom events, accessibility patches. Each addition increases complexity and the risk of bugs. Built-in elements, being simpler, resist feature creep. If you need more functionality, you can add it in a separate layer, but the core remains stable.
Cost Comparison Table
| Factor | Built-in Element | Custom Component |
|---|---|---|
| Initial development time | Low (minutes) | Medium to high (hours to days) |
| Accessibility baseline | High (native) | Variable (must be manually implemented) |
| Cross-browser testing | Minimal (same behavior) | Extensive (each browser may differ) |
| Maintenance over 2 years | Near zero | Ongoing (framework updates, bug fixes) |
| Flexibility for customization | Moderate (CSS and light JS) | High (full control) |
When Not to Use This Approach
Built-in elements are not a silver bullet. There are clear cases where custom components are the better choice.
Complex Animations and Transitions
If your UI requires intricate animations—like a multi-step wizard with sliding panels or a drag-and-drop interface—native elements may not provide the necessary hooks. For example, <details> does not support animating the open/close transition smoothly without CSS tricks that can be brittle. In such cases, a custom component with a library like Framer Motion or GSAP gives you the control you need.
Highly Customized UI That Must Match a Design System Exactly
Some design systems require pixel-perfect control over every aspect of an element, including focus rings, disabled states, and hover effects. While you can style built-in elements extensively, there are limits. For instance, styling the <select> element's dropdown options is notoriously difficult across browsers. If your design calls for a completely custom dropdown, a custom component may be necessary. However, consider whether a native <select> with some CSS styling can meet the design's intent—often it can.
Real-time Collaborative Features
If you're building a collaborative editor where multiple users interact with the same UI elements simultaneously, native elements may not provide the event model you need. Custom components with state management libraries (like Redux or MobX) give you fine-grained control over updates and synchronization.
Decision Criteria: A Quick Checklist
- Does the native element support the core interaction? (e.g.,
<details>for toggle,<dialog>for modal) - Can you achieve the desired appearance with CSS alone? (If not, consider enhancement, not replacement)
- Is the audience using modern browsers? (If yes, native is safe; if no, add a polyfill)
- Does the design require complex animations? (If yes, custom may be better)
- Is accessibility a priority? (Native elements give a strong baseline)
Open Questions and FAQ
How do I handle older browsers like IE11?
Use polyfills. For <details>, the details-element-polyfill is lightweight. For <dialog>, use dialog-polyfill. These polyfills add the missing behavior while allowing you to write modern HTML. Alternatively, use feature detection and provide a fallback message or a simpler UI for unsupported browsers.
Can I use built-in elements with React or Vue?
Yes, but with caution. React, for example, has its own event system that can conflict with native events. For <details>, you can use onToggle event in React, but it may not fire as expected in all cases. It's often simpler to use a ref and add a native event listener. Vue handles native elements well, but be aware of two-way binding quirks with <dialog>. In general, built-in elements work fine with frameworks as long as you respect their lifecycle.
What about styling the <select> element?
Styling <select> is notoriously limited. You can change the font, colors, and padding, but the dropdown arrow and the options list are hard to customize. If your design requires a custom dropdown, consider using a <div> with ARIA roles, but be aware of the accessibility cost. Alternatively, use a library like Choices.js that enhances a native select rather than replacing it.
Is <dialog> accessible?
Yes, when used correctly. The <dialog> element automatically manages focus: when opened with .showModal(), focus is trapped inside the dialog. It also sets aria-modal='true' and hides background content from screen readers. However, you must ensure that the dialog content itself is accessible—proper headings, labels, and focusable elements. Avoid using display: none to hide dialogs; use the native open/close methods.
Summary and Next Experiments
Built-in elements are not a regression; they are a foundation. By starting with them, you reduce code, improve accessibility, and lower maintenance costs. The key is to understand their strengths and limitations, and to resist the urge to replace them prematurely.
Here are four concrete next steps to try in your next project:
- Replace one custom accordion with
<details>. Note the reduction in code and test for accessibility. You'll likely find it works better than expected. - Enable native form validation on an existing form. Remove
novalidateand style:invalidinputs. See how much JavaScript you can delete. - Use
<dialog>for a modal. Start with a simple confirmation dialog. Test focus trapping and keyboard behavior. Compare it to your current modal library. - Audit your codebase for custom components that could be replaced. Look for accordions, modals, tooltips, and form validation. Estimate the maintenance cost of each and consider switching.
Remember, the goal is not to eliminate JavaScript entirely, but to use it where it adds real value. Built-in elements give you a solid, accessible baseline. Build on that, and your projects will be simpler, faster, and more resilient.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!