[{"data":1,"prerenderedAt":1531},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fuse-html-formatted-labels-pyqgis":3},{"id":4,"title":5,"body":6,"description":1520,"extension":1521,"meta":1522,"navigation":194,"path":1527,"seo":1528,"stem":1529,"__hash__":1530},"docs\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fuse-html-formatted-labels-pyqgis\u002Findex.md","Use HTML-Formatted Labels in PyQGIS",{"type":7,"value":8,"toc":1505},"minimark",[9,13,17,26,115,120,134,138,141,361,388,392,395,468,479,483,486,543,552,556,575,757,770,774,777,855,878,895,899,902,1290,1299,1303,1310,1354,1364,1368,1375,1379,1410,1414,1420,1424,1434,1440,1446,1455,1461,1467,1471,1501],[10,11,5],"h1",{"id":12},"use-html-formatted-labels-in-pyqgis",[14,15,16],"p",{},"A plain map label has one font, one size and one colour. Good cartography often needs more inside a single label: a station name in bold with its elevation small and grey underneath, a peak's name followed by its height, a parcel number with the owner in italics, a warning value in red. Without HTML formatting, these need two stacked labels that drift apart or collide. With it, one label carries several styles, placed and collision-checked as a single block.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002F","Labeling & Annotations",". It enables HTML formatting on a layer's labels, writes expressions that emit HTML, styles parts of labels conditionally, escapes attribute values safely, and explains which HTML features labels support.",[14,27,28],{},[29,30,35,39,43,51,60,67,74,80,86,94,100,106,110],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"121 0 639 236","img","A single map label combining a bold name, a large coloured temperature and a small grey elevation line","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"One label, several styles",[40,41,42],"desc",{},"A weather station label rendered with HTML formatting: the station name in bold dark text, a second line with the temperature in a larger blue font, and a third line with the elevation in small grey text. The whole block is placed and checked for collisions as one label, so its parts never separate.",[44,45],"rect",{"x":46,"y":47,"width":48,"height":49,"fill":50},"121","0","639","236","#f6f3ea",[52,53,59],"text",{"x":54,"y":55,"style":56,"fill":57,"textAnchor":58},"440.5","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","Three styles, one label block",[61,62],"circle",{"cx":63,"cy":64,"r":65,"fill":57,"stroke":57,"style":66},"160","150","7","stroke-width:1",[52,68,73],{"x":69,"y":70,"style":71,"fill":57,"textAnchor":72},"180","112","text-anchor:start;font-size:15.0px;font-family:sans-serif;font-weight:bold","start","Brocken",[52,75,79],{"x":69,"y":76,"style":77,"fill":78,"textAnchor":72},"140","text-anchor:start;font-size:17.0px;font-family:sans-serif;font-weight:bold","#2563eb","−3.4 °C",[52,81,85],{"x":69,"y":82,"style":83,"fill":84,"textAnchor":72},"164","text-anchor:start;font-size:10.5px;font-family:sans-serif","#59645f","1,141 m",[44,87],{"x":88,"y":89,"width":90,"height":64,"rx":91,"fill":92,"stroke":84,"style":93},"430","70","306","8","#fffdf7","stroke-width:2",[52,95,99],{"x":96,"y":97,"style":98,"fill":57,"textAnchor":58},"583","106.6","text-anchor:middle;font-size:11.0px;font-family:sans-serif;font-weight:bold","label expression",[52,101,105],{"x":96,"y":102,"style":103,"fill":104,"textAnchor":58},"134.6","text-anchor:middle;font-size:9.5px;font-family:monospace","#2f3b35","'\u003Cb>' || \"name\" || '\u003C\u002Fb>\u003Cbr>'",[52,107,109],{"x":96,"y":108,"style":103,"fill":104,"textAnchor":58},"162.6","'\u003Cspan style=\"color:#2563eb\">'…",[52,111,114],{"x":96,"y":112,"style":113,"fill":84,"textAnchor":58},"190.6","text-anchor:middle;font-size:10.0px;font-family:sans-serif","one block for placement",[116,117,119],"h2",{"id":118},"prerequisites","Prerequisites",[121,122,123,127],"ul",{},[124,125,126],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series. HTML formatting in labels arrived in QGIS 3.14; support for more CSS properties grew through the 3.x series.",[124,128,129,130,133],{},"A layer with labelling configured, as in the other ",[21,131,132],{"href":23},"labeling recipes",".",[116,135,137],{"id":136},"enable-html-formatting","Enable HTML formatting",[14,139,140],{},"HTML formatting is a property of the label's text format. With it enabled, the label text — usually from an expression — is interpreted as HTML.",[142,143,148],"pre",{"className":144,"code":145,"language":146,"meta":147,"style":147},"language-python shiki shiki-themes github-dark","from qgis.core import (QgsProject, QgsPalLayerSettings, QgsTextFormat,\n                       QgsVectorLayerSimpleLabeling)\nfrom qgis.PyQt.QtGui import QFont, QColor\n\nstations = QgsProject.instance().mapLayersByName(\"weather_stations\")[0]\n\nfmt = QgsTextFormat()\nfmt.setFont(QFont(\"Noto Sans\"))\nfmt.setSize(9)\nfmt.setColor(QColor(\"#2f3b35\"))\nfmt.setAllowHtmlFormatting(True)\n\nsettings = QgsPalLayerSettings()\nsettings.isExpression = True\nsettings.fieldName = \"\"\"'\u003Cb>' || \"name\" || '\u003C\u002Fb>\u003Cbr>' ||\n    '\u003Cspan style=\"font-size:11pt; color:#2563eb\">' || format_number(\"temp_c\", 1) || ' °C\u003C\u002Fspan>\u003Cbr>' ||\n    '\u003Cspan style=\"font-size:7pt; color:#59645f\">' || format_number(\"elev_m\", 0) || ' m\u003C\u002Fspan>'\"\"\"\nsettings.setFormat(fmt)\nstations.setLabeling(QgsVectorLayerSimpleLabeling(settings))\nstations.setLabelsEnabled(True)\nstations.triggerRepaint()\n","python","",[149,150,151,170,176,189,196,221,226,237,249,261,272,283,288,299,310,321,327,333,339,345,355],"code",{"__ignoreMap":147},[152,153,156,160,164,167],"span",{"class":154,"line":155},"line",1,[152,157,159],{"class":158},"snl16","from",[152,161,163],{"class":162},"s95oV"," qgis.core ",[152,165,166],{"class":158},"import",[152,168,169],{"class":162}," (QgsProject, QgsPalLayerSettings, QgsTextFormat,\n",[152,171,173],{"class":154,"line":172},2,[152,174,175],{"class":162},"                       QgsVectorLayerSimpleLabeling)\n",[152,177,179,181,184,186],{"class":154,"line":178},3,[152,180,159],{"class":158},[152,182,183],{"class":162}," qgis.PyQt.QtGui ",[152,185,166],{"class":158},[152,187,188],{"class":162}," QFont, QColor\n",[152,190,192],{"class":154,"line":191},4,[152,193,195],{"emptyLinePlaceholder":194},true,"\n",[152,197,199,202,205,208,212,215,218],{"class":154,"line":198},5,[152,200,201],{"class":162},"stations ",[152,203,204],{"class":158},"=",[152,206,207],{"class":162}," QgsProject.instance().mapLayersByName(",[152,209,211],{"class":210},"sU2Wk","\"weather_stations\"",[152,213,214],{"class":162},")[",[152,216,47],{"class":217},"sDLfK",[152,219,220],{"class":162},"]\n",[152,222,224],{"class":154,"line":223},6,[152,225,195],{"emptyLinePlaceholder":194},[152,227,229,232,234],{"class":154,"line":228},7,[152,230,231],{"class":162},"fmt ",[152,233,204],{"class":158},[152,235,236],{"class":162}," QgsTextFormat()\n",[152,238,240,243,246],{"class":154,"line":239},8,[152,241,242],{"class":162},"fmt.setFont(QFont(",[152,244,245],{"class":210},"\"Noto Sans\"",[152,247,248],{"class":162},"))\n",[152,250,252,255,258],{"class":154,"line":251},9,[152,253,254],{"class":162},"fmt.setSize(",[152,256,257],{"class":217},"9",[152,259,260],{"class":162},")\n",[152,262,264,267,270],{"class":154,"line":263},10,[152,265,266],{"class":162},"fmt.setColor(QColor(",[152,268,269],{"class":210},"\"#2f3b35\"",[152,271,248],{"class":162},[152,273,275,278,281],{"class":154,"line":274},11,[152,276,277],{"class":162},"fmt.setAllowHtmlFormatting(",[152,279,280],{"class":217},"True",[152,282,260],{"class":162},[152,284,286],{"class":154,"line":285},12,[152,287,195],{"emptyLinePlaceholder":194},[152,289,291,294,296],{"class":154,"line":290},13,[152,292,293],{"class":162},"settings ",[152,295,204],{"class":158},[152,297,298],{"class":162}," QgsPalLayerSettings()\n",[152,300,302,305,307],{"class":154,"line":301},14,[152,303,304],{"class":162},"settings.isExpression ",[152,306,204],{"class":158},[152,308,309],{"class":217}," True\n",[152,311,313,316,318],{"class":154,"line":312},15,[152,314,315],{"class":162},"settings.fieldName ",[152,317,204],{"class":158},[152,319,320],{"class":210}," \"\"\"'\u003Cb>' || \"name\" || '\u003C\u002Fb>\u003Cbr>' ||\n",[152,322,324],{"class":154,"line":323},16,[152,325,326],{"class":210},"    '\u003Cspan style=\"font-size:11pt; color:#2563eb\">' || format_number(\"temp_c\", 1) || ' °C\u003C\u002Fspan>\u003Cbr>' ||\n",[152,328,330],{"class":154,"line":329},17,[152,331,332],{"class":210},"    '\u003Cspan style=\"font-size:7pt; color:#59645f\">' || format_number(\"elev_m\", 0) || ' m\u003C\u002Fspan>'\"\"\"\n",[152,334,336],{"class":154,"line":335},18,[152,337,338],{"class":162},"settings.setFormat(fmt)\n",[152,340,342],{"class":154,"line":341},19,[152,343,344],{"class":162},"stations.setLabeling(QgsVectorLayerSimpleLabeling(settings))\n",[152,346,348,351,353],{"class":154,"line":347},20,[152,349,350],{"class":162},"stations.setLabelsEnabled(",[152,352,280],{"class":217},[152,354,260],{"class":162},[152,356,358],{"class":154,"line":357},21,[152,359,360],{"class":162},"stations.triggerRepaint()\n",[14,362,363,367,368,371,372,375,376,379,380,383,384,387],{},[364,365,366],"strong",{},"Breakdown:"," ",[149,369,370],{},"setAllowHtmlFormatting(True)"," switches the text renderer into HTML mode for this format. The label expression builds a string of HTML: ",[149,373,374],{},"\u003Cb>"," for bold, ",[149,377,378],{},"\u003Cbr>"," for line breaks, and ",[149,381,382],{},"\u003Cspan style=\"…\">"," with CSS for size and colour. The format's own font, size and colour act as defaults for any text not overridden by tags. ",[149,385,386],{},"format_number"," controls decimals and thousands separators; doing formatting in the expression keeps the label consistent with the data's precision.",[116,389,391],{"id":390},"know-what-html-labels-support","Know what HTML labels support",[14,393,394],{},"Labels render HTML with QGIS's own text engine, not a web browser, so only a subset of HTML and CSS works. Staying within it avoids labels that look fine in one release and break in another.",[14,396,397],{},[29,398,401,404,407,411,415,423,430,435,439,443,447,452,456,459,462,465],{"viewBox":399,"role":32,"ariaLabel":400,"xmlns":34},"0 0 760 250","HTML features supported in QGIS map labels compared with unsupported layout features, with buffers and backgrounds coming from the text format",[36,402,403],{},"Supported and unsupported HTML",[40,405,406],{},"Supported in map labels: bold, italic and underline tags, line breaks, span and font tags with colour, font size, font family, font weight, and on recent releases vertical alignment for superscript and subscript, and images. Not supported: tables, borders, padding, backgrounds per span, and external stylesheets. Buffers, shadows and backgrounds still come from the text format and apply to the whole label.",[44,408],{"x":47,"y":47,"width":409,"height":410,"fill":50},"760","250",[52,412,414],{"x":413,"y":55,"style":56,"fill":57,"textAnchor":58},"380","Inline styling yes, page layout no",[44,416],{"x":417,"y":418,"width":419,"height":420,"rx":91,"fill":421,"stroke":422,"style":93},"24","56","340","170","#e8efe6","#15803d",[52,424,429],{"x":425,"y":426,"style":427,"fill":428,"textAnchor":58},"194","88.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","#166534","works",[52,431,434],{"x":425,"y":432,"style":433,"fill":104,"textAnchor":58},"116.78","text-anchor:middle;font-size:10.5px;font-family:sans-serif","b · i · u · br",[52,436,438],{"x":425,"y":437,"style":433,"fill":104,"textAnchor":58},"144.78","span: color, font-size",[52,440,442],{"x":425,"y":441,"style":433,"fill":104,"textAnchor":58},"172.78","font-family, font-weight",[52,444,446],{"x":425,"y":445,"style":433,"fill":84,"textAnchor":58},"200.78","sup\u002Fsub on recent releases",[44,448],{"x":449,"y":418,"width":419,"height":420,"rx":91,"fill":450,"stroke":451,"style":93},"396","#fdf2e2","#b91c1c",[52,453,455],{"x":454,"y":426,"style":427,"fill":451,"textAnchor":58},"566","does not",[52,457,458],{"x":454,"y":432,"style":433,"fill":104,"textAnchor":58},"tables, borders",[52,460,461],{"x":454,"y":437,"style":433,"fill":104,"textAnchor":58},"padding, per-span background",[52,463,464],{"x":454,"y":441,"style":433,"fill":104,"textAnchor":58},"external CSS",[52,466,467],{"x":454,"y":445,"style":433,"fill":84,"textAnchor":58},"use the text format",[14,469,470,471,474,475,478],{},"Think of HTML labels as rich text, not web pages: inline styling of runs of text works; layout does not. Effects that apply to the whole label — buffer, shadow, background shape — still come from the text format, and are drawn around the entire block. Units in ",[149,472,473],{},"font-size"," should be points (",[149,476,477],{},"pt",") to match the rest of QGIS's text handling; pixel sizes behave differently between screen and print.",[116,480,482],{"id":481},"style-parts-conditionally","Style parts conditionally",[14,484,485],{},"Expressions can choose styles per feature, which turns labels into a small data visualisation: values outside a range in red, missing values in grey italics.",[142,487,489],{"className":144,"code":488,"language":146,"meta":147,"style":147},"settings.fieldName = \"\"\"\n'\u003Cb>' || \"name\" || '\u003C\u002Fb>\u003Cbr>' ||\nCASE\n  WHEN \"temp_c\" IS NULL THEN '\u003Ci>\u003Cspan style=\"color:#59645f\">no reading\u003C\u002Fspan>\u003C\u002Fi>'\n  WHEN \"temp_c\" \u003C 0 THEN '\u003Cspan style=\"color:#2563eb; font-size:11pt\">' || format_number(\"temp_c\", 1) || ' °C\u003C\u002Fspan>'\n  WHEN \"temp_c\" > 30 THEN '\u003Cspan style=\"color:#b91c1c; font-size:11pt\">\u003Cb>' || format_number(\"temp_c\", 1) || ' °C\u003C\u002Fb>\u003C\u002Fspan>'\n  ELSE '\u003Cspan style=\"font-size:11pt\">' || format_number(\"temp_c\", 1) || ' °C\u003C\u002Fspan>'\nEND\"\"\"\nstations.setLabeling(QgsVectorLayerSimpleLabeling(settings))\nstations.triggerRepaint()\n",[149,490,491,500,505,510,515,520,525,530,535,539],{"__ignoreMap":147},[152,492,493,495,497],{"class":154,"line":155},[152,494,315],{"class":162},[152,496,204],{"class":158},[152,498,499],{"class":210}," \"\"\"\n",[152,501,502],{"class":154,"line":172},[152,503,504],{"class":210},"'\u003Cb>' || \"name\" || '\u003C\u002Fb>\u003Cbr>' ||\n",[152,506,507],{"class":154,"line":178},[152,508,509],{"class":210},"CASE\n",[152,511,512],{"class":154,"line":191},[152,513,514],{"class":210},"  WHEN \"temp_c\" IS NULL THEN '\u003Ci>\u003Cspan style=\"color:#59645f\">no reading\u003C\u002Fspan>\u003C\u002Fi>'\n",[152,516,517],{"class":154,"line":198},[152,518,519],{"class":210},"  WHEN \"temp_c\" \u003C 0 THEN '\u003Cspan style=\"color:#2563eb; font-size:11pt\">' || format_number(\"temp_c\", 1) || ' °C\u003C\u002Fspan>'\n",[152,521,522],{"class":154,"line":223},[152,523,524],{"class":210},"  WHEN \"temp_c\" > 30 THEN '\u003Cspan style=\"color:#b91c1c; font-size:11pt\">\u003Cb>' || format_number(\"temp_c\", 1) || ' °C\u003C\u002Fb>\u003C\u002Fspan>'\n",[152,526,527],{"class":154,"line":228},[152,528,529],{"class":210},"  ELSE '\u003Cspan style=\"font-size:11pt\">' || format_number(\"temp_c\", 1) || ' °C\u003C\u002Fspan>'\n",[152,531,532],{"class":154,"line":239},[152,533,534],{"class":210},"END\"\"\"\n",[152,536,537],{"class":154,"line":251},[152,538,344],{"class":162},[152,540,541],{"class":154,"line":263},[152,542,360],{"class":162},[14,544,545,547,548,551],{},[364,546,366],{}," A ",[149,549,550],{},"CASE"," inside the expression picks a different HTML fragment per feature: blue for frost, bold red for heat, grey italics for missing values — checking NULL first, as always in expressions. Conditional styling inside labels complements data-defined properties: data-defined colour changes the whole label, while HTML spans change only part of it. Keep the number of styles small; a label with five colours is harder to read than two labels.",[116,553,555],{"id":554},"escape-attribute-values","Escape attribute values",[14,557,558,559,562,563,566,567,570,571],{},"Attribute values become part of the HTML. A name containing ",[149,560,561],{},"\u003C",", ",[149,564,565],{},">"," or ",[149,568,569],{},"&"," — \"Smith & Sons\", \"",[572,573,574],"unknown",{},"\" — breaks the markup or disappears. Escaping values before inserting them prevents that.",[142,576,578],{"className":144,"code":577,"language":146,"meta":147,"style":147},"from qgis.core import QgsExpression, qgsfunction\n\n@qgsfunction(args=\"auto\", group=\"Custom\", referenced_columns=[])\ndef html_escape(value, feature, parent):\n    \"\"\"Escape &, \u003C and > for use in HTML-formatted labels.\"\"\"\n    if value is None:\n        return \"\"\n    return (str(value).replace(\"&\", \"&amp;\").replace(\"\u003C\", \"&lt;\").replace(\">\", \"&gt;\"))\n\nsettings.fieldName = \"'\u003Cb>' || html_escape(\\\"name\\\") || '\u003C\u002Fb>\u003Cbr>' || html_escape(\\\"operator\\\")\"\nstations.setLabeling(QgsVectorLayerSimpleLabeling(settings))\n",[149,579,580,591,595,633,644,649,666,674,719,723,753],{"__ignoreMap":147},[152,581,582,584,586,588],{"class":154,"line":155},[152,583,159],{"class":158},[152,585,163],{"class":162},[152,587,166],{"class":158},[152,589,590],{"class":162}," QgsExpression, qgsfunction\n",[152,592,593],{"class":154,"line":172},[152,594,195],{"emptyLinePlaceholder":194},[152,596,597,601,604,608,610,613,615,618,620,623,625,628,630],{"class":154,"line":178},[152,598,600],{"class":599},"svObZ","@qgsfunction",[152,602,603],{"class":162},"(",[152,605,607],{"class":606},"s9osk","args",[152,609,204],{"class":158},[152,611,612],{"class":210},"\"auto\"",[152,614,562],{"class":162},[152,616,617],{"class":606},"group",[152,619,204],{"class":158},[152,621,622],{"class":210},"\"Custom\"",[152,624,562],{"class":162},[152,626,627],{"class":606},"referenced_columns",[152,629,204],{"class":158},[152,631,632],{"class":162},"[])\n",[152,634,635,638,641],{"class":154,"line":191},[152,636,637],{"class":158},"def",[152,639,640],{"class":599}," html_escape",[152,642,643],{"class":162},"(value, feature, parent):\n",[152,645,646],{"class":154,"line":198},[152,647,648],{"class":210},"    \"\"\"Escape &, \u003C and > for use in HTML-formatted labels.\"\"\"\n",[152,650,651,654,657,660,663],{"class":154,"line":223},[152,652,653],{"class":158},"    if",[152,655,656],{"class":162}," value ",[152,658,659],{"class":158},"is",[152,661,662],{"class":217}," None",[152,664,665],{"class":162},":\n",[152,667,668,671],{"class":154,"line":228},[152,669,670],{"class":158},"        return",[152,672,673],{"class":210}," \"\"\n",[152,675,676,679,682,685,688,691,693,696,699,702,704,707,709,712,714,717],{"class":154,"line":239},[152,677,678],{"class":158},"    return",[152,680,681],{"class":162}," (",[152,683,684],{"class":217},"str",[152,686,687],{"class":162},"(value).replace(",[152,689,690],{"class":210},"\"&\"",[152,692,562],{"class":162},[152,694,695],{"class":210},"\"&amp;\"",[152,697,698],{"class":162},").replace(",[152,700,701],{"class":210},"\"\u003C\"",[152,703,562],{"class":162},[152,705,706],{"class":210},"\"&lt;\"",[152,708,698],{"class":162},[152,710,711],{"class":210},"\">\"",[152,713,562],{"class":162},[152,715,716],{"class":210},"\"&gt;\"",[152,718,248],{"class":162},[152,720,721],{"class":154,"line":251},[152,722,195],{"emptyLinePlaceholder":194},[152,724,725,727,729,732,735,738,740,743,745,748,750],{"class":154,"line":263},[152,726,315],{"class":162},[152,728,204],{"class":158},[152,730,731],{"class":210}," \"'\u003Cb>' || html_escape(",[152,733,734],{"class":217},"\\\"",[152,736,737],{"class":210},"name",[152,739,734],{"class":217},[152,741,742],{"class":210},") || '\u003C\u002Fb>\u003Cbr>' || html_escape(",[152,744,734],{"class":217},[152,746,747],{"class":210},"operator",[152,749,734],{"class":217},[152,751,752],{"class":210},")\"\n",[152,754,755],{"class":154,"line":274},[152,756,344],{"class":162},[14,758,759,761,762,764,765,769],{},[364,760,366],{}," A custom expression function, registered with the ",[149,763,600],{}," decorator, escapes the three characters that matter in HTML text. Using it around every attribute inserted into the markup makes labels robust against whatever the data contains. Register the function at startup or in a plugin so projects that use it work for everyone; ",[21,766,768],{"href":767},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fregister-custom-expression-function-pyqgis\u002F","registering a custom expression function"," covers the details. Recent QGIS releases also include built-in string functions for this; check the expression builder's String group on your version.",[116,771,773],{"id":772},"superscripts-units-and-small-caps","Superscripts, units and small caps",[14,775,776],{},"Cartographic text often needs typographic details: m² and m³, footnote markers, small capitals for region names. HTML labels can produce many of them.",[14,778,779],{},[29,780,783,786,789,792,795,800,805,809,813,817,821,824,827,831,835,838,841,845,849,852],{"viewBox":781,"role":32,"ariaLabel":782,"xmlns":34},"0 0 760 230","Typographic details in labels: superscript units, footnote markers, small caps and subscripts, with Unicode characters as a fallback",[36,784,785],{},"Typographic details",[40,787,788],{},"Examples of label typography with HTML: an area value with a superscript 2 for square metres, a footnote marker in superscript, a region name in small caps using a smaller upper-case span, and a chemical formula with subscripts. Where superscript tags are not supported, the Unicode characters for superscript two and three are a reliable alternative.",[44,790],{"x":47,"y":47,"width":409,"height":791,"fill":50},"230",[52,793,794],{"x":413,"y":55,"style":56,"fill":57,"textAnchor":58},"Units, markers, small caps",[44,796],{"x":417,"y":418,"width":797,"height":64,"rx":91,"fill":798,"stroke":799,"style":93},"168","#eef7f4","#0f766e",[52,801,804],{"x":802,"y":803,"style":427,"fill":799,"textAnchor":58},"108","106.78","units",[52,806,808],{"x":802,"y":807,"style":433,"fill":104,"textAnchor":58},"134.78","12,400 m²",[52,810,812],{"x":802,"y":811,"style":433,"fill":84,"textAnchor":58},"162.78","sup or Unicode",[44,814],{"x":815,"y":418,"width":797,"height":64,"rx":91,"fill":816,"stroke":78,"style":93},"206","#eff3ff",[52,818,820],{"x":819,"y":803,"style":427,"fill":78,"textAnchor":58},"290","markers",[52,822,823],{"x":819,"y":807,"style":433,"fill":104,"textAnchor":58},"Station A¹",[52,825,826],{"x":819,"y":811,"style":433,"fill":84,"textAnchor":58},"footnotes",[44,828],{"x":829,"y":418,"width":797,"height":64,"rx":91,"fill":450,"stroke":830,"style":93},"388","#b45309",[52,832,834],{"x":833,"y":803,"style":427,"fill":830,"textAnchor":58},"472","small caps",[52,836,837],{"x":833,"y":807,"style":433,"fill":104,"textAnchor":58},"HARZ",[52,839,840],{"x":833,"y":811,"style":433,"fill":84,"textAnchor":58},"smaller caps",[44,842],{"x":843,"y":418,"width":844,"height":64,"rx":91,"fill":92,"stroke":84,"style":93},"570","166",[52,846,848],{"x":847,"y":803,"style":427,"fill":57,"textAnchor":58},"653","formulas",[52,850,851],{"x":847,"y":807,"style":433,"fill":104,"textAnchor":58},"NO₂",[52,853,854],{"x":847,"y":811,"style":433,"fill":84,"textAnchor":58},"sub or Unicode",[142,856,858],{"className":144,"code":857,"language":146,"meta":147,"style":147},"settings.fieldName = \"\"\"\n'\u003Cspan style=\"font-variant:small-caps; letter-spacing:1px\">' || upper(\"region\") || '\u003C\u002Fspan>\u003Cbr>' ||\nformat_number(\"area_m2\", 0) || ' m²\u003Csup>1\u003C\u002Fsup>'\"\"\"\n",[149,859,860,868,873],{"__ignoreMap":147},[152,861,862,864,866],{"class":154,"line":155},[152,863,315],{"class":162},[152,865,204],{"class":158},[152,867,499],{"class":210},[152,869,870],{"class":154,"line":172},[152,871,872],{"class":210},"'\u003Cspan style=\"font-variant:small-caps; letter-spacing:1px\">' || upper(\"region\") || '\u003C\u002Fspan>\u003Cbr>' ||\n",[152,874,875],{"class":154,"line":178},[152,876,877],{"class":210},"format_number(\"area_m2\", 0) || ' m²\u003Csup>1\u003C\u002Fsup>'\"\"\"\n",[14,879,880,882,883,886,887,890,891,894],{},[364,881,366],{}," Unicode superscript characters such as ² and ³ render everywhere and are the safest choice for units. ",[149,884,885],{},"\u003Csup>"," and ",[149,888,889],{},"\u003Csub>"," tags, supported on recent releases, handle arbitrary superscripts like footnote markers. Small caps can be imitated with an upper-case span at a slightly smaller size where ",[149,892,893],{},"font-variant"," is not honoured. Test typographic details in an actual export: screen rendering and PDF output can differ for small text.",[116,896,898],{"id":897},"build-label-expressions-from-python","Build label expressions from Python",[14,900,901],{},"Long HTML label expressions are hard to read and easy to break with a missing quote. Building them from small Python helpers keeps each part readable and makes styles reusable across layers.",[142,903,905],{"className":144,"code":904,"language":146,"meta":147,"style":147},"def span(expr, size=None, colour=None, bold=False, italic=False):\n    style = []\n    if size:\n        style.append(f\"font-size:{size}pt\")\n    if colour:\n        style.append(f\"color:{colour}\")\n    inner = expr\n    if bold:\n        inner = f\"'\u003Cb>' || {inner} || '\u003C\u002Fb>'\"\n    if italic:\n        inner = f\"'\u003Ci>' || {inner} || '\u003C\u002Fi>'\"\n    if style:\n        return f\"'\u003Cspan style=\\\"{'; '.join(style)}\\\">' || {inner} || '\u003C\u002Fspan>'\"\n    return inner\n\ndef lines(*parts):\n    return \" || '\u003Cbr>' || \".join(parts)\n\nexpr = lines(\n    span('html_escape(\"name\")', bold=True),\n    span(\"format_number(\\\"temp_c\\\", 1) || ' °C'\", size=11, colour=\"#2563eb\"),\n    span(\"format_number(\\\"elev_m\\\", 0) || ' m'\", size=7, colour=\"#59645f\"),\n)\nsettings.fieldName = expr\nprint(expr)\n",[149,906,907,947,957,964,989,996,1017,1027,1034,1057,1064,1084,1091,1124,1131,1135,1150,1160,1164,1174,1194,1231,1267,1272,1281],{"__ignoreMap":147},[152,908,909,911,914,917,919,922,925,927,929,932,934,937,940,942,944],{"class":154,"line":155},[152,910,637],{"class":158},[152,912,913],{"class":599}," span",[152,915,916],{"class":162},"(expr, size",[152,918,204],{"class":158},[152,920,921],{"class":217},"None",[152,923,924],{"class":162},", colour",[152,926,204],{"class":158},[152,928,921],{"class":217},[152,930,931],{"class":162},", bold",[152,933,204],{"class":158},[152,935,936],{"class":217},"False",[152,938,939],{"class":162},", italic",[152,941,204],{"class":158},[152,943,936],{"class":217},[152,945,946],{"class":162},"):\n",[152,948,949,952,954],{"class":154,"line":172},[152,950,951],{"class":162},"    style ",[152,953,204],{"class":158},[152,955,956],{"class":162}," []\n",[152,958,959,961],{"class":154,"line":178},[152,960,653],{"class":158},[152,962,963],{"class":162}," size:\n",[152,965,966,969,972,975,978,981,984,987],{"class":154,"line":191},[152,967,968],{"class":162},"        style.append(",[152,970,971],{"class":158},"f",[152,973,974],{"class":210},"\"font-size:",[152,976,977],{"class":217},"{",[152,979,980],{"class":162},"size",[152,982,983],{"class":217},"}",[152,985,986],{"class":210},"pt\"",[152,988,260],{"class":162},[152,990,991,993],{"class":154,"line":198},[152,992,653],{"class":158},[152,994,995],{"class":162}," colour:\n",[152,997,998,1000,1002,1005,1007,1010,1012,1015],{"class":154,"line":223},[152,999,968],{"class":162},[152,1001,971],{"class":158},[152,1003,1004],{"class":210},"\"color:",[152,1006,977],{"class":217},[152,1008,1009],{"class":162},"colour",[152,1011,983],{"class":217},[152,1013,1014],{"class":210},"\"",[152,1016,260],{"class":162},[152,1018,1019,1022,1024],{"class":154,"line":228},[152,1020,1021],{"class":162},"    inner ",[152,1023,204],{"class":158},[152,1025,1026],{"class":162}," expr\n",[152,1028,1029,1031],{"class":154,"line":239},[152,1030,653],{"class":158},[152,1032,1033],{"class":162}," bold:\n",[152,1035,1036,1039,1041,1044,1047,1049,1052,1054],{"class":154,"line":251},[152,1037,1038],{"class":162},"        inner ",[152,1040,204],{"class":158},[152,1042,1043],{"class":158}," f",[152,1045,1046],{"class":210},"\"'\u003Cb>' || ",[152,1048,977],{"class":217},[152,1050,1051],{"class":162},"inner",[152,1053,983],{"class":217},[152,1055,1056],{"class":210}," || '\u003C\u002Fb>'\"\n",[152,1058,1059,1061],{"class":154,"line":263},[152,1060,653],{"class":158},[152,1062,1063],{"class":162}," italic:\n",[152,1065,1066,1068,1070,1072,1075,1077,1079,1081],{"class":154,"line":274},[152,1067,1038],{"class":162},[152,1069,204],{"class":158},[152,1071,1043],{"class":158},[152,1073,1074],{"class":210},"\"'\u003Ci>' || ",[152,1076,977],{"class":217},[152,1078,1051],{"class":162},[152,1080,983],{"class":217},[152,1082,1083],{"class":210}," || '\u003C\u002Fi>'\"\n",[152,1085,1086,1088],{"class":154,"line":285},[152,1087,653],{"class":158},[152,1089,1090],{"class":162}," style:\n",[152,1092,1093,1095,1097,1100,1103,1106,1109,1112,1115,1117,1119,1121],{"class":154,"line":290},[152,1094,670],{"class":158},[152,1096,1043],{"class":158},[152,1098,1099],{"class":210},"\"'\u003Cspan style=",[152,1101,1102],{"class":217},"\\\"{",[152,1104,1105],{"class":210},"'; '",[152,1107,1108],{"class":162},".join(style)",[152,1110,1111],{"class":217},"}\\\"",[152,1113,1114],{"class":210},">' || ",[152,1116,977],{"class":217},[152,1118,1051],{"class":162},[152,1120,983],{"class":217},[152,1122,1123],{"class":210}," || '\u003C\u002Fspan>'\"\n",[152,1125,1126,1128],{"class":154,"line":301},[152,1127,678],{"class":158},[152,1129,1130],{"class":162}," inner\n",[152,1132,1133],{"class":154,"line":312},[152,1134,195],{"emptyLinePlaceholder":194},[152,1136,1137,1139,1142,1144,1147],{"class":154,"line":323},[152,1138,637],{"class":158},[152,1140,1141],{"class":599}," lines",[152,1143,603],{"class":162},[152,1145,1146],{"class":158},"*",[152,1148,1149],{"class":162},"parts):\n",[152,1151,1152,1154,1157],{"class":154,"line":329},[152,1153,678],{"class":158},[152,1155,1156],{"class":210}," \" || '\u003Cbr>' || \"",[152,1158,1159],{"class":162},".join(parts)\n",[152,1161,1162],{"class":154,"line":335},[152,1163,195],{"emptyLinePlaceholder":194},[152,1165,1166,1169,1171],{"class":154,"line":341},[152,1167,1168],{"class":162},"expr ",[152,1170,204],{"class":158},[152,1172,1173],{"class":162}," lines(\n",[152,1175,1176,1179,1182,1184,1187,1189,1191],{"class":154,"line":347},[152,1177,1178],{"class":162},"    span(",[152,1180,1181],{"class":210},"'html_escape(\"name\")'",[152,1183,562],{"class":162},[152,1185,1186],{"class":606},"bold",[152,1188,204],{"class":158},[152,1190,280],{"class":217},[152,1192,1193],{"class":162},"),\n",[152,1195,1196,1198,1201,1203,1206,1208,1211,1213,1215,1217,1220,1222,1224,1226,1229],{"class":154,"line":357},[152,1197,1178],{"class":162},[152,1199,1200],{"class":210},"\"format_number(",[152,1202,734],{"class":217},[152,1204,1205],{"class":210},"temp_c",[152,1207,734],{"class":217},[152,1209,1210],{"class":210},", 1) || ' °C'\"",[152,1212,562],{"class":162},[152,1214,980],{"class":606},[152,1216,204],{"class":158},[152,1218,1219],{"class":217},"11",[152,1221,562],{"class":162},[152,1223,1009],{"class":606},[152,1225,204],{"class":158},[152,1227,1228],{"class":210},"\"#2563eb\"",[152,1230,1193],{"class":162},[152,1232,1234,1236,1238,1240,1243,1245,1248,1250,1252,1254,1256,1258,1260,1262,1265],{"class":154,"line":1233},22,[152,1235,1178],{"class":162},[152,1237,1200],{"class":210},[152,1239,734],{"class":217},[152,1241,1242],{"class":210},"elev_m",[152,1244,734],{"class":217},[152,1246,1247],{"class":210},", 0) || ' m'\"",[152,1249,562],{"class":162},[152,1251,980],{"class":606},[152,1253,204],{"class":158},[152,1255,65],{"class":217},[152,1257,562],{"class":162},[152,1259,1009],{"class":606},[152,1261,204],{"class":158},[152,1263,1264],{"class":210},"\"#59645f\"",[152,1266,1193],{"class":162},[152,1268,1270],{"class":154,"line":1269},23,[152,1271,260],{"class":162},[152,1273,1275,1277,1279],{"class":154,"line":1274},24,[152,1276,315],{"class":162},[152,1278,204],{"class":158},[152,1280,1026],{"class":162},[152,1282,1284,1287],{"class":154,"line":1283},25,[152,1285,1286],{"class":217},"print",[152,1288,1289],{"class":162},"(expr)\n",[14,1291,1292,1294,1295,1298],{},[364,1293,366],{}," Each helper returns an expression fragment, and ",[149,1296,1297],{},"lines"," joins fragments with line breaks, so the structure of the label — name, temperature, elevation — reads directly from the Python code. Styles are parameters rather than hand-typed CSS, which removes a whole class of quoting mistakes. Printing the final expression lets you paste it into the expression builder to preview it on real features before applying it, the quickest way to debug an HTML label. Keep the helpers in a shared module if several layers or projects use the same label style.",[116,1300,1302],{"id":1301},"use-html-labels-in-layouts-and-legends","Use HTML labels in layouts and legends",[14,1304,1305,1306,133],{},"HTML-formatted map labels render the same in print layouts, because layouts use the same labelling engine. Layout label items have their own HTML mode, configured separately, as described in ",[21,1307,1309],{"href":1308},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-label-and-html-items-to-layout-pyqgis\u002F","adding label and HTML items to a layout",[142,1311,1313],{"className":144,"code":1312,"language":146,"meta":147,"style":147},"from qgis.core import QgsLabelingEngineSettings\n\nengine = QgsProject.instance().labelingEngineSettings()\nengine.setFlag(QgsLabelingEngineSettings.DrawUnplacedLabels, False)\nQgsProject.instance().setLabelingEngineSettings(engine)\n",[149,1314,1315,1326,1330,1340,1349],{"__ignoreMap":147},[152,1316,1317,1319,1321,1323],{"class":154,"line":155},[152,1318,159],{"class":158},[152,1320,163],{"class":162},[152,1322,166],{"class":158},[152,1324,1325],{"class":162}," QgsLabelingEngineSettings\n",[152,1327,1328],{"class":154,"line":172},[152,1329,195],{"emptyLinePlaceholder":194},[152,1331,1332,1335,1337],{"class":154,"line":178},[152,1333,1334],{"class":162},"engine ",[152,1336,204],{"class":158},[152,1338,1339],{"class":162}," QgsProject.instance().labelingEngineSettings()\n",[152,1341,1342,1345,1347],{"class":154,"line":191},[152,1343,1344],{"class":162},"engine.setFlag(QgsLabelingEngineSettings.DrawUnplacedLabels, ",[152,1346,936],{"class":217},[152,1348,260],{"class":162},[152,1350,1351],{"class":154,"line":198},[152,1352,1353],{"class":162},"QgsProject.instance().setLabelingEngineSettings(engine)\n",[14,1355,1356,1358,1359,1363],{},[364,1357,366],{}," HTML labels are larger blocks than single-line labels, so they collide more often; the labelling engine settings decide what happens to labels that cannot be placed. Keeping unplaced labels hidden in exports, and using ",[21,1360,1362],{"href":1361},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fcontrol-label-placement-and-collisions-pyqgis\u002F","placement and collision settings"," such as priority and obstacles, keeps the map readable. Multi-line HTML labels usually work best with \"around point\" placement for points and \"horizontal\" placement for polygons.",[116,1365,1367],{"id":1366},"qgis-version-compatibility","QGIS version compatibility",[14,1369,1370,1371,1374],{},"HTML label formatting is available from QGIS 3.14 and on 3.34 LTR, 3.40 LTR and QGIS 4. Support for additional CSS properties and tags — including superscript, subscript and vertical alignment — expanded during the 3.2x and 3.3x series; test on the oldest version your projects must support. ",[149,1372,1373],{},"setAllowHtmlFormatting"," is the same on all of them.",[116,1376,1378],{"id":1377},"troubleshooting","Troubleshooting",[121,1380,1381,1387,1398,1404],{},[124,1382,1383,1386],{},[364,1384,1385],{},"Tags appear as literal text."," HTML formatting is not enabled on the text format.",[124,1388,1389,1392,1393,566,1395,1397],{},[364,1390,1391],{},"A label disappears entirely."," An attribute value contains ",[149,1394,561],{},[149,1396,569],{}," that broke the markup; escape values.",[124,1399,1400,1403],{},[364,1401,1402],{},"Sizes look different in print."," Pixel units were used in CSS; use points.",[124,1405,1406,1409],{},[364,1407,1408],{},"Labels collide more."," HTML labels are taller; adjust placement, priority and font sizes.",[116,1411,1413],{"id":1412},"conclusion","Conclusion",[14,1415,1416,1417,1419],{},"Enable HTML formatting on the label's text format, build label expressions that emit simple inline HTML — bold, italic, line breaks and styled spans — keep to the supported subset, style parts conditionally with ",[149,1418,550],{}," and NULL checked first, escape attribute values, use Unicode or sup\u002Fsub for typographic details, and check collisions and exports.",[116,1421,1423],{"id":1422},"frequently-asked-questions","Frequently Asked Questions",[14,1425,1426,1429,1430,1433],{},[364,1427,1428],{},"Can HTML labels include images?","\nRecent releases support ",[149,1431,1432],{},"\u003Cimg>"," tags in labels; check your version, and keep images small.",[14,1435,1436,1439],{},[364,1437,1438],{},"Do HTML labels work with curved placement?","\nLine-following placement uses plain text; HTML formatting applies best to horizontal labels.",[14,1441,1442,1445],{},[364,1443,1444],{},"Can I combine HTML labels with rule-based labelling?","\nYes. Each rule has its own settings and text format, so HTML formatting can be enabled for some rules — detailed labels at large scales — and plain labels used for others.",[14,1447,1448,1451,1452,133],{},[364,1449,1450],{},"Can I set the font for one word?","\nYes, with ",[149,1453,1454],{},"\u003Cspan style=\"font-family:'Noto Serif'\">",[14,1456,1457,1460],{},[364,1458,1459],{},"Why does my label show HTML code in the attribute table?","\nThe expression produces HTML text; only the label renderer interprets it. That is expected — store data plain and add markup only in the label expression.",[14,1462,1463,1466],{},[364,1464,1465],{},"Is there a performance cost?","\nA small one; for tens of thousands of labels, keep the HTML minimal.",[116,1468,1470],{"id":1469},"related","Related",[121,1472,1473,1478,1484,1490,1496],{},[124,1474,1475,1477],{},[21,1476,24],{"href":23}," — the guide this recipe belongs to",[124,1479,1480],{},[21,1481,1483],{"href":1482},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fadd-rule-based-labels-pyqgis\u002F","Add Rule-Based Labels in PyQGIS",[124,1485,1486],{},[21,1487,1489],{"href":1488},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fformat-label-buffers-backgrounds-and-shadows-pyqgis\u002F","Format Label Buffers, Backgrounds and Shadows in PyQGIS",[124,1491,1492],{},[21,1493,1495],{"href":1494},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fadd-label-callouts-pyqgis\u002F","Add Label Callouts in PyQGIS",[124,1497,1498],{},[21,1499,1500],{"href":767},"Register a Custom Expression Function in PyQGIS",[1502,1503,1504],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":147,"searchDepth":172,"depth":172,"links":1506},[1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518,1519],{"id":118,"depth":172,"text":119},{"id":136,"depth":172,"text":137},{"id":390,"depth":172,"text":391},{"id":481,"depth":172,"text":482},{"id":554,"depth":172,"text":555},{"id":772,"depth":172,"text":773},{"id":897,"depth":172,"text":898},{"id":1301,"depth":172,"text":1302},{"id":1366,"depth":172,"text":1367},{"id":1377,"depth":172,"text":1378},{"id":1412,"depth":172,"text":1413},{"id":1422,"depth":172,"text":1423},{"id":1469,"depth":172,"text":1470},"Mix fonts, sizes, colours and line breaks inside one map label with HTML text formatting — enabling allowHtmlFormatting, building label expressions that emit HTML, styling parts conditionally, handling special characters, and the limits of HTML in map labels.","md",{"slug":1523,"type":1524,"breadcrumb":1525,"datePublished":1526,"dateModified":1526},"use-html-formatted-labels-pyqgis","article","Use HTML-Formatted Labels","2026-10-02","\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fuse-html-formatted-labels-pyqgis",{"title":5,"description":1520},"pyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fuse-html-formatted-labels-pyqgis\u002Findex","YbxAhSQgGMYWtITKLs-qlUIjjhvCb9psgycjnLWAxmM",1790966252907]