Quick Start

The Sun Protection Factor is an objective calculation required for sun screens. The app lets you see what is going on.


This is an up-to-date version of something I created in old code.


Width 1
Width 1
I1 : I2
//One universal basic required here to get things going once loaded
window.onload = function () {
    //restoreDefaultValues(); //Un-comment this if you want to start with defaults

//Main() is hard wired as THE place to start calculating when inputs change
//It does no calculations itself, it merely sets them up, sends off variables, gets results and, if necessary, plots them.
function Main() {
    //Save settings every time you calculate, so they're always ready on a reload

    //Send all the inputs as a structured object
    //If you need to convert to, say, SI units, do it here!

    const inputs = {
        LMax1: sliders.SlideL1.value,
        Abs1: sliders.SlideA1.value,
        Width1: sliders.SlideW1.value,
        LMax2: sliders.SlideL2.value,
        Abs2: sliders.SlideA2.value,
        Width2: sliders.SlideW2.value,
        TMode: document.getElementById('TMode').checked,

    //Send inputs off to CalcIt where the names are instantly available
    //Get all the resonses as an object, result
    const result = CalcIt(inputs);
    document.getElementById('I1I2').value = result.I1I2;
    document.getElementById('SPFCW').value = result.SPFCW;
    document.getElementById('UVABSUI').value = result.UVABSUI;
    if (result.plots) {
        for (let i = 0; i < result.plots.length; i++) {
            plotIt(result.plots[i], result.canvas[i]);

    //You might have some other stuff to do here, but for most apps that's it for Main!

//Here's the app calculation
//The inputs are just the names provided - their order in the curly brackets is unimportant!
//By convention the input values are provided with the correct units within Main
function CalcIt({ LMax1, LMax2, Abs1, Abs2, Width1, Width2, TMode }) {
    let GPts = [], v, EVals = [], EPts = [], UVVals = [], UVPts = [], EUPts = [], TPts = [], C1Tot = 0, C2Tot = 0, Abs400 = 0
    for (i = 290; i <= 400; i++) {
        if (i <= 298) EVals[i] = 1
        if (i > 298 && i <= 327) EVals[i] = Math.pow(10, -(i - 298) * 0.094)
        if (i > 327) EVals[i] = 0.001566758 * Math.pow(10, -(i - 327) * 0.015)
        EPts.push({ x: i, y: EVals[i] })
        if (i < 316.5) { UVVals[i] = Math.pow(10, -(316.5 - i) * 0.2) } else { UVVals[i] = 1 }
        UVPts.push({ x: i, y: UVVals[i] })
    let UVA = 0, UVB = 0, Ext = [], SigE = 0, SigEbar = 0, Ebar = 0
    const TPc = TMode ? 0.25 : 1
    for (i = 290; i <= 400; i++) {
        v = Abs1 * Math.exp(-Math.pow((LMax1 - i) / Width1, 2)) + Abs2 * Math.exp(-Math.pow((LMax2 - i) / Width2, 2))
        if (i <= 320) { UVB += v } else { UVA += v }
        if (i <= 380) { Ext[i] = v; SigE += v }
        Abs400 += v
        if (TMode) { GPts.push({ x: i, y: Math.pow(10, -v) }) } else { GPts.push({ x: i, y: v }) }
        EUPts.push({ x: i, y: 10 * EVals[i] * UVVals[i] })
        TPts.push({ x: i, y: TPc * 5000 * EVals[i] * UVVals[i] * Math.pow(10, -v) })
        C1Tot += EVals[i] * UVVals[i]
        C2Tot += EVals[i] * UVVals[i] * Math.pow(10, -v)
    let Absc = 0, CW = 400
    Ebar = SigE / (380 - 290)

    for (i = 290; i <= 400; i++) {
        v = Abs1 * Math.exp(-Math.pow((LMax1 - i) / Width1, 2)) + Abs2 * Math.exp(-Math.pow((LMax2 - i) / Width2, 2))
        if (i <= 380) { SigEbar += Math.abs(Ext[i] - Ebar) }
        Absc += v
        if (Absc >= 0.9 * Abs400) { CW = i; break }
    const SPF = C1Tot / C2Tot
    const I1 = (C1Tot * 100).toFixed(1)
    const I2 = (C2Tot * 100).toFixed(3)
    const SPFv = SPF.toPrecision(3)
    const CWv = CW.toFixed(0)
    const UVAB = (UVA / UVB).toFixed(1)
    const SUI = (SigE / SigEbar).toFixed(1)
    YMax = 4; YLabel = "Absorbance"
    if (TMode) { YMax = 1; YLabel = "Transmission" }
    let CWPts = []
    CWPts.push({ x: CW, y: 0 })
    CWPts.push({ x: CW, y: YMax })

    //Now set up all the graphing data detail by detail.
    let plotData = [GPts, TPts, CWPts, EPts, UVPts, EUPts], lineLabels = ["A(λ)", "Teff(λ)", "CW", "E(λ)", "UV(λ)", "ExUV(λ)"]

    const prmap = {
        plotData: plotData, //An array of 1 or more datasets
        lineLabels: lineLabels, //An array of labels for each dataset
        hideLegend: false, //Set to true if you don't want to see any labels/legnds.
        dottedLine: [false, false, true, false, false, false],
        xLabel: 'λ&nm', //Label for the x axis, with an & to separate the units
        yLabel: YLabel, //Label for the y axis, with an & to separate the units
        y2Label: "Rel. Val.", //Label for the y2 axis, null if not needed
        yAxisL1R2: [1, 1, 1, 2, 2, 2], //Array to say which axis each dataset goes on. Blank=Left=1
        logX: false, //Is the x-axis in log form?
        xTicks: undefined, //We can define a tick function if we're being fancy
        logY: false, //Is the y-axis in log form?
        yTicks: undefined, //We can define a tick function if we're being fancy
        legendPosition: 'top', //Where we want the legend - top, bottom, left, right
        xMinMax: [290, 400], //Set min and max, e.g. [-10,100], leave one or both blank for auto
        yMinMax: [0, YMax], //Set min and max, e.g. [-10,100], leave one or both blank for auto
        y2MinMax: [,], //Set min and max, e.g. [-10,100], leave one or both blank for auto
        xSigFigs: 'F0', //These are the sig figs for the Tooltip readout. A wide choice!
        ySigFigs: 'F0', //F for Fixed, P for Precision, E for exponential

    //Now we return everything - text boxes, plot and the name of the canvas, which is 'canvas' for a single plot
    return {
        I1I2: I1 + " : " + I2,
        SPFCW: SPFv + " : " + CWv,
        UVABSUI: UVAB + " : " + SUI,
        plots: [prmap],
        canvas: ['canvas'],


The Sun Protection Factor (SPF) factor in cosmetic formulations is, unlike many cosmetics claims, rather objective and claims are closely controlled for both in vitro and in vivo values. But what does it mean?

The skin is bombarded with UV light which is quite strong down to 320nm then falls off rapidly with decreasing wavelength. Graphs of UV irradiance spectra UV(λ) usually show a gentle tailing off below 320nm but this is because they are log-plots. In linear plots (as used in this app) the fall off is rapid. The skin is also known to go red (show erythema) with doses of UV. The erythema action potential E(λ) falls off rapidly above 298nm. Graphs typically show it extending right up to 400nm, but again this is because they use log-plots. Because evolution is not stupid, it's no coincidence that the skin is rather immune to UV, in other words, its E(λ) values are very small in the region where UV(λ) is high. So sensible humans need not worry about filtering out harmful UV because the skin is rather robust against normal doses. But those who choose to display light skin for hours to lots of bright sunshine find that even the low E(λ) values are sufficient to cause skin damage - so they need to add creams to cut out the harmful UV.

How are these creams measured for efficacy? The SPF is defined by the equation:


The integral is made over 290-400nm. All this is saying is "See how much total damage is caused by unfiltered UV light (the top intergral) divided by the damage caused by filtered UV light (the bottom integral)". T(λ) is the Transmission of the filter at a given wavelength. We are more used to looking at A(λ) the Absorbance which is -log(T), but by selecting the T option you can view in Transmission.

To show how SPF works, the graph shows E(λ) and UV(λ) along with A(λ) based on your inputs. Your UV absorber is assumed to be made up of two components with different maximum absorbance (AMax) at different maximum wavelengths (λMax) and with different absorbtion widths W. The worst place for damage is shown in the graph of E(λ)xUV(λ) which is magnified 10x to make it visible. The effective UV transmission, Teff(λ) is therefore E(λ)xUV(λ)xT(λ), magnified 10000x. Moving the mouse over any of the lines shows the real values. The app calculates I1 and I2 (the top and bottom integrals) and produces SPF=I1/I2.

Because not all SPFs are equal other calculated values allow different formulations to be distinguished. The Critical Wavelength (CW) index, λc, describes the range of protection. It is obtained when the integral of the absorbance spectrum reaches 90% of the total absorbance from 290–400 nm and is shown by the vertical line:

` ∫A(λ)(290-λc)=0.9∫A(λ)(290-400)`

The UVA/UVB index is the ratio of the average absorption in the UVA region (320-400) to that in the UVB region (290-320):


The factors of 80 and 30 are necessary to average the absorption over different wavelength ranges.

The Spectral Uniformity Index (SUI) gives an idea of how evenly distributed the absorption is. Given the absorption A(λ) and its average A̅ the SUI is given by:


These factors sometimes become incorporated into star ratings. Because they are in vitro measurements they need to be interpreted with caution. And you need to know if they are obtained after zero UV exposure, which will flatter their values, or after considerable exposure which will reduce the values more or less depending on the nature of the absorber(s) (chemical type or nanoparticle type) and their formulation. Changes of values after exposure to (salt) water are also important.

Amazing though it might sound, humans have managed rather well without SPF creams for 100,000's of years. Indeed, one problem for humans has been getting enough sun exposure to provide the right amount of Vitamin D. Those who overprotect their children with sunblocks have only themselves to blame if their children develop rickets (as has happened in the UK). There is also some evidence that sunshine helps reduce risks of heart disease via generation of nitric oxide. Too much of anything (sun or sun protection) is, by definition, bad.

Technical notes

  • The typical in vitro assumption is that the sunscreen is applied at 2mg/cm² (20µm).
  • Increasing A by adding more blocker or increasing thickness produces limited benefits.
  • Doubling A from 1 to 2 decreases T from 90% to 99%, but going from 2 to 3 increases T to 99.9% - reducing the light falling on the skin only by 0.9% with a considerable increase in cost to supplier (adding more blocker) or the user (slapping more on).
  • Because increasing A adds little value above a certain point, manipulating λmax is much more useful in terms of achieving maximum SPF for minimum chemical.
  • Going for a large CW means adding lots of absorbance in the high wavelength regions. This tends to increase absorbance above 400nm, reducing the blue and therefore giving a yellow stain to clothes if the formulation is allowed to transfer.
  • A scattering formulation cannot be measured using a standard UV spectrometer because the scattered light misses the spectrometer slit and gives false broad and high A values. An integrating detector must be used. Of course back scattering does reduce the UV that reaches the skin so scattering formulations can perform well - though many users don't like going around covered in white.
  • When formulating with standard UV absorbing chemicals, the trick is to ensure that they stay on the surface and don't penetrate the SC and get lost. Penetration is reduced via a high MWt absorber (so polymeric absorbers can't penetrate) and via high MWt "oils" that are poorly compatible with the skin. Such oils (with assistance from silicones) help avoid loss of the chemical to the sea or swimming pool.
  • Particle absorbers have many advantages over chemicals; they tend not to diminish in effectiveness and they don't produce side-products from chemical reactions with the UV. The scattering versions have obvious cosmetic drawbacks.
  • The only way to avoid scattering is by having very small particles, hence nanoparticle formulations. "Nano" tends to induce hysteria. In Australia where melanomas are common, the scientific advice is that the risks of nano are very much smaller than the risks of melanomas. There is considerable evidence that nanoparticles cannot penetrate normal skin. If you have "abnormal" skin then not only might nanoparticles penetrate but also the classic UV-blocking chemicals (and also the dangerous UV!) can penetrate. The safest strategy in that case is to not expose abnormal skin to too much sun.
  • Although this app is concerned with in vitro measurements, claims of SPF have to be validated on real humans. Volunteers are exposed on their backs to varying amounts of UV light. From the amount of light required to create redness (erythema) the SPF can be calculated. Often this test is done after the volunteer has spent some time in the shower (e.g. with salt water) or a spa pool if the product is to justify a claim to being water-proof. Deliberately causing sun-burn on volunteers can only be done by experience organisations with a good safety infrastructure, so SPF testing is relatively expensive to carry out. Hence, a lot of in vitro work has to be done before going to human validation studies.
  • As a disclaimer I have to point out that the app is for illustrative purposes only. The details of the E(λ) and UV(λ)curves are important and the ones used here are simplified "typical" examples. The opinions expressed are simply my opinions as I make no claim to being an SPF expert. If you have better curves or more informed views I will be happy to update the app.