Wow! Dealing with all these workarounds has made me realize that styling the HTML checkbox can be quite challenging.
Just a heads up, this isn't a CSS solution. I thought I'd share the workaround I came up with in case it could be useful to someone else.
My solution involved using the canvas
element in HTML5.
The benefit of this approach is that you can avoid using external images and potentially save on bandwidth.
The downside is that if a browser is unable to render the canvas correctly, there's no fallback option. However, this might not be as big of an issue in 2017.
Update
I found the original code to be quite messy, so I decided to give it a revamp.
Object.prototype.create = function(args){
var newObj = Object.create(this);
newObj.constructor(args || null);
return newObj;
}
var Checkbox = Object.seal({
width: 0,
height: 0,
state: 0,
document: null,
parent: null,
canvas: null,
ctx: null,
/*
* args:
* name default desc.
*
* width 15 width
* height 15 height
* document window.document explicit document reference
* target this.document.body target element to insert checkbox into
*/
constructor: function(args){
if(args === null)
args = {};
this.width = args.width || 15;
this.height = args.height || 15;
this.document = args.document || window.document;
this.parent = args.target || this.document.body;
this.canvas = this.document.createElement("canvas");
this.ctx = this.canvas.getContext('2d');
this.canvas.width = this.width;
this.canvas.height = this.height;
this.canvas.addEventListener("click", this.ev_click(this), false);
this.parent.appendChild(this.canvas);
this.draw();
},
ev_click: function(self){
return function(unused){
self.state = !self.state;
self.draw();
}
},
draw_rect: function(color, offset){
this.ctx.fillStyle = color;
this.ctx.fillRect(offset, offset,
this.width - offset * 2, this.height - offset * 2);
},
draw: function(){
this.draw_rect("#CCCCCC", 0);
this.draw_rect("#FFFFFF", 1);
if(this.is_checked())
this.draw_rect("#000000", 2);
},
is_checked: function(){
return !!this.state;
}
});
Check out the live demo here.
The updated version utilizes prototypes and differential inheritance to create an efficient system for generating checkboxes. To create a checkbox:
var myCheckbox = Checkbox.create();
This will instantly add the checkbox to the DOM and set up the events. To check if a checkbox is ticked:
myCheckbox.is_checked(); // Returns true if checked, otherwise false
It's also worth noting that I eliminated the loop.
Update 2
In my previous update, I forgot to mention that utilizing the canvas offers more benefits than just customizing the checkbox appearance. You can even create checkboxes with multiple states, if desired.
Object.prototype.create = function(args){
var retObj = Object.create(this);
retObj.constructor(args || null);
return retObj;
}
Object.prototype.extend = function(newObj){
var oldObj = Object.create(this);
for(property in newObj)
oldObj[property] = newObj[property];
return Object.seal(oldObj);
}
var Checkbox = Object.seal({
width: 0,
height: 0,
state: 0,
document: null,
parent: null,
canvas: null,
ctx: null,
/*
* args:
* name default desc.
*
* width 15 width
* height 15 height
* document window.document explicit document reference
* target this.document.body target element to insert checkbox into
*/
constructor: function(args){
if(args === null)
args = {};
this.width = args.width || 15;
this.height = args.height || 15;
this.document = args.document || window.document;
this.parent = args.target || this.document.body;
this.canvas = this.document.createElement("canvas");
this.ctx = this.canvas.getContext('2d');
this.canvas.width = this.width;
this.canvas.height = this.height;
this.canvas.addEventListener("click", this.ev_click(this), false);
this.parent.appendChild(this.canvas);
this.draw();
},
ev_click: function(self){
return function(unused){
self.state = (self.state + 1) % 3;
self.draw();
}
},
draw_rect: function(color, offsetX, offsetY){
this.ctx.fillStyle = color;
this.ctx.fillRect(offsetX, offsetY,
this.width - offsetX * 2, this.height - offsetY * 2);
},
draw: function(){
this.draw_rect("#CCCCCC", 0, 0);
this.draw_rect("#FFFFFF", 1, 1);
this.draw_state();
},
draw_state: function(){
if(this.is_checked())
this.draw_rect("#000000", 2, 2);
if(this.is_partial())
this.draw_rect("#000000", 2, (this.height - 2) / 2);
},
is_checked: function(){
return this.state == 1;
},
is_partial: function(){
return this.state == 2;
}
});
var Checkbox3 = Checkbox.extend({
ev_click: function(self){
return function(unused){
self.state = (self.state + 1) % 3;
self.draw();
}
},
draw_state: function(){
if(this.is_checked())
this.draw_rect("#000000", 2, 2);
if(this.is_partial())
this.draw_rect("#000000", 2, (this.height - 2) / 2);
},
is_partial: function(){
return this.state == 2;
}
});
I made slight adjustments to the previous Checkbox
implementation to make it more versatile, allowing for extension with a 3-state checkbox. Check out the demo here. As you can see, it already offers more functionality than a standard checkbox.
When deciding between JavaScript and CSS, consider these points.
Original Code
Live Demo
First, prepare a canvas:
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'),
checked = 0; // The state of the checkbox
canvas.width = canvas.height = 15; // Set the width and height of the canvas
document.body.appendChild(canvas);
document.body.appendChild(document.createTextNode(' Togglable Option'));
Next, establish a method for updating the canvas automatically:
(function loop(){
// Draws a border
ctx.fillStyle = '#ccc';
ctx.fillRect(0,0,15,15);
ctx.fillStyle = '#fff';
ctx.fillRect(1, 1, 13, 13);
// Fills in canvas if checked
if(checked){
ctx.fillStyle = '#000';
ctx.fillRect(2, 2, 11, 11);
}
setTimeout(loop, 1000/10); // Refresh 10 times per second
})();
Lastly, make it interactive. Fortunately, it's straightforward:
canvas.onclick = function(){
checked = !checked;
}
This may cause issues in IE due to its peculiar event handling model in JavaScript.
I hope this guide proves helpful to someone; it certainly met my requirements.