Skin HTML forms with CSS
by ZOODUCK / Wesley Pumpkinhead on 14-08-2015

HTML form element such as <select> and <input> have been around since HTML was first invented in 1832. Their functionality is at the core of any interactive website, but unlike other HTML elements they have their own native skins or styles (as defined by the browser engine).

In the same way that each browser has its own individual style of UI (search bar, menus etc.) each browser has its own set of styles for HTML form elements. This has always been a minor nuisance to developer designers like myself, but with the advent of single page web apps, creating a consistent cross-browser UI became critical to achieving a professional result.

The table below illustrates the default styles assigned by your browser for the <select> element and 4 variations of the <input> element (checkbox, radio, button and text).

Your browser defaults for form elements
selectcheckboxradiobuttontext

The default size of form elements can be altered by changing the font-size (for select, button, text etc.) or using the scale method of the CSS transform property (for radio or checkbox).

Unfortunately, not all browsers use vector graphics to display form elements - as illustrated in the table below.

Oi! Firefox! - NO!
Browserradiocheckbox
Chrome 44 chrome_radio chrome_checkbox
Firefox 40 firefox_radio firefox_checkbox
IE11, Edge ie11_radio ie11_checkbox

In fact, it was Firefox's deplorable non-scalable form elements that first got me seriously interested in looking for alternative solutions.

It has to be said that applying styles to HTML form elements is a little more complicated than applying styles to other HTML elements. It requires using pseudo-classes and psuedo-elements, and some properties which you might not have encountered before.

So if you only have a basic understanding of CSS, you might struggle to decipher the examples below. For intermediate level and above, it should take no more than a few hours for you to grasp the key principles involved and be ready to start making your own HTML form skins.

The form elements covered in this article are:

Tagtyperequired
selectHTML, CSS
inputcheckboxHTML, CSS
inputradioHTML, CSS
inputbuttonHTML, CSS
inputtextHTML, CSS, JavaScript

HTML FORM <select>

CSS

.my-select-wrapper{
	position:relative;
	width:360px;
}
.my-select-wrapper::after{
	position:absolute;
	top:10px;
	right:25px;
	width:15px;
	height:15px;
	border:solid #222;
	border-width:0px 3px 3px 0px;
	content:"";
	pointer-events:none;
	transform:rotate(45deg);
	-moz-transform:rotate(45deg);
	-webkit-transform:rotate(45deg);
	-ms-transform:rotate(45deg);
	-o-transform:rotate(45deg);
}
select.my-skin-style{
	position:relative;
	left:0px;
	top:0px;
	width:100%;
	background:tomato;
	color:#222;
	border:0px none;
	font:normal 32px calibri, sans-serif;
	-moz-appearance:none; /* hide dropdown arrow in Firefox */
	-webkit-appearance:none; /*  hide dropdown arrow in Chrome */
	-ms-appearance:none;
	-o-appearance:none;
}
select.my-skin-style::-ms-expand{
	display:none; /* hide dropdown arrow in IE10+ */
}
select.my-skin-style:focus{
	outline:solid 2px #222;
}
select.my-skin-style:hover{
	outline:solid 2px #222;
}
option.my-skin-style{
	background:palegoldenrod;
	font:normal 32px calibri, sans-serif;
}

HTML

<div class="my-select-wrapper">
	<select class="my-skin-style">
		<option class="my-skin-style">Pomidorka</option>
		<option class="my-skin-style">Begemotik</option>
		<option class="my-skin-style">Obezyanka</option>
		<option class="my-skin-style">Utochkachok</option>
		<option class="my-skin-style">Homyachok</option>
	</select>
</div>

Explanation

To create the icon we use a <div> element with a partial border and the rotate method of its transform property.

Fig. 1
Fig. 2
Fig. 3

By placing the <select> inside a relatively positioned <div> wrapper, we can use the ::after pseudo-element of the wrapper to create the icon and place it 5px from the far right of the <select> using position:absolute; right:5px;

Hiding the default dropdown arrow is achieved in Firefox and Chrome using -moz-apperance:none and -webkit-appearance:none respectively. For IE10+ you will need to use the -ms-expand pseudo-element of the <select> instead, and set its display property to none.

The last step is to give the pointer-events property a value of none. This allows click events on the icon to filter through to the <select> and activate the dropdown list. (If you don't set this property, the dropdown functionality of the <select> will still work, but not when the icon is clicked on).

HTML FORM <input> checkbox

input.checked = true

CSS

input[type=checkbox].my-skin-style{
	display:none;
}
input[type=checkbox].my-skin-style:checked + .my-checkbox-icon{
	background:tomato;
}
input[type=checkbox].my-skin-style:checked + .my-checkbox-icon::after{
	opacity:1;
}
.my-checkbox-icon{
	position:relative;
	width:40px;
	height:40px;
	border:solid 3px tomato;	
}
.my-checkbox-icon::after{
	position:absolute;
	right:0px;
	width:25%;
	height:75%;
	border:solid #fff;	
	border-width:0px 8px 8px 0px;
	opacity:0;
	transform-origin:50% 10%;
	transform:rotate(45deg);
	-moz-transform:rotate(45deg);
	-webkit-transform:rotate(45deg);
	-ms-transform:rotate(45deg);
	-o-transform:rotate(45deg);
	content:"";
}

HTML

<label id="checkbox_example">
	<input
		class="my-skin-style"
		id="checkbox_example"
		type="checkbox" 
		checked="checked" />
	<div class="my-checkbox-icon"></div>
</label>

Explanation

The checkbox and radio elements are different in that they hold a boolean state of checked (true or false). In order for our custom checkbox (which is nothing but an ordinary <div>) to have the same functionality, we need somehow to be able to affect the state of the real checkbox (which will be hidden) by clicking on the custom checkbox.

The solution is to place both elements inside a <label> wrapper. The <label> element is unique in that it will automatically transfer click events to the first form element that is an immediate child or sibling of itself. In other words, clicking anywhere inside or on the label will have the exact same effect as clicking on the checkbox itself.

To create the icon we use a <div> element with a border (to form the box) and the ::after pseudo-element to add a rectangle with a partial border and set the rotate method of its transform property to 45deg. You will also need to set the x-axis and y-axis of the transform-origin property to center the checkmark icon.

Fig. 4
Fig. 5
Fig. 6

We next place both checkbox elements inside a <label> element (Fig. 7), and set the display property of the real checkbox to none (Fig. 8). Then we alter the checkmark icon's style so that it's hidden by default (opacity:0) and use the ::checked pseudo-class (of the real checkbox) along with the + combinator on both the checkmark icon element and it's ::after pseudo-element to set the background color and make the checkmark icon visible when checked.

Fig. 7
Fig. 8
Fig. 9

HTML FORM <input> radio

radio.value = 1

CSS

label.my-radio-label{
	float:left;
	padding:0px 10px;
}
label.my-radio-label::after{
	clear:both;
	display:block;
	content:"";
}
input[type=radio]{
	display:none;
}
input[type=radio]:checked + .my-radio-icon::after{
	opacity:1;				
}
.my-radio-icon{
	position:relative;
	width:40px;
	height:40px;
	border:solid 3px tomato;
	border-radius:50%;
	background:transparent;
}
.my-radio-icon::after{
	position:absolute;
	left:15%;
	top:15%;
	width:70%;
	height:70%;
	background-color:#fff;
	border-radius:50%;
	opacity:0;
	content:"";
}

HTML

<label class="my-radio-label">
	<input class="my-skin-style" type="radio" name="radio-example" value="1" />
	<div class="my-radio-icon"></div>
</label>
<label class="my-radio-label">
	<input class="my-skin-style" type="radio" name="radio-example" value="2" />
	<div class="my-radio-icon"></div>
</label>

Explanation

The radio button works in exactly the same way as the checkbox. But instead of using a rectangle inside a rectangle, we use a circle inside a circle. (You create circles with CSS by using a border-radius property of 50%).

HTML FORM <input> button

CSS

input[type=button].my-skin-style{
	position:relative;
	background:tomato;
	color:#222;
	font:normal 32px calibri, sans-serif;
	border:none;
	outline:none;
	box-shadow:7px 7px #222;
	padding:5px 0px;
	width:100%;
}
input[type=button].my-skin-style:hover{
	outline-offset:-2px;
	outline:solid 2px #222;
}
input[type=button].my-skin-style:focus{
	outline-offset:-2px;
	outline:solid 2px #222;
}
.my-button-icon{
	position:absolute;
	top:5px;
	right:5px;
	width:10px;
	height:23px;
	border:solid #fff;
	border-width:10px 3px 0px 0px;
	pointer-events:none;
}
input[type=button].my-skin-style:active{
	transform:scale(0.95, 0.95);
	box-shadow:none;
}
input[type=button].my-skin-style:active + .my-button-icon{
	transform:scale(.95, .95);
	right:10px;
}

HTML

<div class="my-button-wrapper">
	<input class="my-skin-style" type="button" value="Button" />
	<div class='my-button-icon'></div>
</div>

Explanation

To create the button skin I disabled the outline and border, assigned new values to the background and font, and added a box-shadow.

When you mousedown on a button element, its state becomes active until you release the mouse button. I used the :active psuedo-class to reduce the size of the button transform:scale(.95, .95) and remove the box-shadow when the button is pressed.

I also used the :hover and :focus pseudo-classes to add an outline with a negative outline-offset (to keep the outline inside the button).

The button "icon" is added as a separate element (instead of the ::after pseudo-element of the wrapper) so that we can reference it using the + combinator of the :active psuedo-class of the button, allowing us to apply the same transform property to both elements at the same time.

HTML FORM <input> text

Enter some text then click or tab outside to see the icon change

CSS

.my-text-wrapper{
	position:relative;
	float:left;
}				
.my-text-wrapper::after{
	position:absolute;
	top:7px;
	right:7px;
	padding:0px 5px;
	font:bold 32px calibri, sans-serif;
	color:#fff;
	pointer-events:none;
	content:"";
}
.my-text-wrapper.required::after{
	background:coral;
	content:"*";
}
.my-text-wrapper.warning::after{
	background:#e13718;
	content:"!";
}
.my-text-wrapper.success::after{
	background:#3dbb3d;
	content:"OK";
}
input[type=text].my-skin-style{
	font:normal 32px calibri, sans-serif;
	padding:5px 60px 5px 5px;
	text-transform:uppercase;
}
input[type=text].my-skin-style:focus{
	outline:none;
	border:solid 2px #222;
}
input[type=text].my-skin-style:hover{
	outline:none;
	border:solid 2px #222;
}

HTML

<div class="my-text-wrapper required">
	<input class="my-skin-style" type="text" placeholder="Currency (USD, GBP etc.)" maxlength="3" onblur="validateText(this)" />
</div>

JavaScript

<script>
function validateText(textInput){
	if(textInput.value == ""){
		textInput.parentNode.className = textInput.parentNode.className.replace(/( warning| success)/g, "");
		textInput.parentNode.className += " required";
	}else if(textInput.value.match(/[A-Za-z]{3}/)){
		// positive match of 3 alpha characters
		textInput.parentNode.className = textInput.parentNode.className.replace(/ warning/g, "");
		textInput.parentNode.className += " success";
	}else{
		textInput.parentNode.className = textInput.parentNode.className.replace(/ success/g, "");
		textInput.parentNode.className += " warning";
	}
}
</script>

Explanation

The text <input> has a very neutral style. To create a uniform look across browsers requires only that you set the outline property to none and replace it with a border.

(We disable the outline property and use the border property instead because certain browsers have pre-defined and engine-specific styles for the :focus psuedo-element. For example, Chrome 44 uses: outline:focus{ webkit-focus-ring-color auto 5px; } That is the rule responsible for the unsightly fuzzy blue border that you get whenever focus is applied to a form element in Chrome!)

However, if you require form validation and a more fancy look you will need to use a combination of CSS and JavaScript.

The first thing is to create different classes for the different states of validation - Required, Warning and Success. I have created the icons by using the ::after pseudo-element of the <div> wrapper, because it allows us to specify a different content value (text) for each class. (The alternative would be to use three separate elements - one for each validation state, then show or hide them as required).

Fig. 10
Fig. 11
Fig. 12

In this example we are validating user input to currency. The JavaScript uses a simplified RegExp that returns true if it matches 3 alpha characters, or false for any other input. (In a real-world situation you would of course use a more complicated RegExp or switch statement to match specific currencies).

I have added my script function validateText to the onblur attribute of the input, so that it fires whenever the input loses focus (such as when the tab key is pressed, or mouse is clicked outside the element).

The function itself is a simple if else statement which says if the <input> is empty then show the required icon (Fig. 10), if it matches the RegExp pattern (3 alpha characters) then show the success icon (Fig. 11), or if both conditions fail then show the warning icon (Fig.12).

Additionally in this example I have set the text-transform property of the <input> to uppercase (so that text will always appear in upper-case) and the maxlength attribute to "3" (to prevent more than 3 characters from being entered).