96

I have a page which has <link> in the header that loads the CSS named light.css. I also have a file named dark.css. I want a button to swap the style of the page all together (there are 40 selectors used in css file and some do not match in two files).

How can I remove reference to light.css with JS and remove all the styles that were applied and then load dark.css and apply all the styles from that? I can't simply reset all of the elements, since some of the styles are applied through different css files and some are dynamically generated by JS. Is there a simple, yet effective way to do that without reloading the page? Vanilla JS is preferable, however I will use jQuery for later processing anyways, so jQ is also fine.

Xeos
  • 5,975
  • 11
  • 50
  • 79
  • 1
    I chose Mattew's answer because with that method, I can easily extend the for() loop to handle multiple files and swap `light` and `dark` in other scripts/stylesheets. – Xeos Nov 07 '13 at 19:53

11 Answers11

86

You can include all the stylesheets in the document and then activate/deactivate them as needed.

In my reading of the spec, you should be able to activate an alternate stylesheet by changing its disabled property from true to false, but only Firefox seems to do this correctly.

So I think you have a few options:

Toggle rel=alternate

<link rel="stylesheet"           href="main.css">
<link rel="stylesheet alternate" href="light.css" id="light" title="Light">
<link rel="stylesheet alternate" href="dark.css"  id="dark"  title="Dark">

<script>
function enableStylesheet (node) {
  node.rel = 'stylesheet';
}

function disableStylesheet (node) {
  node.rel = 'alternate stylesheet';
}
</script>

Set and toggle disabled

<link rel="stylesheet" href="main.css">
<link rel="stylesheet" href="light.css" id="light" class="alternate">
<link rel="stylesheet" href="dark.css"  id="dark"  class="alternate">

<script>
function enableStylesheet (node) {
  node.disabled = false;
}

function disableStylesheet (node) {
  node.disabled = true;
}

document
  .querySelectorAll('link[rel=stylesheet].alternate')
  .forEach(disableStylesheet);
</script>

Toggle media=none

<link rel="stylesheet" href="main.css">
<link rel="stylesheet" href="light.css" media="none" id="light">
<link rel="stylesheet" href="dark.css"  media="none" id="dark">

<script>
function enableStylesheet (node) {
  node.media = '';
}

function disableStylesheet (node) {
  node.media = 'none';
}
</script>

You can select a stylesheet node with getElementById, querySelector, etc.

(Avoid the nonstandard <link disabled>. Setting HTMLLinkElement#disabled is fine though.)

sam
  • 40,318
  • 2
  • 41
  • 37
  • @sam now I am sorry that I can't accept an answer on somebody else's question :)... only, is there any impact on page's performance? Is the disabled link ignored by the browser before that attribute is set to false? – dzenesiz Jul 09 '15 at 00:06
  • 8
    unfortunately setting `disabled` as an attribute is discuraged: "The use of disabled as an HTML attribute is non-standard and only used by some browsers (W3 #27677). Do not use it." -> https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-disabled – Daniel Jul 24 '15 at 09:15
  • @dzenesiz browsers can skip downloading an alternate stylesheet until it's needed but I don't know how they actually behave. – sam Jan 31 '16 at 00:54
  • 3
    Just to clarify what @Daniel said, the HTML attribute is non standard, BUT according to Mozilla, the DOM property IS. So toggling it with javascript in the way described above is, in fact, standard-compliant. Unless I'm missing something this should be the accepted answer. – emersonthis Jan 19 '17 at 19:50
  • Hi @emersonthis, [earlier version](http://stackoverflow.com/posts/19844757/revisions) had used ``. Current version is legit. – sam Jan 21 '17 at 17:49
  • @emersonthis and yes this should be the accepted answer :D – sam Apr 06 '17 at 23:15
  • 1
    The [CSS Object Model](https://www.w3.org/TR/cssom-1/#concept-css-style-sheet-disabled-flag) says this about the `disabled` flag: "Note: Even when unset it does not necessarily mean that the CSS style sheet is actually used for rendering". It's probably the reason why the answer works on Firefox, but not on Webkit & Blink ([Epyphany](https://wiki.gnome.org/Apps/Web) & [Chromium](http://www.chromium.org/)). – Cristian Ciupitu Mar 06 '18 at 12:46
  • @CristianCiupitu that part is unclear, unfortunately. It may refer to other ways a stylesheet could be disabled (e.g., by moving/removing the node). WebKit/Blink **do** apply the `disabled` value (true/false) to default stylesheets. The problem is that they don't initially set the `disabled` flag (true) on alternate stylesheets and they don't apply the `disabled` value to those stylesheets. – sam Mar 06 '18 at 17:18
  • How do the different CSS files get applied? Do they use a button? If so what does the code look like? – GreyHippo Nov 27 '18 at 21:09
  • 1
    Just want to jump in and say that toggling `disabled` with `setAttribute`/`removeAttribute` doesn't work on Firefox. You have to use the `.disabled = value` statement. Hope this helps any else struggle with this. – ShortFuse May 08 '19 at 19:27
85

You can create a new link, and replace the old one with the new one. If you put it in a function, you can reuse it wherever it's needed.

The Javascript:

function changeCSS(cssFile, cssLinkIndex) {

    var oldlink = document.getElementsByTagName("link").item(cssLinkIndex);

    var newlink = document.createElement("link");
    newlink.setAttribute("rel", "stylesheet");
    newlink.setAttribute("type", "text/css");
    newlink.setAttribute("href", cssFile);

    document.getElementsByTagName("head").item(cssLinkIndex).replaceChild(newlink, oldlink);
}

The HTML:

<html>
    <head>
        <title>Changing CSS</title>
        <link rel="stylesheet" type="text/css" href="positive.css"/>
    </head>
    <body>
        <a href="#" onclick="changeCSS('positive.css', 0);">STYLE 1</a> 
        <a href="#" onclick="changeCSS('negative.css', 0);">STYLE 2</a>
    </body>
</html>

For simplicity, I used inline javascript. In production you would want to use unobtrusive event listeners.

Matthew Johnson
  • 4,875
  • 2
  • 38
  • 51
  • 13
    I think simply updating the `href` property might work just as well: `oldlink.href = cssFile; // done` – sam Nov 07 '13 at 21:47
  • True. This replaces the whole link node in the DOM. – Matthew Johnson Nov 07 '13 at 21:51
  • If you're wanting to pull a live-reload type of thing, this is probably the way to go since the href may be the same and the browser may not actually do anything if the href hasn't actually changed. – flcoder Nov 08 '15 at 03:55
  • 3
    while switching css files, the gap in downloading the file and rendering the styles leaves the html without styles. I guess, there should be a default stylesheet, which adds basic styles, always there. Mobile browsers simply take much time to replace the css style sheet. Also the index of link might vary in different pages, given a static website generated by jekyll – Irshad Nov 29 '19 at 15:41
  • You could do an absolute URL to get around pages being in different folders or whatnot. I like the idea of a default style sheet to prevent the FOUC from showing. Nice tip! – Matthew Johnson Dec 01 '19 at 11:57
  • Replace document.getElementsByTagName("head").item(cssLinkIndex).replaceChild(newlink, oldlink); witth document.getElementsByTagName("head").item(0).replaceChild(newlink, oldlink); in case your CSS file is not the first imported. – Alex Sep 14 '21 at 05:46
21

If you set an ID on the link element

<link rel="stylesheet" id="stylesheet" href="stylesheet1.css"/>

you can target it with Javascript

document.getElementsByTagName('head')[0].getElementById('stylesheet').href='stylesheet2.css';

or just..

document.getElementById('stylesheet').href='stylesheet2.css';

Here's a more thorough example:

<head>
    <script>
    function setStyleSheet(url){
       var stylesheet = document.getElementById("stylesheet");
       stylesheet.setAttribute('href', url);
    }
    </script>

    <link id="stylesheet" rel="stylesheet" type="text/css" href="stylesheet1.css"/>
</head>
<body>
    <a onclick="setStyleSheet('stylesheet1.css')" href="#">Style 1</a>
    <a onclick="setStyleSheet('stylesheet2.css')" href="#">Style 2</a>
</body>
davidcondrey
  • 34,416
  • 17
  • 114
  • 136
12

This question is pretty old but I would suggest an approach which is not mentioned here, in which you will include both the CSS files in the HTML, but the CSS will be like

light.css

/*** light.css ***/

p.main{
   color: #222;
}

/*** other light CSS ***/

and dark.css will be like

/*** dark.css ***/
 
.dark_theme p.main{
   color: #fff;
   background-color: #222;
}

/*** other dark CSS ***/

basicall every selector in dark.css will be a child of .dark_theme

Then all you need to do is to change the class of body element if someone selects to change the theme of the website.

$("#changetheme").click(function(){
   $("body").toggleClass("dark_theme");
});

And now all your elements will have the dark css once the user clicks on #changetheme. This is very easy to do if you are using any kind of CSS preprocessors.

You can also add CSS animations for backgrounds and colors which makes the transition highly smooth.

Lampe2020
  • 115
  • 1
  • 12
void
  • 36,090
  • 8
  • 62
  • 107
8

Using jquery you can definitely swap the css file. Do this on button click.

var cssLink = $('link[href*="light.css"]');
cssLink.replaceWith('<link href="dark.css" type="text/css" rel="stylesheet">');

Or as sam's answer, that works too. Here is the jquery syntax.

$('link[href*="light.css"]').prop('disabled', true);
$('link[href*="dark.css"]').prop('disabled', false);
Venkata Krishna
  • 14,926
  • 5
  • 42
  • 56
  • 1
    it could be better, but i like how you've cleanly defined sam's jQ portion & helped me understand everything clearly. – RozzA Nov 01 '17 at 21:34
4

Using jquery .attr() you can set href of your link tag .i.e

Sample code

$("#yourButtonId").on('click',function(){
   $("link").attr(href,yourCssUrl);
});
Satinder singh
  • 10,100
  • 16
  • 60
  • 102
0

Maybe I'm thinking too complicated, but since the accepted answer was not working for me I thought I'd share my solution as well.

Story:
What I wanted to do was to include different 'skins' of my page in the head as additional stylesheets that where added to the 'main' style and switch them by pressing a button on the page (no browser settings or stuff).

Problem:
I thought @sam's solution was very elegant but it did not work at all for me. At least part of the problem is that I'm using one main CSS file and just add others on top as 'skins' and thus I had to group the files with the missing 'title' property.

Here is what I came up with.
First add all 'skins' to the head using 'alternate':

<link rel="stylesheet" href="css/main.css" title='main'>
<link rel="stylesheet alternate" href="css/skin1.css" class='style-skin' title=''>
<link rel="stylesheet alternate" href="css/skin2.css" class='style-skin' title=''>
<link rel="stylesheet alternate" href="css/skin3.css" class='style-skin' title=''>

Note that I gave the main CSS file the title='main' and all others have a class='style-skin' and no title.

To switch the skins I'm using jQuery. I leave it up to the purists to find an elegant VanillaJS version:

var activeSkin = 0;    
$('#myButton').on('click', function(){
    var skins = $('.style-skin');
    if (activeSkin > skins.length) activeSkin=0;
    skins.each(function(index){
        if (index === activeSkin){
            $(this).prop('title', 'main');
            $(this).prop('disabled', false);
        }else{
            $(this).prop('title', '');
            $(this).prop('disabled', true);
        }
    });
    activeSkin++
});

What it does is it iterates over all available skins, takes the (soon) active one, sets the title to 'main' and activates it. All other skins are disabled and title is removed.

Flow
  • 29
  • 4
0

Simply update you Link href attribute to your new css file.

function setStyleSheet(fileName){
       document.getElementById("WhatEverYouAssignIdToStyleSheet").setAttribute('href', fileName);
    }
Hanzla Habib
  • 3,457
  • 25
  • 25
0

I reworked lampe's example, and you can add a class using a selector in this way: first apply the class to specific selectors in a javascript (repeat as many times you need for specific element selectors (in my HTML):

$("p:nth-of-type(even)").toggleClass("main mainswitch");

Then the html looks like this

<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"> 
</script>
<script>
$(document).ready(function(){
    $("button").click(function(){
         $(".mainswitch").toggleClass("main");
  });
});
</script>
<style>
.main {
    font-size: 120%;
    color: red;
  }
 </style>
 </head>
 <body>
<div>
 <p>This is a paragraph.</p>
 <p class="main mainswitch">This is another paragraph.</p>
 <p>This is a paragraph.</p>
 <p>This is a paragraph.</p>
 <p>This is a paragraph.</p>
 </div>
 <button>Toggle class "main" for p elements</button>

 </body>
</html>
0

If you're using Angular (cause it's not 2013 anymore), you can try the answer/solution/suggestion from here:

How can I change the targeted CSS file on a click event

It did the trick for me.

librogil
  • 91
  • 1
  • 7
0

I do understand this is not the best solution but you can also do something like this:

<head>
<link href="//awesome-style-sunny.css" rel="stylesheet" id="default-style"></link>
<link href="//awesome-style-sunny.css" rel="stylesheet" id="themed-style"></link>
</head>

as you can see the same style is being loaded twice, this helps when you are switching the 2nd style href, your users would not see a naked website without any style.

The order of style does matter as the later one would override the first one. Then we can do something like this as it was mentioned in most of the answers:

const changeTheme = (theme) => {
  if (document.getElementById("themed-style")) {
    document.getElementById("themed-style").href = theme
    localStorage.setItem("user-theme", theme);
  }
}
Amirmasoud
  • 590
  • 4
  • 11
  • 27