Module talk:Color contrast
Template:WikiProject banner shell Template:Copied
Making the lum function accessible from modules
I've made the lum function accessible from modules by changing the code from:
function p.lum(frame) return color2lum(frame.args[1] or frame:getParent().args[1]) end
to:
function p.lum(frame) local args = frame.args[1] or frame:getParent().args[1] return p._lum(args) end function p._lum(args) return color2lum(args) end
The changes are on the sandbox and will implement them if there are no comments. --Gonnym (talk) 11:03, 6 January 2019 (UTC)
- Template:Re That looks like a very sensible and useful improvement. My only reservation is that passing the parameters as a table gives the person wanting to import the routine no idea of what values/types to supply to the routine without having to read through all of the code to determine them. In general I'd usually recommend either documenting a brief list of required values for the args table or passing the parameters as a list of obviously-named variables rather than a table. But that's just a minor point and shouldn't stop you from updating the main module. --RexxS (talk) 13:03, 6 January 2019 (UTC)
- Template:Ping I don't mind doing either if that helps. I just followed the current style used for the other three public functions. Do you want me to change _lum(args) to _lum(color)? --Gonnym (talk) 13:27, 6 January 2019 (UTC)
- Template:Re Sorry, my confusion: now I've looked harder at it, you're actually passing a string representing the colour, not a table (which is what args would most commonly indicate). I would suggest:<syntaxhighlight lang=Lua>
- Template:Ping I don't mind doing either if that helps. I just followed the current style used for the other three public functions. Do you want me to change _lum(args) to _lum(color)? --Gonnym (talk) 13:27, 6 January 2019 (UTC)
-- use Script error: The function "somecolor" does not exist. directly or Script error: You must specify a function to call. from a wrapper template: function p.lum(frame) local color = frame.args[1] or frame:getParent().args[1] return p._lum(color) end
-- This exports the function for use in other modules. -- The colour is passed as a string: function p._lum(color) return color2lum(color) end</syntaxhighlight> I know we can put fuller information in the documentation, but I always suggest leaving small annotations in the code to help any re-users.
Possible typos in code?
Observing the code, I might have spotted a couple of typos:
In p._greatercontrast function, lines 160 and 161
I think <syntaxhighlight lang='lua'>if mw.ustring.match(v3, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') or
mw.ustring.match(v3, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') then</syntaxhighlight> should read <syntaxhighlight lang='lua'>if mw.ustring.match(c3, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') or
mw.ustring.match(c3, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') then</syntaxhighlight> instead. Here, v3 is expected to be a number, thus there is no point in applying it a regular expression. Besides, given the four lines above, it seems logical to use c3 variable here instead of v3.
In p._styleratio function, line 209
I think <syntaxhighlight lang="lua">if( lum ~= ) then bg, lum_fg = v, lum end</syntaxhighlight> should read <syntaxhighlight lang="lua">if( lum ~= ) then fg, lum_fg = v, lum end</syntaxhighlight> instead, since at this point we have isolated a forecolor specification from input CSS.
My apologies if I would have misinterpreted your code.
Kindly, Olivier Ph. (talk) 12:11, 13 October 2020 (UTC)
Categorisation broken?
Through Documentation subpage, Category:Modules handling colors is added as expected. However, the page does not appear in Category:Modules handling colors (1). -DePiep (talk) 15:12, 7 January 2021 (UTC)
- Template:Re categorisation behaves as something of a black art. The page appears in Category:Modules handling colors now, but obviously didn't earlier today. Wikipedia:FAQ/Categorization #Why might a category list not be up to date? discusses some of the issues that may cause the lag. I usually try a null edit to "touch" the pages concerned and often find the page then appears in the category, but it's not always 100% successful. --RexxS (talk) 16:07, 7 January 2021 (UTC)
- I had just edited out the {{Sandbox other}} [1]. So we have a cause (bug). Did not investigate problem further.-DePiep (talk) 16:11, 7 January 2021 (UTC)
- Yes I saw that. But because the category was being added via {{Sandbox other}} and now is added directly via Module:Color contrast/doc, that becomes effectively a different transclusion and the usual caveats about delays because of cached transclusions apply. --RexxS (talk) 16:56, 7 January 2021 (UTC)
- I had just edited out the {{Sandbox other}} [1]. So we have a cause (bug). Did not investigate problem further.-DePiep (talk) 16:11, 7 January 2021 (UTC)
Template-protected edit request on 18 March 2025
Template:Edit template-protected
Line 135:
Template:Td
Line 137:
Template:Td
Change the specification of the default colors from hex triplets <syntaxhighlight lang="lua" inline>'#FFFFFF'</syntaxhighlight> and <syntaxhighlight lang="lua" inline>'#000000'</syntaxhighlight> to named HTML colors <syntaxhighlight lang="lua" inline>'white'</syntaxhighlight> and <syntaxhighlight lang="lua" inline>'black'</syntaxhighlight> respectively. This allows the default case to take advantage of the pre-computed table of relative luminances of named HTML colors at Module:Color contrast/colors checked by the color2lum() function instead of calculating the relative luminances of white and black every time (as the module as currently written does), as well as, at least for the default colors, avoiding a parser bug that causes output that begins with # to result in a numbered list. Kinsio (talk ★ contribs ★ rights) 18:55, 19 March 2025 (UTC)
Done * Pppery * it has begun... 20:30, 30 March 2025 (UTC)
Template-protected edit request on 20 July 2026
Template:Edit template-protected Please update the module to support additional CSS3/CSS4 color formats ("rgba", "hsla", 4-digit and 8-digit hex values) and incorporate several code fixes, bringing it up to speed with the improvements from "ptwiki".
Key changes
- Updated regex patterns in "color2lum" to support "rgba()", "hsla()", space-separated values, and "#RGBA" (4-digit) / "#RRGGBBAA" (8-digit) hex formats.
- Fixed a variable check bug in "_greatercontrast" where "v3" was tested instead of "c3".
- Stripped newlines and normalized whitespace in the CSS output for "_greatercontrast" when the "css" flag is active.
- Cleaned up variable scoping in "_styleratio" to prevent shadowing loop index "k" and removed unused variables ("bg", "fg").
Replace the current code of the module with:
Template:Collapse-top <syntaxhighlight lang="lua"> -- -- This module implements -- Template:Color contrast ratio -- -- Template:ColorToLum -- Template:RGBColorToLum -- local p = {} local HTMLcolor = mw.loadData( 'Module:Color contrast/colors' )
local function sRGB (v)
if (v <= 0.03928) then
v = v / 12.92
else
v = math.pow((v+0.055)/1.055, 2.4)
end
return v
end
local function rgbdec2lum(R, G, B)
if ( 0 <= R and R < 256 and 0 <= G and G < 256 and 0 <= B and B < 256 ) then
return 0.2126 * sRGB(R/255) + 0.7152 * sRGB(G/255) + 0.0722 * sRGB(B/255)
else
return
end
end
local function hsl2lum(h, s, l)
if ( 0 <= h and h < 360 and 0 <= s and s <= 1 and 0 <= l and l <= 1 ) then
local c = (1 - math.abs(2*l - 1))*s
local x = c*(1 - math.abs( math.fmod(h/60, 2) - 1) )
local m = l - c/2
local r, g, b = m, m, m
if( 0 <= h and h < 60 ) then
r = r + c
g = g + x
elseif( 60 <= h and h < 120 ) then
r = r + x
g = g + c
elseif( 120 <= h and h < 180 ) then
g = g + c
b = b + x
elseif( 180 <= h and h < 240 ) then
g = g + x
b = b + c
elseif( 240 <= h and h < 300 ) then
r = r + x
b = b + c
elseif( 300 <= h and h < 360 ) then
r = r + c
b = b + x
end
return rgbdec2lum(255*r, 255*g, 255*b)
else
return
end
end
local function color2lum(c)
if (c == nil) then
return
end
-- html '#' entity
c = c:gsub("#", "#")
-- whitespace c = c:match( '^%s*(.-)[%s;]*$' )
-- unstrip nowiki strip markers c = mw.text.unstripNoWiki(c)
-- lowercase c = c:lower()
-- first try to look it up
local L = HTMLcolor[c]
if (L ~= nil) then
return L
end
-- convert from hsl / hsla (accepts comma or space delimiters)
if mw.ustring.match(c,'^hsla?%([%s]*[0-9][0-9%.]*[%s]*[,%s][%s]*[0-9][0-9%.]*%%[%s]*[,%s][%s]*[0-9][0-9%.]*%%.*%)$') then
local h, s, l = mw.ustring.match(c,'^hsla?%([%s]*([0-9][0-9%.]*)[%s]*[,%s][%s]*([0-9][0-9%.]*)%%[%s]*[,%s][%s]*([0-9][0-9%.]*)%%.*%)$')
return hsl2lum(tonumber(h), tonumber(s)/100, tonumber(l)/100)
end
-- convert from rgb / rgba (numeric values 0 to 255)
if mw.ustring.match(c,'^rgba?%([%s]*[0-9][0-9]*[%s]*[,%s][%s]*[0-9][0-9]*[%s]*[,%s][%s]*[0-9][0-9]*.*%)$') then
local R, G, B = mw.ustring.match(c,'^rgba?%([%s]*([0-9][0-9]*)[%s]*[,%s][%s]*([0-9][0-9]*)[%s]*[,%s][%s]*([0-9][0-9]*).*%)$')
return rgbdec2lum(tonumber(R), tonumber(G), tonumber(B))
end
-- convert from rgb / rgba percent
if mw.ustring.match(c,'^rgba?%([%s]*[0-9][0-9%.]*%%[%s]*[,%s][%s]*[0-9][0-9%.]*%%[%s]*[,%s][%s]*[0-9][0-9%.]*%%.*%)$') then
local R, G, B = mw.ustring.match(c,'^rgba?%([%s]*([0-9][0-9%.]*)%%[%s]*[,%s][%s]*([0-9][0-9%.]*)%%[%s]*[,%s][%s]*([0-9][0-9%.]*)%%.*%)$')
return rgbdec2lum(255*tonumber(R)/100, 255*tonumber(G)/100, 255*tonumber(B)/100)
end
-- remove leading # (if there is one) and whitespace c = mw.ustring.match(c, '^[%s#]*([a-f0-9]*)[%s]*$')
-- split into rgb
local cs = mw.text.split(c or , )
if( #cs == 6 or #cs == 8 ) then -- Accepts 6 digits (#RRGGBB) or 8 digits (#RRGGBBAA)
local R = 16*tonumber('0x' .. cs[1]) + tonumber('0x' .. cs[2])
local G = 16*tonumber('0x' .. cs[3]) + tonumber('0x' .. cs[4])
local B = 16*tonumber('0x' .. cs[5]) + tonumber('0x' .. cs[6])
return rgbdec2lum(R, G, B)
elseif ( #cs == 3 or #cs == 4 ) then -- Accepts 3 digits (#RGB) or 4 digits (#RGBA)
local R = 16*tonumber('0x' .. cs[1]) + tonumber('0x' .. cs[1])
local G = 16*tonumber('0x' .. cs[2]) + tonumber('0x' .. cs[2])
local B = 16*tonumber('0x' .. cs[3]) + tonumber('0x' .. cs[3])
return rgbdec2lum(R, G, B) end
-- failure, return blank return
end
-- This exports the function for use in other modules. -- The colour is passed as a string. function p._lum(color)
return color2lum(color)
end
function p._greatercontrast(args)
local bias = tonumber(args['bias'] or '0') or 0
local css = (args['css'] and args['css'] ~= ) and true or false
local v1 = color2lum(args[1] or )
local c2 = args[2] or 'white'
local v2 = color2lum(c2)
local c3 = args[3] or 'black'
local v3 = color2lum(c3)
local ratio1 = -1;
local ratio2 = -1;
if (type(v1) == 'number' and type(v2) == 'number') then
ratio1 = (v2 + 0.05)/(v1 + 0.05)
ratio1 = (ratio1 < 1) and 1/ratio1 or ratio1
end
if (type(v1) == 'number' and type(v3) == 'number') then
ratio2 = (v3 + 0.05)/(v1 + 0.05)
ratio2 = (ratio2 < 1) and 1/ratio2 or ratio2
end
if css then
local c1 = args[1] or
if mw.ustring.match(c1, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') or
mw.ustring.match(c1, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') then
c1 = '#' .. c1
end
if mw.ustring.match(c2, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') or
mw.ustring.match(c2, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') then
c2 = '#' .. c2
end
if mw.ustring.match(c3, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') or
mw.ustring.match(c3, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') then
c3 = '#' .. c3
end
local css_output = 'background-color:' .. c1 .. '; color:' .. ((ratio1 > 0) and (ratio2 > 0) and ((ratio1 + bias > ratio2) and c2 or c3) or ) .. ';'
-- Remove newlines (\n, \r) and extra spaces before returning CSS
return css_output:gsub('[\r\n]', ):gsub('%s+', ' ')
end
return (ratio1 > 0) and (ratio2 > 0) and ((ratio1 + bias > ratio2) and c2 or c3) or
end
function p._ratio(args)
local v1 = color2lum(args[1])
local v2 = color2lum(args[2])
if (type(v1) == 'number' and type(v2) == 'number') then
-- v1 should be the brighter of the two.
if v2 > v1 then
v1, v2 = v2, v1
end
return (v1 + 0.05)/(v2 + 0.05)
else
return args['error'] or '?'
end
end
function p._styleratio(args)
local style = (args[1] or ):lower() local lum_bg, lum_fg = 1, 0
if args[2] then
local lum = color2lum(args[2])
if lum ~= then lum_bg = lum end
end
if args[3] then
local lum = color2lum(args[3])
if lum ~= then lum_fg = lum end
end
local slist = mw.text.split(mw.ustring.gsub(mw.ustring.gsub(style or , '&#[Xx]23;', '#'), '#', '#'), ';')
for k = 1,#slist do
local s = slist[k]
local prop, val = s:match( '^[%s]*([^:]-):([^:]-)[%s;]*$' )
prop = prop or
val = val or
if (prop:match('^[%s]*(background)[%s]*$') or prop:match('^[%s]*(background%-color)[%s]*$')) then
local lum = color2lum(val)
if( lum ~= ) then lum_bg = lum end
elseif (prop:match('^[%s]*(color)[%s]*$')) then
local lum = color2lum(val)
if( lum ~= ) then lum_fg = lum end
end
end
if lum_bg > lum_fg then
return (lum_bg + 0.05)/(lum_fg + 0.05)
else
return (lum_fg + 0.05)/(lum_bg + 0.05)
end
end
--[[ Use Script error: The function "somecolor" does not exist. directly or Script error: You must specify a function to call. from a wrapper template.
Parameters:
-- |1= — required; A color to check.
--]] function p.lum(frame)
local color = frame.args[1] or frame:getParent().args[1] return p._lum(color)
end
function p.ratio(frame)
local args = frame.args[1] and frame.args or frame:getParent().args return p._ratio(args)
end
function p.styleratio(frame)
local args = frame.args[1] and frame.args or frame:getParent().args return p._styleratio(args)
end
function p.greatercontrast(frame)
local args = frame.args[1] and frame.args or frame:getParent().args return p._greatercontrast(args)
end
return p </syntaxhighlight> Template:Collapse-bottom --GKNishimoto (talk) 05:16, 20 July 2026 (UTC)
- I've gone ahead and done some cleanup on the code and everything generally looks good, but I do have a question. Since this adds support for specifying colours with an alpha, is it correct behaviour to just ignore that alpha property entirely? It feels like that should matter for the purpose of calculating the ratio, though I'm not familiar with the exact scope of this module. Aidan9382 (talk) 21:33, 26 July 2026 (UTC)
- Hi @Aidan9382,
- Thanks for reviewing and refining the code in the sandbox! The regex fix in '_greatercontrast' to support 4-digit and 8-digit hex values when using the 'css' flag is a great spot.
- To answer your question directly: Yes, ignoring the alpha component is the intended and correct behavior for this module.
- Under the WCAG 2.1 accessibility guidelines, computing the exact contrast ratio of a semi-transparent color requires blending it against whatever background is rendered behind it. Since this Lua module evaluates color values statically without access to the surrounding page layout or DOM context, it cannot reliably determine the underlying background layer.
- Stripping or ignoring the alpha channel allows the module to gracefully fall back to evaluating the base RGB/HSL contrast without throwing errors or returning blank values when editors pass modern CSS formats (such as '#RRGGBBAA' or 'rgba()').
- The code currently in the sandbox looks great and is ready to be applied. Thanks again for the help! -- GKNishimoto (talk) 21:42, 26 July 2026 (UTC)
- P.S. Thanks again! Once this goes live here, I'll be porting this updated version directly to ptwiki as well to keep both modules completely aligned.-- GKNishimoto (talk) 21:51, 26 July 2026 (UTC)
- In that case this seems fine,
Done. Aidan9382 (talk) 12:42, 27 July 2026 (UTC)
- This change broke the layout at Template:Basketballbox/testcases for transclusions using <syntaxhighlight lang="wikitext" inline style="display: none;" />
|bg=. Please test the sandbox module with that template. – Jonesey95 (talk) 16:11, 27 July 2026 (UTC)- More specifically, it caused output in the form of <syntaxhighlight lang="text" class="" style="" inline="1">background-color:#eeeeee; color:#202122;1</syntaxhighlight>, breaking following css rules. LittlePuppers (talk) 16:16, 27 July 2026 (UTC)
- Well, it seems I didn't aniticipate that scribunto would just implicitly concatenate multiple returns instead of taking just the first return, which gsub will cause. Thanks for that. I've gone ahead and fixed that in the sandbox. Aidan9382 (talk) 16:23, 27 July 2026 (UTC)
- Updated module sandbox to fix the CSS output formatting issue. Removed the
:gsub()whitespace/newline compression that was causing multiple return values and appending an unintended '1' to the style string, fully resolving the layout breaks on templates using|bg=(such as Basketballbox) while retaining all new CSS3/CSS4 color format features. - While Aidan9382's quick fix in the sandbox solved the immediate multiple-return bug by keeping a single
:gsub('[\r\n]', ''), completely removing the extra string manipulation—as we did in our version—is cleaner and fully avoids any potential edge cases with complex template markup or string concatenation in Scribunto. It keeps the CSS output pure, exact, and 100% robust. --GKNishimoto (talk) 17:46, 27 July 2026 (UTC)
- Updated module sandbox to fix the CSS output formatting issue. Removed the
- Well, it seems I didn't aniticipate that scribunto would just implicitly concatenate multiple returns instead of taking just the first return, which gsub will cause. Thanks for that. I've gone ahead and fixed that in the sandbox. Aidan9382 (talk) 16:23, 27 July 2026 (UTC)
- More specifically, it caused output in the form of <syntaxhighlight lang="text" class="" style="" inline="1">background-color:#eeeeee; color:#202122;1</syntaxhighlight>, breaking following css rules. LittlePuppers (talk) 16:16, 27 July 2026 (UTC)
- This change broke the layout at Template:Basketballbox/testcases for transclusions using <syntaxhighlight lang="wikitext" inline style="display: none;" />
- In that case this seems fine,