Skip to main content
Built-in Elements

Mastering Built-in Elements: Practical Strategies for Modern Web Development

Every web page is built on a small set of native HTML elements— <div> , <span> , <button> , <input> , <form> , and a few dozen others. These are the built-in elements, and they are surprisingly powerful. Yet in modern web development, it's common to see teams reach for a framework component or a custom element when a native solution would work better. This guide is for front-end developers, designers, and technical leads who want to make deliberate choices about when to use built-in elements and when to extend them. We'll walk through the options, compare them on practical criteria, and show you how to avoid common mistakes. Who Should Choose and By When If you are starting a new project or refactoring an existing one, the decision about how much to rely on built-in elements affects everything: file size, accessibility, maintenance cost, and even SEO.

Every web page is built on a small set of native HTML elements—<div>, <span>, <button>, <input>, <form>, and a few dozen others. These are the built-in elements, and they are surprisingly powerful. Yet in modern web development, it's common to see teams reach for a framework component or a custom element when a native solution would work better. This guide is for front-end developers, designers, and technical leads who want to make deliberate choices about when to use built-in elements and when to extend them. We'll walk through the options, compare them on practical criteria, and show you how to avoid common mistakes.

Who Should Choose and By When

If you are starting a new project or refactoring an existing one, the decision about how much to rely on built-in elements affects everything: file size, accessibility, maintenance cost, and even SEO. The choice is not binary—you can mix approaches—but you need a strategy before you write the first line of markup.

This decision typically falls on the lead developer or architect during the initial planning phase. For a content-heavy site (a blog, documentation, or marketing pages), the answer is often straightforward: lean heavily on semantic HTML and minimal custom styling. For a complex web application (a dashboard, a project management tool, or a form-heavy workflow), you will need a mix of built-in elements and custom components, but you should still prefer native elements for their built-in behaviors.

The timeline matters: if you are under a tight deadline, using built-in elements directly saves time because you avoid writing and testing custom logic for keyboard navigation, focus management, and form validation. If you have a long-lived product, investing in lightweight custom elements (using the Web Components standard) can pay off in maintainability. But if you skip the planning step, you risk ending up with a mess of nested <div>s and inaccessible widgets that are hard to refactor later. We recommend making this decision in the first sprint, before any component library is chosen.

When to Revisit the Decision

Even after you choose a strategy, revisit it when you add a new interactive pattern (like a tab panel or a modal) or when accessibility requirements change. Built-in elements are not a one-time choice; they need ongoing evaluation as the project evolves.

The Option Landscape: Three Core Approaches

There are three main ways to use built-in elements in modern web development. Each has its own strengths and weaknesses, and the best choice depends on your project's constraints.

Approach 1: Semantic HTML with Progressive Enhancement

This is the most conservative approach. You write HTML that uses the most appropriate native element for each piece of content: <nav> for navigation, <main> for the primary content, <article> for a self-contained composition, <button> for actions, and so on. You then enhance behavior with JavaScript only where needed—for example, adding a click handler to a button that opens a modal. You do not replace native elements with custom ones unless absolutely necessary.

Pros: Excellent accessibility out of the box (screen readers and keyboards understand native elements), small file size, no framework dependency, and easy to maintain. Cons: Limited styling control for some elements (like <select> and <input type='file'>), and some UI patterns (like a custom-styled toggle switch) require workarounds.

Approach 2: Custom Elements (Web Components)

With the Custom Elements API, you can extend built-in elements or create new ones. For example, you could create a <custom-button> that extends <button> and adds a loading state. This approach lets you encapsulate behavior and styling while still inheriting native semantics (if you extend a native element).

Pros: Reusable, encapsulated, and maintainable; works across frameworks. Cons: Requires more JavaScript, can be overkill for simple patterns, and browser support for extending built-in elements (like is="...") is not universal.

Approach 3: Framework Components That Wrap Native Elements

Most modern frameworks (React, Vue, Angular) allow you to create components that render native HTML elements underneath. For example, a React <Button> component might render a <button> element with added props. This is the most common approach in large applications, but it can lead to abstraction layers that hide the underlying native element.

Pros: Familiar to most developers, integrates with framework tooling, and allows complex logic. Cons: Can produce inaccessible markup if the developer does not pass the right props (like type="button" to prevent form submission), and adds bundle size.

Which Approach to Choose?

For most projects, we recommend starting with Approach 1 (semantic HTML) and only moving to Approach 2 or 3 when you hit a specific limitation. For example, if you need a custom-styled <select> that works on mobile, consider a custom element that uses a listbox pattern with ARIA roles, rather than a full framework component.

Comparison Criteria: How to Evaluate Your Options

When deciding between these approaches, use these five criteria. They will help you avoid the trap of choosing a tool just because it's popular.

1. Accessibility (A11y)

Native elements come with built-in keyboard interaction, focus management, and screen reader announcements. A <button> is automatically focusable and activated with Enter/Space. A <div> with a click handler is not. If you use custom elements, you must manually add these behaviors. The more you deviate from native elements, the more accessibility testing you need.

2. Performance and Bundle Size

Every custom component you write adds JavaScript. If you use a framework, the framework itself adds overhead. For a simple page, a few kilobytes of JavaScript might not matter, but for a large application, it adds up. Built-in elements require zero JavaScript for their base behavior. We have seen teams reduce their bundle size by 30% simply by replacing custom dropdowns with native <select> elements.

3. Maintainability and Learning Curve

HTML is a standard that every web developer knows. Custom elements and framework components require documentation and onboarding. If your team changes frequently, sticking close to built-in elements reduces the learning curve. On the other hand, a well-designed custom component library can make maintenance easier by centralizing behavior.

4. Styling Flexibility

Some native elements are notoriously hard to style consistently across browsers. The <input type='date'> look varies wildly. If your design requires pixel-perfect control, you might need to use a custom element or a framework component that hides the native control. But this trade-off comes at the cost of accessibility and behavior—many custom date pickers fail to work with screen readers.

5. Form Participation

Native form elements automatically participate in form submission and validation. Custom elements do not, unless you explicitly implement the form-associated custom element pattern. If you are building a form-heavy application, prefer native elements or form-associated custom elements to avoid reinventing form behavior.

How to Weigh the Criteria

Rank these criteria for your project. For a public-facing government site, accessibility and form participation are non-negotiable, so you would lean heavily on native elements. For an internal dashboard with a small user base, styling flexibility might be more important, and you could accept some accessibility trade-offs (though we do not recommend it).

Trade-Offs Table: A Structured Comparison

To make the decision clearer, here is a comparison of the three approaches across the criteria above. The table uses a simple rating: Excellent (best), Good (works well), Fair (requires effort), and Poor (problematic).

CriterionSemantic HTMLCustom ElementsFramework Components
AccessibilityExcellentGood (if done right)Fair (depends on implementation)
PerformanceExcellentGood (small JS overhead)Fair (framework overhead)
MaintainabilityExcellentGood (encapsulated)Good (familiar patterns)
Styling FlexibilityFair (some elements hard to style)Good (full control)Good (full control)
Form ParticipationExcellentFair (needs extra work)Fair (needs extra work)

As the table shows, semantic HTML wins on accessibility, performance, and form participation, but loses on styling flexibility for a few elements. Custom elements and framework components offer more styling control but require more effort to match native accessibility and form behavior. The key is to use the right tool for each part of your UI.

Composite Scenario: A Content Site with a Custom Widget

Imagine you are building a recipe blog. The main content is articles with headings, lists, and images. For this, semantic HTML is perfect: use <article>, <h1><h6>, <ul>, and <figure>. But you also want a recipe card that users can save to a collection. You could build a custom element <save-button> that extends <button> and adds a saved state. This gives you the best of both worlds: native semantics for the content and a reusable, encapsulated widget for the interactive part.

Implementation Path After the Choice

Once you have chosen your approach, follow these steps to implement it correctly. We will assume you are starting with semantic HTML and adding custom elements where needed.

Step 1: Write Semantic HTML First

Before writing any CSS or JavaScript, write the HTML for your page using the most appropriate elements. Use <header>, <nav>, <main>, <article>, <section>, <aside>, and <footer> for structure. Use <button> for actions, <a> for navigation, and <form> with <input>, <select>, and <textarea> for user input. This gives you a solid foundation that works even without JavaScript.

Step 2: Style with CSS, Using the Cascade

Style the native elements directly. For example, to style a button, target button or input[type='submit']. Avoid using <div> for buttons—it is a common anti-pattern. If you need a custom look for a <select>, consider using the appearance: none property and custom arrows, but test across browsers. For elements that are hard to style, like <input type='file'>, you can use a <label> trick to trigger the native picker while hiding the ugly input.

Step 3: Add Behavior with JavaScript, Progressively

Add JavaScript to enhance the native behavior. For example, to make a button open a modal, add a click event listener. Do not replace the button with a <div> just because you need a click handler. Use the <button> element and let the browser handle keyboard and focus automatically.

Step 4: Create Custom Elements for Reusable Patterns

If you find yourself repeating the same pattern (like a tooltip or a toggle switch), create a custom element. Use the customElements.define() API and extend HTMLElement or a specific HTML element. For example, to create a toggle switch, you could extend <input type='checkbox'> using the is attribute, but browser support is limited. Instead, create a new element that wraps a hidden checkbox and uses ARIA roles to communicate the state.

Step 5: Test for Accessibility and Form Behavior

Test your implementation with a screen reader (like NVDA or VoiceOver) and keyboard-only navigation. Ensure that all interactive elements are reachable and operable. For form elements, verify that they submit the correct data and that validation messages appear. If you used custom elements, check that they participate in form submission by implementing the ElementInternals API.

Risks If You Choose Wrong or Skip Steps

Choosing the wrong approach or skipping the planning step can lead to several problems. Here are the most common ones we see in practice.

Risk 1: Inaccessible Interfaces

The biggest risk is creating a UI that is not usable by people who rely on assistive technology. If you replace a native <button> with a <div> and forget to add role='button' and keyboard handlers, the element becomes invisible to screen readers and unusable with a keyboard. This is not just a compliance issue—it excludes real users. We have seen projects that had to be rewritten because the custom dropdown did not work with screen readers.

Risk 2: Performance Bloat

Using framework components for everything can bloat your bundle size. A React component that renders a <div> with an onClick handler might be small, but if you have hundreds of them, the overhead adds up. Additionally, if you use a component library like Material-UI, you might import the entire library for just a few components. This slows down initial load time and hurts user experience, especially on mobile.

Risk 3: Maintenance Nightmares

When you build custom components that wrap native elements, you create an abstraction. If the underlying native element has a bug or a new feature, your abstraction might hide it. For example, if a new version of Chrome adds a feature to <input type='date'>, your custom date picker will not benefit from it unless you update your code. This is why we prefer to use native elements directly and only wrap them when necessary.

Risk 4: Form Submission Failures

Custom elements that are not form-associated do not submit their data automatically. If you build a custom input component and forget to implement the form-associated pattern, the user's data will not be sent when the form is submitted. This is a subtle bug that can go unnoticed during development and cause data loss in production.

How to Mitigate These Risks

The best mitigation is to follow the implementation path above: start with native elements, test early, and only add custom components when you have a clear need. Also, include accessibility and form testing in your CI pipeline to catch issues before they reach production.

Mini-FAQ: Common Questions About Built-in Elements

Here are answers to questions we often hear from developers.

Can I style a <select> element consistently across browsers?

Not perfectly. The appearance: none property removes the default styling, but you still have limited control over the dropdown list. For a consistent look, you might need a custom solution, but weigh the accessibility cost. If you need a styled select, consider using the <datalist> element for autocomplete, or a custom element that uses a listbox pattern with ARIA roles.

Should I use <div> or <span> for a custom widget?

Generally, no. Use <button> for clickable actions, <a> for navigation, and <input> for user input. If you must use a <div> (for example, for a custom tooltip that is not interactive), add the appropriate ARIA role and ensure it is not focusable unless needed. But always ask: is there a native element that does this?

Do custom elements work with all frameworks?

Yes, custom elements are part of the web platform and work with any framework (React, Vue, Angular, Svelte, etc.). However, some frameworks have quirks. For example, React requires you to use class instead of className for custom elements, and you may need to use ref to access the element directly. Test your custom elements in the context of your framework.

What about shadow DOM? Should I use it?

Shadow DOM provides style encapsulation, which can be useful for reusable widgets. However, it also breaks global styles and can make it harder to theme components. For simple projects, we recommend avoiding shadow DOM unless you need strict encapsulation. You can achieve style scoping with CSS naming conventions (like BEM) instead.

How do I make a custom element participate in form submission?

Use the ElementInternals API. In your custom element's constructor, call this.attachInternals() to get an ElementInternals object. Then set the form value using internals.setFormValue(value). This allows the custom element to work like a native form element. Note that this API is supported in modern browsers (Chrome, Firefox, Safari) but check compatibility for older browsers.

Recommendation Recap Without Hype

Here is a straightforward summary of what we recommend for most projects.

Start with Semantic HTML

Write your markup using the most appropriate native elements. This gives you a solid baseline that is accessible, performant, and maintainable. You can always add custom elements later if needed.

Use Custom Elements for Reusable Interactive Patterns

If you find yourself repeating a pattern (like a tooltip, a toggle, or a custom select), create a custom element. This keeps your code DRY and encapsulated. Prefer extending native elements when possible (e.g., <button is='my-button'>), but fall back to custom elements that wrap native ones if browser support is an issue.

Avoid Framework Components for Simple Elements

Do not create a React component for every <button> or <input> just for consistency. Use native elements directly and only wrap them when you need additional logic or styling that cannot be achieved with CSS alone. This reduces bundle size and avoids abstraction overhead.

Test Early and Often

Test your implementation with a screen reader and keyboard navigation from the start. Include accessibility checks in your CI pipeline. If you use custom elements, test form participation and ensure they work without JavaScript as much as possible.

Next Steps

If you are starting a new project today, begin by writing the HTML structure with semantic elements. Then style it with CSS. Then add JavaScript for interactivity, using native event handlers. Only when you hit a limitation should you reach for custom elements or framework components. This approach will save you time, reduce bugs, and make your site more inclusive.

Share this article:

Comments (0)

No comments yet. Be the first to comment!