Guide | Creating Fully Featured Custom Icon Sets for Themes
-
Note: I am currently working on writing all the posts, but figured it was better to post them as they are completed rather than all at once.
While the number of people willing to go through the trouble of creating an icon set may be fairly low (especially since the contest is over), it is still good to have all the information about how to create a fully featured icon set available. The Create a custom icon set help page is a good place to start, but it lacks all the details necessary to take full advantage of what is available.
Here are some guides on how to handle some of the more difficult icons and some tips for achieving interesting effects.
(WIP = Work in Progress)
Icons:
- Analog Clock Dial - Status Bar Button - (WIP)
- Dial hands
- Countdown timer
- Alarm
- Downloads - Address / Panel Bar Button - (WIP)
- Lining up the progress indicator
- Page Tiling - Status Bar Button - (WIP)
- Tiling horizontal/vertical state
- Panel Toggle - Address Bar Button - (WIP)
- Panel open/closed state
- Toggle Images - Status Bar Button - (WIP)
- Reflecting selected options
General State Variables:
--activeButton
CSS variable- Changing appearance of the active panel button icon
--buttonHover
CSS variable - (WIP)- Changing appearance of icons when their button is hovered
- Custom CSS variables
- Using custom CSS to add additional state variables
Misc:
- Using theme colors - (WIP)
- Using fallback values to properly show icons on the themes page - (WIP)
- Using Vivaldi's predefined gradients and masks - (WIP)
If you have any of your own tips and/or tricks, feel free to share them below and I will link them in this first post!
- Analog Clock Dial - Status Bar Button - (WIP)
-
Custom CSS State Variables
Throughout Vivaldi's various toolbars, there are several buttons with icons that are displayed differently depending on different states of the browser. The mail
Read
/Unread
icons in the theme editor are 2 states of the same icon, each with their own entry in the editor, while theImage Toggle
button has 1 single icon with numerous different states handled by CSS variables.Unfortunately, there are icons that lose functionality when you customize the icon. The
Break Mode
button, for example, shows a play or pause icon depending on whether break mode is active or not, when you use the default icon, but when it is customized, you lose the ability to display the play button state.While we wait for Vivaldi to hopefully add more icon states, either through individual states in the theme editor or CSS variable state control, we can accomplish some of the different state changes with a CSS modification. Icon sets dependent on this CSS mod can still be uploaded to the Theme Store, but people without the mod won't get the benefits of the additional states. Just make sure to have the default state of the icon be usable without the mod and link to this post in the theme description to help people unlock the additional functionality.
- I tried to find as many different icons dependant on different states as possible, but I might have missed some. Please let me know if there are any icons/states that are missing.
- Watch the change logs for
VB-96517 - Themes and multi state buttons
to see when some of these changes are hopefully implemented.
The CSS Mod: (Instructions for installing a CSS mod are here.)
/* Break mode state variable */ #browser { --breakModeActive: 0; } #browser.break-mode { --breakModeActive: 1; } /* Hidden extension toggle state variable */ #browser .toolbar-extensions { --extensionsExpanded: 0; } #browser .toolbar-extensions:has(.ExtensionIcon--Hidden), #browser:has(.extensionIconPopupMenu) .toolbar-extensions { --extensionsExpanded: 1; } /* Download state variables */ /* TODO: Find way to detect completed download (for --downloadCompleted) without JS */ .button-toolbar > button[name="DownloadButton"], .button-toolbar > button[name="PanelDownloads"] { --downloadInProgress: 0; --downloadCompleted: 0; } .button-toolbar > button[name="DownloadButton"]:has(+ .progress), .button-toolbar > button[name="PanelDownloads"]:has(+ .progress) { --downloadInProgress: 1; } /* Mail rendering method state variable */ /* TODO: Make independent of English interface language setting */ .button-toolbar > button[name="MailRenderingMethod"] { --mailRenderingIsHTML: 1; } .button-toolbar > button[name="MailRenderingMethod"][title*="html" i] { --mailRenderingIsHTML: 0; } /* Show Mail threads state variable */ /* TODO: Make independent of English interface language setting */ .button-toolbar > button[name="MailViewThreading"] { --mailThreadsAreShown: 0; } .button-toolbar > button[name="MailRenderingMethod"][title*="hide" i] { --mailThreadsAreShown: 1; } /* Mail view state variables */ .button-toolbar > button[name="MailViewLayout"] { --mailViewIsHorizontal: 0; --mailViewIsVertical: 0; --mailViewIsVerticalWide: 0; } #browser:has(#mail-view.vertical) .button-toolbar > button[name="MailViewLayout"] { --mailViewIsVertical: 1; } #browser:has(#mail-view.vertical_wide) .button-toolbar > button[name="MailViewLayout"] { --mailViewIsVerticalWide: 1; } #browser:has(#mail-view.horizontal) .button-toolbar > button[name="MailViewLayout"] { --mailViewIsHorizontal: 1; } /* Mail Flag state variable */ /* TODO: Not working and can't handle different colors */ /* .button-toolbar > button[name="MailMsgFlag"] { --mailWillFlag: 1; } #browser:has(.vivaldi-tree .tree-row[data-selected] > .mail_entry > .mail_entry_row:not(> .flag-color)) .button-toolbar > button[name="MailMsgFlag"], #browser:has(.vivaldi-tree .tree-row[data-selected] > .mail_entry > label:not(.mailattachment):not(.labels):not(> span)) .button-toolbar > button[name="MailMsgFlag"] { --mailWillFlag: 0; } */ /* Sync status state variable */ /* TODO: Don't know what broken sync looks like, and connected/disconnected is dependant on language */ /* Image toggle state variable for missing animation loop state */ .button-toolbar > button[name="ImagesToggle"][style*="--displayAnimationOnce: none; --displayAnimationNever: none;"] { --displayAnimationLoop: block; } /* Image toggle state variables that are usable in calculations */ .button-toolbar > button[name="ImagesToggle"] { --allImagesAreDisplayed: 0; --cachedImagesAreDisplayed: 0; --noImagesAreDisplayed: 0; --animationsArePlayedLooped: 0; --animationsArePlayedOnce: 0; --animationsArePlayedNever: 0; } .button-toolbar > button[name="ImagesToggle"][style*="--displayImagesAll: block;"] { --allImagesAreDisplayed: 1; } .button-toolbar > button[name="ImagesToggle"][style*="--displayImagesCached: block;"] { --cachedImagesAreDisplayed: 1; } .button-toolbar > button[name="ImagesToggle"][style*="--displayImagesNever: block;"] { --noImagesAreDisplayed: 1; } .button-toolbar > button[name="ImagesToggle"][style*="--displayAnimationOnce: none; --displayAnimationNever: none;"] { --animationsArePlayedLooped: 1; } .button-toolbar > button[name="ImagesToggle"][style*="--displayAnimationOnce: block;"] { --animationsArePlayedOnce: 1; } .button-toolbar > button[name="ImagesToggle"][style*="--displayAnimationNever: block;"] { --animationsArePlayedNever: 1; } /* Tab tiling toggle state variables that are usable in calculations */ .button-toolbar > button[name="TilingToggle"] { --tileColumnIsDisplayed: 0; --tileRowIsDisplayed: 0; } .button-toolbar > button[name="TilingToggle"][style*="--displayTileColumn: unset;"] { --tileColumnIsDisplayed: 1; } .button-toolbar > button[name="TilingToggle"][style*="--displayTileRow: unset;"] { --tileRowIsDisplayed: 1; } /* Clock state variables that are usable in calculations */ .button-toolbar > button[name="Clock"] { --clockCountdownSet: 0; --clockAlarmSet: 0; } .button-toolbar > button[name="Clock"][style*="--countdownHourPercent"], .button-toolbar > button[name="Clock"][style*="--countdownMinutePercent"], .button-toolbar > button[name="Clock"][style*="--countdownSecondPercent"] { --clockCountdownSet: 1; } .button-toolbar.ClockButton--alarm > button[name="Clock"] { --clockAlarmSet: 1; } /* Page action state variable */ .button-toolbar > button[name="PageActions"] { --pageActionActive: 0; } .button-toolbar > button[name="PageActions"].button-on { --pageActionActive: 1; } /* Capture Images state variable */ .button-toolbar > button[name="CaptureImages"] { --captureImagesActive: 0; } #browser:has(#capture-area) .button-toolbar > button[name="CaptureImages"] { --captureImagesActive: 1; }
-
--activeButton
CSS VariableVivaldi has a few general purpose CSS variables that can be used to manipulate the appearance of icons. One of these variables is the
--activeButton
variable, which is set on each panel toolbar button to indicate if the panel associated with the button is open. When a panel isn't open,--activeButton
has a value of0
, and when it is, the value is1
.
Direct usage of the variable
On its own, the variable can be used to control things directly, like with
opacity: var(--activeButton);
ortransform: scale(var(--activeButton));
which can be applied in thestyle=""
attributes of SVG elements likepath
s, where a value of0
will make the object disappear and1
will make it show up again. This can either be an instantaneous change, or you can pair it with atransition
to give it an animated appearance. More info ontransition
here: https://developer.mozilla.org/en-US/docs/Web/CSS/transitionIf you use
transform: scale(var(--activeButton));
with atransition
, you might notice the SVG element scaling up in an odd way that is relative to the top left corner of the icon. This happens because thescale
is relative to theorigin
of the SVG, which is the top left corner by default. Luckily, you can adjust the position of theorigin
if the default behavior isn't what you are looking for with the propertytransform-origin
. For a centered element,transform-origin: center;
can be used, but you can also set specific x and y coordinates if necessary. More info ontransform-origin
here: https://developer.mozilla.org/en-US/docs/Web/CSS/transform-originSometimes it can also be necessary to have the opposite behavior of simply putting the
--activeButton
variable into a CSS property. For example, if you wanted an SVG element to disappear when a panel button is activated, then you couldn't use the variable on its own. In such cases, you can easily invert the variable and set a new variable to hold it like so:--activeButtonInverse: calc(1 - var(--activeButton));
. Then using something likeopacity: var(--activeButtonInverse);
will let you accomplish your goal. If you are going to use the inverted variable for multiple elements, then it makes sense to set the variable definition on a parent group element,<g>
, or even on the main<svg>
element, so you don't need to repeat the inverted variable definition in thestyle=""
attribute of each element that requires it.
Using the variable with
calc()
for greater controlCSS has many numerical properties that can be manipulated but have ranges that fall outside of the range between
0
and1
, so for the--activeButton
variable to be useful with these properties, you need to use it withincalc()
s which allows you to use mathematical operators on the contents of thecalc()
.Say you want to have a part of the icon move from the top of the icon to the bottom when the panel button is activated. To accomplish this, you would use
transform: translateY();
, but since SVG icons are suggested to fit in a16x16
pixel area of a28x28
pixel icon, you won't be able to move the icon element the entire distance without increasing the effect of the--activeButton
variable like so:transform: translateY(calc(16px * var(--activeButton)));
. So when the panel button isn't active (--activeButton
is0
) thecalc()
will return0px
and not move the element, but once the button is activated and the variable is1
, the element will be moved16px
down in the icon.Using the
calc()
also allows you to convert the unitless variable into a pixel value by including thepx
on the multiplication factor. Depending on the CSS attribute you are manipulating, a unit of measurement or percentage value might be required.Another way you could use
calc()
with--activeButton
is to provide a slight icon size increase to help indicate that the panel button is active. This could be accomplished with something like this:style="transform-origin: center; transform: scale(calc(0.2 * var(--activeButton) + 1)); transition: transform 0.1s ease-in;"
. This way, thescale
will default to1
when on an inactive panel button and increase to1.2
times larger for an active one.
Changing icon color with
--activeButton
Vivaldi suggests using the option
currentColor
for allfill
s andstroke
s in SVG icons to better allow the icon set to match the theme it is paired with, but when trying to indicate that a button is active, it can be beneficial to change the color from the normal white or blackForeground
color to something more distinctive.By default, when you activate a panel button, it gets a bar along the side that uses your theme's
Highlight
color, so you might want to change the icon color to match. You can get the themeHighlight
color with the CSS variablevar(--colorHighlightBg)
, but you can't use acalc()
alone to toggle between between 2 non-numerical values. That is wherecolor-mix()
can be useful. If you use something like this on the top level<svg>
tag:style="color: color-mix(in srgb, var(--colorHighlightBg) calc(var(--activeButton) * 100%), currentcolor);"
, then you can control what ratio of theHighlight
color and theForeground
(currentcolor
) are mixed together to result in the final used color. By toggling between0%
and100%
of theHighlight
color ratio, you can completely swap the color without any mixing. More info oncolor-mix()
here: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color-mix
Example:
History Panel
icon with animated active indicatorPreview:
Final SVG:
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="none" viewBox="0 0 28 28" style="--activeButtonInverse: calc(1 - var(--activeButton));"> <path id="history-icon-blob-1" fill="#b193f0" style="transform: translateX(calc(-22px * var(--activeButtonInverse))); opacity: var(--activeButton); transition: 0.5s;" d="M8.83 24.72c9.97.79 14.37-7.1 11.77-13.82C17.2 2.13.32-1.47 2.84 7.3c1.69 5.9-.13 16.94 5.99 17.42Z" /> <path id="history-icon-blob-2" fill="#ffd75a" style="transform: translateX(calc(26px * var(--activeButtonInverse))); opacity: var(--activeButton); transition: 0.5s;" d="M12.38 9.66c-18.1 1.38-10.38 8.21.72 12.12 9.26 3.26 11.92-1.24 12.35-7.01C26.68-1.8 20.8 9 12.38 9.66Z" /> <path id="history-icon-star-1" fill="#fff" style="transform-origin: 20.4px 5.4px; transform: scale(var(--activeButton)); opacity: var(--activeButton); transition: 0.3s; transition-delay: 0.2s;" d="M20.45 2.2a3.2 3.2 0 0 1-3.2 3.2 3.2 3.2 0 0 1 3.2 3.2 3.2 3.2 0 0 1 3.2-3.2 3.2 3.2 0 0 1-3.2-3.2z" /> <path id="history-icon-star-2" fill="#fff" style="transform-origin: 8.8px 23.5px; transform: scale(var(--activeButton)); opacity: var(--activeButton); transition: 0.3s; transition-delay: 0.4s;" d="M8.92 20.32a3.2 3.2 0 0 1-3.2 3.2 3.2 3.2 0 0 1 3.2 3.2 3.2 3.2 0 0 1 3.2-3.2 3.2 3.2 0 0 1-3.2-3.2z" /> <g> <path fill="#fff" stroke="#fff" stroke-width=".5" d="m10.006 8.077.991 1.712a6.545 4.55 13.88 0 1 .338-.078l-.913-1.589Zm3.965.35v1.17a6.545 4.55 13.88 0 1 .338.046V8.46Zm3.537.315-.901 1.566a6.545 4.55 13.88 0 1 .304.135l.969-1.667Zm-11.085 1.51-.168.292 1.903 1.104a6.545 4.55 13.88 0 1 .203-.282zm15.433 0-2.839 1.644a6.545 4.55 13.88 0 1 .237.248l2.782-1.6-.169-.293zm-15.68 4.483.022.338h1.803a6.545 4.55 13.88 0 1-.136-.338Zm14.328 0a6.545 4.55 13.88 0 1 0 .338h1.352l.034-.338zM9.837 17.202l-3.312 1.915a1.352 1.352 0 0 0 .169.292l3.447-1.993a6.545 4.55 13.88 0 1-.304-.214Zm9.394.439a6.545 4.55 13.88 0 1-.292.225l2.534 1.465a1.318 1.318 0 0 0 .079-.338zm-7.265.71-.969 1.678h.394l.901-1.566a6.545 4.55 13.88 0 1-.326-.112zm4.63.45a6.545 4.55 13.88 0 1-.35.056l.733 1.285h.394zm-2.614.045v1.24h.338V18.89a6.545 4.55 13.88 0 1-.338-.045z" /> <path stroke="#fd7100" stroke-width="1.126" d="m6.942 7.807 14.137 1.25a1.25 1.25 0 0 1 1.138 1.352l-.676 8.572a1.318 1.318 0 0 1-1.34 1.217l-12.46-.225a1.352 1.352 0 0 1-1.318-1.262l-.642-9.778a1.059 1.059 0 0 1 1.16-1.126Z" /> <path fill="#f30000" d="m16.834 12.574-.546.14-2.42 1.876-.107 1.066 1.066-.11 1.87-2.425z" /> <path fill="#f30000" d="m10.51 11.82.183.531 3.21 3.23 1.072.02-.196-1.053-3.713-2.636z" /> </g> </svg>
Design Notes:
(WIP)...