VOOZH about

URL: https://blog.logrocket.com/style-forms-css/

⇱ How to style forms with CSS: A beginner’s guide - LogRocket Blog


2024-02-13
2135
#css
Supun Kavinda
15792
👁 Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

Editor’s note: This article was last updated by Rahul Chhodde on 13 February 2024 to cover integrating responsive design into forms, as well as information about styling form elements like buttons and labels using CSS properties and a few notable CSS frameworks.

👁 How To Style Forms With CSS: A Beginner’s Guide

Websites and apps rely on forms to gather data from users. For example, a typical login form provides users with dedicated fields to collect their username, password, and a login button.

It is crucial for frontend developers to understand how to properly style form elements while ensuring accessibility. With this knowledge, you can create more engaging forms using the latest CSS features and a little creativity.

Form elements appear plain and simple by default to offer you a neutral foundation with basic accessibility features for building your unique UI. Here’s an example of an unstyled HTML form:

See the Pen HTML form elements with no CSS styles by Rahul (@_rahul)
on CodePen.


The form above might look slightly different across browsers. This is due to the different interpretations of rendering and web standards as well as optimizing HTML elements according to the browser UI.

In this tutorial, you’ll learn how to recreate HTML forms, ensuring cross-browser compatibility and enhancing them for visual appeal and user-friendliness. The process involves establishing a baseline for form elements and refining them step-by-step with interactive states and animations.

We’ll use SCSS nesting to streamline the CSS process; all the SCSS and the generated CSS code involved here are accessible on this GitHub repository.

🚀 Sign up for The Replay newsletter

The Replay is a weekly newsletter for dev and engineering leaders.

Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

Setting the box-sizing

Modern browsers use a padding box for form inputs (especially text fields), which excludes padding and borders from declared dimensions. This can lead to unexpected behavior, especially in responsive layouts.

Setting border-box as the box-sizing for all elements ensures consistent behavior across browsers:

:root {
 box-sizing: border-box;
}

*,
*::before,
*::after {
 box-sizing: inherit;
}

Form-specific CSS selectors

Let’s discuss CSS selectors that are used exclusively to select form elements. You may already be familiar with some of these; we’ll go through them quickly and use these techniques later when styling the form elements.

Type-based selection

You’re probably familiar with the different input elements used in forms, each designated by the input tag with a specific type attribute.

Here’s a simple HTML form demonstrating the structure of a login form using various types of HTML input elements:

<form>
 <input type="text" placeholder="Username" />
 <input type="password" placeholder="Password" />
 <input type="submit" value="Login" />
</form>

As you can see, each input element has a type attribute set to its specific function: "text" for username, "password" for password entry, and "submit" for the button. These attributes allow us to target them individually with CSS rules as shown below:

input[type="text"] { ... }
input[type="password"] { ... }
input[type="submit"] { ... }

Pseudo-elements

In CSS, form inputs do not support insertion with ::before and ::after pseudo-elements. The only pseudo-elements supported by inputs are as follows, primarily used for decorating elements rather than inserting content:

  • ::placeholder and ::selection: Supported by text inputs to style placeholder text and text selection, respectively
  • ::file-selection-button: Supported only by native HTML file inputs to style the button in the file input

You’ll see the implementation of some of these pseudo-elements in a later section when customizing these particular input types. Check out this guide for more information about CSS pseudo-elements.

Note: This article excludes vendor-specific pseudo-elements because they lack standardization and can’t guarantee consistent appearance across different browsers.

Pseudo-classes

In addition to :hover, :active, and :focus, other pseudo-classes help us quantify the validation and perceive different states of input elements. Here’s a quick list of these pseudo-classes, which are fairly self-explanatory:

/* Validation */
input:valid {}
input:invalid {}

/* Active and inactive */
input:enabled {}
input:disabled {}

/* Required inputs */
input:required {}

/* Read-only text inputs */
input:read-only {}

/* Inputs with their value to be autofilled by the browser */ 
input:autofill {}

Setting defaults

You can write custom default styles or use a pre-built CSS reset like normalize.css for consistent form styling across browsers. Here, we’ll focus on the manual method.

First, let’s identify all the key input elements, which you can find in this SCSS file, and set their font and color properties to inherit from their parent elements. Typically, this involves inheriting from the body element’s font-family and font-size.

We can then specify properties that require explicit definitions to differentiate elements from their parent’s styles, such as a shorter line height for form elements:

input,
select,
button,
...
input::file-selector-button {
 font: inherit;
 color: inherit;
 line-height: 120%;
}

Next, let’s add a pointer cursor to the actionable form inputs like buttons. We may also do the same with the label element, but for now, let’s just focus on the buttons:

button,
input[type="button"],
input[type="submit"],
input[type="reset"],
input::file-selector-button {
 cursor: pointer;
}

Finally, let’s add CSS properties to standardize the shape, size, and spacing of the elements. We should avoid styling the input tag directly to prevent applying unconventional styles to all the input types, like borders on checkboxes and radio elements.

Specifying the input type for selection is the right way to apply CSS to similar input elements. Here’s a simple demonstration of type selection to shape things up:

:root {
 --width-input-border: 2px;
 --radius-inputs: 0.25em;
 --padding-inputs: 0.75em;
}

select,
textarea,
input[type="text"]
... {
 padding: var(--padding-inputs);
 border: var(--width-input-border) solid;
 border-radius: var(--radius-inputs);
}

Adding a solid border for text fields, select boxes, and buttons will keep them equal in height. Adding a slight border-radius will make them look more polished, and padding will provide some breathing room for the buttons.

Tip: Prioritize using CSS custom properties whenever possible to simplify organization, maintenance, and customization.

States and accessibility

Some state-based styles, like focus outline and disabled element fading, are often handled by user-agent styles, which provide consistent visual hints for websites. These hints are expected by the users too, who rely on them to navigate a webpage.

For optimal accessibility and user experience, it’s important to carefully decide what states of form elements to depend on browser defaults — such as focus outlines — and which ones require manual styling, such as custom cursors, and disabled, required, or read-only styles:

:root {
 --opacity-input-disabled: 0.5;
}

:read-only { cursor: default }
:disabled {
 opacity: var(--opacity-input-disabled);
 cursor: not-allowed;
}

The above rules ensure that read-only inputs have the default cursor and disabled ones have reduced opacity with a default arrow cursor. When styling individual form elements, we’ll address hover, focus, and active states.


Over 200k developers use LogRocket to create better digital experiences

👁 Image
Learn more →

Responsive form elements

We’ll combine width and max-width to size elements, ensuring they stay within the defined max-width (100%) for a responsive design (RWD) approach. Consider setting a minimum height for textareas to prevent an awkwardly wide but short appearance:

:root {
 ...
 --width-inputs: 250px;
 --width-textarea: 450px;
 --height-textarea: 250px;
}

select,
input[type="text"],
... {
 width: var(--width-inputs);
 max-width: 100%;
}

textarea {
 width: var(--width-textarea);
 min-height: var(--height-textarea);
 max-width: 100%;
}

Styling form elements

Now, let’s add some style to the elements individually, and also tweak them when they get paired with certain elements. Most of our structuring and shaping work is done; the remaining tasks primarily involve refining colors and cosmetics.

Basic theming

Let’s first start with managing the color themes using CSS custom properties:

:root {
 ...
 --co-body-accent: #07c;
}

Targeting and theming text, borders, outlines, and backgrounds using relevant properties is straightforward. However, certain areas and elements, such as the checked state of radio buttons and checkboxes, are controlled by default by the operating system or browser and cannot be customized using regular CSS properties.

The accent-color CSS property comes in handy here. Applying it to elements that typically rely on browser or OS accent colors for their default styling allows you to somewhat alter their appearance:

input,
select,
button,
textarea,
...
input::file-selector-button {
 accent-color: var(--co-body-accent);
}

Consider using the dark value for the color-theme property when implementing dark mode. This instructs the browser to utilize optimized UI decorations for various elements, including form inputs, in dark interfaces. It ensures that decorations in elements like number fields, date pickers, etc., contrast nicely with the dark theme:

body {
 color-scheme: dark;
}

Text fields

Setting border colors and defining different borders for hover, active, and focus states can help guide users regarding the various states of text inputs, including textareas.

Because we’ll use borders to emphasize various states, hiding the default browser outline added on focus is a good idea. We’ll achieve this by specifying it through type-based selection, as demonstrated below:

:root {
 --co-textfld-bg: #222;
 --co-textfld-border: #333;
 --co-textfld-active-border: #444;
 --co-textfld-focus-border: var(--co-body-accent);
}

select,
textarea,
input[type="text"],
... {
 ...
 border: var(--width-input-border) solid var(--co-textfld-border);
 background-color: var(--co-textfld-bg);
 &:focus {
 outline: 0;
 }
}

select,
textarea,
input[type="text"],
... {
 &:hover,
 &:active {
 border-color: var(--co-textfld-active-border);
 }

 &:focus {
 border-color: var(--co-textfld-focus-border);
 }
}

Buttons

Similar to text fields, we should also add colors to our buttons. The background and border colors will be the same, with an additional text color added to provide contrast between the text and the background:

:root {
 --co-btn-text: #fff;
 --co-btn-bg: var(--co-body-accent);
 --co-btn-active-bg: #333;
 --co-btn-focus-bg: #333;
}

button,
input[type="button"],
...
input[type="file"]::file-selector-button {
 border-color: var(--co-btn-bg);
 background-color: var(--co-btn-bg);
 color: var(--co-btn-text);

 &:hover,
 &:active {
 background-color: var(--co-btn-active-bg);
 border-color: var(--co-btn-active-bg);
 }
 &:focus {
 background-color: var(--co-btn-active-bg);
 }
}

We ensured that the file selector button for file inputs is styled like regular button inputs by using the ::file-selector-button pseudo-element.

Radio and checkbox inputs

While our initial setup automatically optimizes the UI of radio and checkbox buttons, nesting them within their label elements creates an even cleaner and more efficient appearance:

<label for="my-radio">
 <input id="my-radio" type="radio" />
 <span>Radio input with a label</span>
</label>

<label for="my-checkbox">
 <input id="my-checkbox" type="checkbox" />
 <span>Checkbox input with a label</span>
</label>

File input

We have already addressed most aspects of file inputs when setting defaults except the spacing between the file selector button and the label necessitates additional styling. Here’s how you can achieve it:

:root {
 --margin-form-gap: 1.5em;
}

input::file-selector-button { margin-right: var(--margin-form-gap) }

Labels

Converting the default labels to block elements allows inputs to position naturally below them without modifying the default display of inputs, ensuring a clean and efficient design.

Also, spacing between the input and label improves form readability and scannability. These adjustments can be implemented as follows:

:root {
 --margin-label: 0.5em;
}

label {
 cursor: pointer;
 display: block;

 & + &,
 & + input,
 & + select,
 & + button,
 & + textarea {
 margin-top: var(--margin-label);
 }
}

Form validation

We can achieve basic form validation with visual feedback using just CSS and HTML functionalities.

By adding the required attribute to essential fields, we can then target them all using the :required pseudo-class and then combine them with the :valid and :invalid pseudo-classes to provide clear visual cues for valid and invalid input:

:root {
 ...
 --co-textfld-valid-border: hsl(140 90% 20%);
 --co-textfld-valid-active-border: hsl(140 90% 30%);
 --co-textfld-valid-focus-border: hsl(140 90% 45%);

 --co-textfld-invalid-border: ...;
 ...
}

select,
textarea,
input[type="text"],
... {
 ...

 &:required {
 &:valid {
 &:hover,
 &:active {
 &:not([readonly], [disabled]) {
 border-color: var(--co-textfld-valid-active-border);
 }
 }
 &:focus {
 &:not([readonly], [disabled]) {
 border-color: var(--co-textfld-valid-focus-border);
 }
 }
 }

 &:invalid {
 /* Similar steps as above */
 }
 }
}

Animating states

In this article, we won’t delve deeply into animations. Instead, we’ll utilize the transition property to create smooth color transitions for text, borders, and backgrounds:

:root {
 --transition-duration-inputs: 250ms;
 --transition-function-inputs: ease-in-out;

 --transition-inputs: 
 color var(--transition-duration-inputs),
 background-color var(--transition-duration-inputs),
 border-color var(--transition-duration-inputs)
 var(--transition-function-inputs);
}

input,
button,
select,
...
input::file-selector-button {
 transition: var(--transition-inputs);
}

Grouping and alignment

A clear separation is vital for establishing a layout hierarchy on larger forms. Introduce this separation strategically within each form area, grouping similar inputs and elements using div containers. This distinction ensures they visually stand apart from identical siblings, enhanced by appropriate margin application:

.form-row {
 & + & {
 margin-top: var(--margin-row-gap);
 }
}

For side-by-side inputs, wrap them in a flexbox and set its flex-wrap property to wrap. This automatically arranges elements vertically when they don’t fit horizontally:

.form-row,
.btn-group {
 display: flex;
 flex-wrap: wrap;
}

.form-row { gap: var(--margin-row-gap) }
.btn-group { gap: var(--margin-btn-gap) }

I’ve combined everything into a CodePen demo below with a few more optimizations. Play around and see how easily you can spin up a dark mode with CSS custom properties:

See the Pen Form Elements styles w/ Pure CSS by Rahul (@_rahul)
on CodePen.

Styling form elements using CSS frameworks

Frameworks like Tailwind CSS, Bootstrap, and Bulma offer resets and extensive utility classes. However, effectively using them requires careful consideration:

  • Tailwind CSS provides a dedicated plugin to implement beautiful forms in your design prototypes quickly
  • Bootstrap offers comprehensive form documentation, dedicating an overview page to forms for easy understanding
  • Bulma’s documentation covers form elements and provides ready-made examples for clarity

Conclusion

With this article, you should hopefully now have a grasp of styling form elements with CSS. These techniques serve as the foundation for more advanced CSS form styling.

These strategies can be leveraged to develop highly customized and accessible controls using popular JavaScript frameworks and WAI-ARIA tech. Find out all the code discussed above in this GitHub repo, and feel free to ask questions in the comments.

Is your frontend hogging your users' CPU?

As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, try LogRocket.

👁 LogRocket Dashboard Free Trial Banner

LogRocket lets you replay user sessions, eliminating guesswork around why bugs happen by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks.

LogRocket's Galileo AI watches sessions for you, instantly identifying and explaining user struggles with automated monitoring of your entire product experience.

Modernize how you debug web and mobile apps — start monitoring for free.

👁 Image
👁 Image
👁 Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

What is TSRX?: What JSX would look like if it were designed today

TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension — no new framework required.

👁 Image
Ikeh Akinyemi
Jun 12, 2026 ⋅ 6 min read

How to add authentication to a React Native app with Better Auth

Learn how to build a full React Native auth system using Better Auth and Expo — with email/password login, Google OAuth, session persistence, and protected routes.

👁 Image
Chinwike Maduabuchi
Jun 9, 2026 ⋅ 13 min read

AI dev tool power rankings & comparison [June 2026]

Compare the top AI development tools and models of June 2026. View updated rankings, feature breakdowns, and find the best fit for you.

👁 Image
Chizaram Ken
Jun 8, 2026 ⋅ 11 min read

How to check username availability at scale with Bloom filters

Learn how Bloom filters reduce database lookups for username availability checks while preserving correctness at scale.

👁 Image
Rosario De Chiara
Jun 8, 2026 ⋅ 6 min read
View all posts

Hey there, want to help make our blog better?

Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.

Sign up now