menu

Questions & Answers

Can JavaScript change the value of @page CSS?

The following CSS affects whether a page prints in portrait or landscape by default.

@page {
   size: landscape;
}

I realize that this only works on a very limited set of browsers, and that the user can override it. That's okay; I'm just trying to provide a good default.

This works fine as a static value in CSS, but I'd like to switch dynamically between portrait & landscape based on on user choices. Is it possible to use JavaScript to change this value?

Comments:
2023-01-19 00:20:04
@J-16SDiZ: I don't think so; I took a look through that question, and don't see anything about changing the setting dynamically.
2023-01-19 00:20:04
@BNL: Not much; I spent some time looking through the DOM for where I might access this property, but couldn't find anything. I thought a bit about changing the stylesheet itself, vaguely along the lines of jasssonpet's answer, but that seemed problematic to me at the time.
Answers(3) :

One simple way is to create a separate style for @page and change it:

var cssPagedMedia = (function () {
    var style = document.createElement('style');
    document.head.appendChild(style);
    return function (rule) {
        style.innerHTML = rule;
    };
}());

cssPagedMedia.size = function (size) {
    cssPagedMedia('@page {size: ' + size + '}');
};

cssPagedMedia.size('landscape');
Comments:
2023-01-19 00:20:04
I typically wait a few days to mark something as correct, in case anyone else comes along with more insight, but this is a good, sensible approach that just works; thank you.
2023-01-19 00:20:04
It works for me when I removed @page declaration from CSS

@jasssonpet saved my butt with his answer. That being said, I changed it to be a little simpler (in my opinion).

** Not quite sure why it was done the way it was, but please educate me if you know. If it's just preference, then I'm sharing my code sample because someone else's preferences might align better with mine.

// just create the style tag and set the page size
function setPageSize(cssPageSize) {
    const style = document.createElement('style');
    style.innerHTML = `@page {size: ${cssPageSize}}`;
    document.head.appendChild(style);
}

// how to use it
setPageSize('letter landscape');

EDIT: I logged in for the first time in a while and saw @jasssonpet's answer and it makes a lot more sense to me now. In case anyone else is confused, the main benefit of his approach is 2 things:

  1. He makes use of closures so that there is only 1 style tag added to the page that can then be referenced and changed.
  2. He extends upon cssPagedMedia so that any methods added are namespaced. This way, the global namespace isn't polluted.

Here is it in Class form (which I find easier to understand):

class SingletonStyle {
  constructor() {
    this.style = document.createElement("style");
    document.head.appendChild(this.style);
  }

  apply(rule) {
    this.style.innerHTML = rule;
  }

  size(size) {
    this.apply("@page {size: " + size + "}");
  }
}
Comments:
2023-01-19 00:20:05
jassonpet's technique creates one Style element in the page head and returns a function for updating its value. Your technique appends a new style element to the page head each time it is executed. Both ways should work (more recently appended CSS rules will always override earlier ones) but the other technique results in less rule pollution and makes it easier to totally replace an existing rule so you don't need to append contradictory rules.

Thanks @jasssonpet for his solution! I ran into a problem that Vue JS can't v-bind to css outside of root, like this

@media print {
  @page {
    size: A4 v-bind(printSize); // <-- doesn't work!
  }
}

I need to manage orientation & margins independently, maybe more rules at future. Therefore, I rewrote and updated that code for embedding in other code and frameworks.

at utils/customizePageStyle.js

let pageStyle;
const pageRules = {};

const pageHasRules = () => !!Object.keys(pageRules).length;

const addCustomCssStylesheet = async () => {
  pageStyle = document.createElement('style');
  document.head.appendChild(pageStyle);
};

export const cleanPageRules = () => pageStyle.innerHTML = `@page {}`;

const writePageRules = async () => {
  if (!pageStyle) await addCustomCssStylesheet();
  if (!pageHasRules) {
    cleanPageRules();
    return;
  }
  let styleTxt = '@media print { @page {';
  Object.keys(pageRules).forEach(ruleName => {
    const ruleTxt = pageRules[ruleName];
    styleTxt += `${ruleName}: ${ruleTxt} !important; `;
  });
  styleTxt += '} } ';
  pageStyle.innerHTML = styleTxt;
};

const addPageRule = ({ruleName, ruleValue}) => pageRules[ruleName] = ruleValue;

const asyncAddPageRule = async rule => {
  addPageRule(rule);
  await writePageRules();
};

const addPageRules = async rules => {
  rules.forEach(rule => addPageRule(rule));
  await writePageRules();
};

const delPageRule = ruleName => pageRules[ruleName] = null;

const asyncDelPageRule = async ruleName => {
  delPageRule(ruleName);
  await writePageRules();
};

const delPageRules = async ruleNames => {
  ruleNames.forEach(ruleName => delPageRule(ruleName));
  pageHasRules ? await writePageRules() : cleanPageRules();
};

export const setPageRules = {
  add: asyncAddPageRule,
  adds: addPageRules,
  del: asyncDelPageRule,
  dels: delPageRules,
  clean: cleanPageRules
};

Vue Js using example:

<script setup>
import { setPageRules } from '@/utils/customizePageStyle.js';
const printSize = computed(()=> yourCondition1 ? 'A4 portrait' : 'A4 landscape');
const printMargin = computed(()=> yourCondition2 ?
  '1cm 1cm 1cm 2cm' : '24pt 24pt auto 18pt');
onMounted(()=> {
  window.addEventListener('beforeprint', async e => {
    await setPageRules.adds([
      {ruleName: 'size', ruleValue: printSize.value},
      {ruleName: 'margin', ruleValue: printMargin.value},
    ]);
  });
  window.addEventListener('afterprint', e => setPageRules.clean());
}
</script>
<style>
@media print {
  body {
    margin: 0px; // don't work with it, I don't know why
  }
}