Silk
Silk is a UI layer included with Kobweb and built upon Compose HTML.
While Compose HTML requires you to understand underlying HTML / CSS concepts, Silk attempts to abstract some of that away, providing an API more akin to what you might experience developing a Compose app on Android or Desktop. Less "div, span, flexbox, attrs, styles, classes" and more "Rows, Columns, Boxes, and Modifiers".
We consider Silk a pretty important part of the Kobweb experience, but it's worth pointing out that it's designed as an optional component. You can absolutely use Kobweb without Silk. (You can also use Silk without Kobweb!)
You can also interleave Silk and Compose HTML components easily (as Silk is just composing them itself).
@InitSilk
methods
Before going further, we want to quickly mention you can annotate a method with @InitSilk
, which will be called when your site starts up.
This method must take a single InitSilkContext
parameter. A context contains various properties that allow adjusting Silk defaults, which will be demonstrated in more detail in sections below.
The names of your @InitSilk
methods don't matter, as long as they're public, take a single InitSilkContext
parameter, and don't collide with another method of the same name. You are encouraged to choose a name for readability purposes.
You can define as many @InitSilk
methods as you want, so feel free to break them up into relevant, clearly named pieces, instead of declaring a single, monolithic, generically named fun initSilk(ctx)
method that does everything.
Just be sure you're OK with them being called in any order, as no particular call order is guaranteed.
CssStyle
With Silk, you can define a style block. This lets you declare modifiers ( Modifier) in a way that will ultimately get embedded into a CSS stylesheet ( Stylesheet advantages).
You do this using the CssStyle
function and putting your modifier in the base
block:
We'll discuss what this base
block is in the next section, so don't worry about it for the moment.
You can convert any such CssStyle
into a Modifier
by using its toModifier()
method (e.g. CustomStyle.toModifier()
). At this point, you can pass it into any composable which takes a Modifier
parameter:
When you declare a CssStyle
, it must be public. This is because code gets generated inside a main.kt
file by the Kobweb Gradle plugin, and that code needs to be able to access your style in order to register it.
In general, it's a good idea to think of styles as global anyway, since technically they all live in a globally applied stylesheet, and you have to make sure that the style name is unique across your whole application.
You can technically make a style private if you add a bit of boilerplate to handle the registration yourself:
However, you are encouraged to keep your styles public and let the Kobweb Gradle plugin handle everything for you.
Additional selectors
So, what's up with the base
block?
True, it looks a bit verbose on its own. However, you can define additional selector blocks that take effect conditionally. The base style will always apply first, but then any additional styles will be applied based on the specific selector's rules.
Order matters when defining additional selectors, especially if multiple selectors are applicable at the same time.
Here, we create a style which is red by default but green when the mouse hovers over it:
Kobweb provides a bunch of standard selectors for you for convenience, but for those who are CSS-savvy, you can always define the CSS rule directly to enable more complex combinations or selectors that Kobweb hasn't added yet.
For example, this is identical to the above style definition:
CssStyle name
The Kobweb Gradle plugin automatically detects your CssStyle
properties and generates a name for it for you, derived from the property name itself but using Kebab Case.
For example, if you write val TitleTextStyle = CssStyle { ... }
, its name will be "title-text".
You usually won't need to care about this name, but if you inspect the DOM using browser devtools, you'll see it there.
If you need to set a name manually, you can use the CssName
annotation to override the default name:
CssStyle.base
A large number of CssStyle
blocks only contain the base
method, so Kobweb provides a convenience syntax for that common case:
You can easily break the base
block out later if you find yourself needing to support additional selectors.
Breakpoints
There's a feature in the world of responsive HTML / CSS design called breakpoints, which confusingly have nothing to do with debugging breakpoints. Rather, they specify size boundaries for your site when styles change. This is how sites present content differently on mobile vs. tablet vs. desktop.
Kobweb provides four breakpoint sizes you can use for your project, which, including using no breakpoint size at all, gives you five buckets you can work with when designing your site:
- no breakpoint - mobile (and larger)
- sm - tablets (and larger)
- md - desktops (and larger)
- lg - widescreen (and larger)
- xl - ultra widescreen (and larger)
You can change the default values of breakpoints for your site by adding an @InitSilk
method to your code and setting ctx.theme.breakpoints
:
To reference a breakpoint in a CssStyle
, just invoke it:
When testing your breakpoint-conditional styles, you should be aware that browser dev tools let you simulate window dimensions to see how your site looks at different sizes. For example, on Chrome, you can follow these instructions: https://developer.chrome.com/docs/devtools/device-mode
You can also specify that a style should only apply to a specific range of breakpoints using Kotlin range operators:
If you aren't a fan of needing to wrap the breakpoint range expression with parentheses, the between
method is provided as well, which is otherwise identical to the ..<
range operator:
Finally, if the first breakpoint in your range is Breakpoint.ZERO
, you can shorten your expression by using the until
method instead:
In fact, you can think of until
as the inverse to declaring a normal breakpoint. In other words, until(Breakpoint.MD) { ... }
means all breakpoint sizes up to the medium size, while Breakpoint.MD { ... }
means medium size and above.
Color-mode aware
When you define a CssStyle
, a property called colorMode
is available for you to use:
Silk defines a bunch of light and dark colors for all of its widgets, and if you'd like to re-use any of them in your own widget, you can query them using colorMode.toPalette()
:
SilkTheme
contains very simple (e.g. black and white) defaults, but you can override them in an @InitSilk
method, perhaps to something that is more brand-aware:
Initial color mode
By default, Kobweb will initialize your site's color mode to ColorMode.LIGHT
.
However, you can control this by setting the initialColorMode
property in an @InitSilk
method:
If you'd like to respect the user's system preferences, you can set initialColorMode
to ColorMode.systemPreference
:
Persisting color-mode preference
If you support toggling the site's color mode, you are encouraged to save the user's last chosen setting into local storage and then restore it if the user revisits your site later.
The restoration will happen in your @InitSilk
block, while the code to save the color mode should happen in your root @App
composable ( Application Root):
Extending CSS styles
You may find yourself occasionally wanting to define a style that should only be applied along with / after another style.
The easiest way to accomplish this is by extending the base CSS style block, using the extendedBy
method:
Once extended, you only need to call toModifier
on the extended style to include both styles automatically:
Component styles
So far, we've discussed basic CSS style blocks that define a miscellaneous assortment of CSS style properties.
However, there is a way to define typed CSS style blocks. You can generate typed variants from them, which tweak or extend their base styles, essentially. You cannot use a variant generated from one typed CSS style block with a different one of another type.
This typed CSS style is called a component style because the pattern is effective when defining widget components. In fact, it is the standard pattern that Silk uses for every single one of its widgets.
To declare one, you first create a marker interface that implements ComponentKind
and then specify that as a type for your CssStyle
declaration block. By convention, their names (minus their suffixes) should match.
For example, if Silk didn't provide its own button widget, here's how you would start to define your own:
Notice two points about our interface declaration:
- It is marked
sealed
. This is technically not necessary to do, but we recommend it as a way to express your intention that no one else is ever supposed to subclass it further. - The interface is empty. It is just a marker interface, useful only in enforcing typing for variants. This is discussed more in the next section.
Component variants
The power of component styles is they can generate component variants, using the addVariant
method:
The recommended naming convention for variants is to take their associated style and use its name as a suffix plus the word "Variant", e.g. ButtonStyle
→ OutlinedButtonVariant
and TextStyle
→ EmphasizedTextVariant
.
Like any CssStyle
, your CssStyleVariant
must be public. This is for the same reason: because code gets generated inside a main.kt
file by the Kobweb Gradle plugin, and that code needs to be able to access your variant in order to register it.
You can technically make a variant private if you add a bit of boilerplate to handle the registration yourself:
However, you are encouraged to keep your variants public and let the Kobweb Gradle plugin handle everything for you.
The idea behind component variants is that they give the widget author power to define a base style along with one or more common tweaks that users might want to apply on top of it. (And even if a widget author doesn't provide any variants for the style, any user can always define their own in their own codebase.)
Let's revisit the button style example, bringing everything together.
When used with a component style, the toModifier()
method optionally takes a variant parameter. When a variant is passed in, both styles will be applied -- the base style followed by the variant style.
For example, ButtonStyle.toModifier(OutlinedButtonVariant)
applies the main button style first followed by some additional outline styling.
You can annotate style variants with the @CssName
annotation, exactly like you can with CssStyle
. Using a leading dash will automatically prepend the base style name. For example:
addVariantBase
Like CssStyle.base
, variants that don't need to support additional selectors can use addVariantBase
instead to slightly simplify their declaration:
Silk widget conventions
Silk always uses component styles when defining its widgets. The full pattern looks like this (which you can imitate in your own project if you define your own widgets):
In other words:
- we define a composable widget method.
- it takes a
Modifier
as the first parameter that takes a default value. - this is followed by a
CssStyleVariant
parameter (typed to your specificComponentKind
implementation). - inside your widget, we apply the modifiers in order of: base style, then passed in variant, then passed in modifier.
- the last parameter is a
@Composable
content lambda parameter (unless this widget doesn't support custom content).
A caller can call a widget one of several ways:
Animations
In CSS, animations work by letting you define keyframes in a stylesheet which you then reference, by name, in an animation style. You can read more about them on the Mozilla docs site.
For example, here's the CSS for an animation of a sliding rectangle (from this tutorial):
Kobweb lets you define your keyframes in code by using a Keyframes
block:
When you declare a Keyframes
animation, it must be public. This is because code gets generated inside a main.kt
file that needs to be able to access and register it.
You can then use the toAnimation
method to convert your collection of keyframes into an animation that uses them, which you can pass into the Modifier.animation
modifier.
The name of the keyframes block is automatically derived from the property name (here, ShiftRightKeyframes
is converted into "shift-right"
).
ElementRefScope
and raw HTML elements
Occasionally, you may need access to the raw element backing the Silk widget you've just created. All Silk widgets provide an optional ref
parameter which takes a listener that provides this information.
All ref
callbacks will receive an org.w3c.dom.Element
subclass. You can check out the Element class (and its often more relevant HTMLElement inheritor) to see the methods and properties that are available on it.
Raw HTML elements expose a lot of functionality not available through the higher-level Compose HTML APIs.
ref
For a trivial but common example, we can use the raw element to capture focus:
The ref { ... }
method can actually take one or more optional keys of any value. If any of these keys change on a subsequent recomposition, the callback will be rerun:
Finally, here is a pattern you can use to extract a raw backing element which has some role to play during composition:
Extracting a raw element as above will cause a composition to take two passes -- the first one where the content of your widget will be empty, and a second where it will be populated -- but in general this should be invisible to the user.
disposableRef
If you need to know both when the element enters and exits the DOM, you can use disposableRef
instead. With disposableRef
, the very last line in your block must be a call to onDispose
:
The disposableRef
method can also take keys that rerun the listener if any of them change. The onDispose
callback will also be triggered in that case.
refScope
And, finally, you may want to have multiple listeners that are recreated independently of one another based on different keys. You can use refScope
as a way to combine two or more ref
and/or disposableRef
calls in any combination:
Compose HTML refs
You may occasionally want the backing element of a normal Compose HTML widget, such as a Div
or Span
. However, these widgets don't have a ref
callback, as that's a convenience feature provided by Silk.
You still have a few options in this case.
The official way to retrieve a reference is by using a ref
block inside an attrs
block. This version of ref
is actually more similar to Silk's disposableRef
concept than its ref
one, as it requires an onDispose
block:
The above snippet was adapted from the official tutorials.
Unlike Silk's version of ref
, Compose HTML's version does not accept keys. If you need this behavior and if the Compose HTML widget accepts a content block (many of them do), you can call Silk's registerRefScope
method directly within it:
Style variables
Kobweb supports CSS variables (also called CSS custom properties), which is a feature where you can store and retrieve property values from variables declared within your CSS styles. It does this through a class called StyleVariable
.
You can find official documentation for CSS custom properties here.
Using style variables is fairly simple. You first declare one without a value (but lock it down to a type) and later you can initialize it within a style using Modifier.setVariable(...)
:
Once a variable is set on a parent element, it can be queried by that element or any of its children.
Compose HTML provides a CSSLengthValue
, which represents concrete values like 10.px
or 5.cssRem
. However, Kobweb provides a CSSLengthNumericValue
type which represents the concept more generally, e.g. as the result of intermediate calculations. There are CSS*NumericValue
types provided for all relevant units, and it is recommended to use them when declaring style variables as they more naturally support being used in calculations.
We discuss CSSNumericValue
in more detail later ( CSSNumericValue type-aliases).
You can later query variables using the value()
method to extract their current value:
You can also provide a fallback value, which, if present, would be used in the case that a variable hadn't already been set previously:
You can even provide a default fallback value when first declaring the variable! (This is something we support in Kobweb even though it's not part of the CSS spec.)
The following code example shows when different fallback scopes take effect:
In the above example in the DialogStyle300
style, we set a variable and query it in the same modifier, which we did purely for demonstration purposes. In practice, you would never do this for any reason I can think of -- instead, the variable would have been set separately elsewhere, e.g. in an inline style or on a parent container.
To demonstrate these concepts all together, below we declare a background color variable, create a root container scope which sets it, a child style that uses it, and, finally, a child style variant that overrides it:
The following code brings the above styles together (and in some cases uses inline styles to override the background color further):
The above renders the following output:
Set values programmatically
You can also set CSS variables directly from code if you have access to the backing HTML element.
Below, we use the ref
callback to get the backing element for a fullscreen Box
and then use a Button
to set it to a random color from the colors of the rainbow:
The above results in the following UI:
Prefer pure Kotlin
Most of the time, you can actually get away with not using CSS Variables! Your Kotlin code is often a more natural place to describe dynamic behavior than HTML / CSS is.
Let's revisit the "colored squares" example from above. Note it's much easier to read if we don't try to use variables at all.
And the "rainbow background" example is similarly easier to read by using Kotlin variables (i.e. var someValue by remember { mutableStateOf(...) }
) instead of CSS variables:
Even though you should rarely need CSS variables, there may be occasions where they can be a useful tool in your toolbox. The above examples were artificial scenarios used as a way to show off CSS variables in relatively isolated environments. But here are some situations that might benefit from CSS variables:
- You have a site which allows users to choose from a list of several themes (e.g. primary and secondary colors). It would be trivial enough to add CSS variables for
themePrimary
andthemeSecondary
(applied at the site's root) which you can then reference throughout your styles. - You need more control for colors in your theming than can be provided for by the simple light / dark color mode. For example, Wordle has light / dark + normal / high contrast modes.
- You want to create a widget which dynamically changes its behavior based on the context it is added within. For example, maybe your site has a dark area and a light area, and the widget should use white outlines in the dark area and black outlines in the light. This can be accomplished by exposing an outline color variable, which each area of your site is responsible for setting.
- You want to allow the user to tweak values within a pseudo-class selector (e.g. hover, focus, active) for some widget (e.g. color or border size), which is much easier to do using variables than listening to events and setting inline styles.
- You have a widget that you ended up creating a bunch of variants for, but instead you realize you could replace them all with one or two CSS variables.
When in doubt, lean on Kotlin for handling dynamic behavior, and occasionally consider using style variables if you feel doing so would clean up the code.
Calc
StyleVariable
s work in a subtle way that is usually fine until it isn't -- which is often when you try to intercept and modify their values instead of just passing them around as is.
Specifically, code like this (multiplying a style variable value by 2) would compile but fail to work at runtime:
To see what the problem is, let's first take a step back. The following code:
generates the following CSS:
However, MyOpacityVar
acts like a Number
in our code! How does something that effectively has a type of Number
generate text output like var(--my-opacity)
?
This is accomplished through the use of Kotlin/JS's unsafeCast
, where you can tell the compiler to treat a value as a different type than it actually is. In this case, MyOpacityVar.value()
returns some object which the Kotlin compiler treats like a Number
for compilation purposes, but it is really some class instance whose toString()
evaluates to var(--my-opacity)
.
Therefore, Modifier.opacity(MyOpacityVar.value())
works seemingly like magic! However, if you try to do some arithmetic, like MyOpacityVar.value().toDouble() * 0.5
, the compiler might be happy, but things will break silently at runtime, when the JS engine is asked to do math on something that's not really a number.
In CSS, doing math with variables is accomplished by using calc
blocks, so Kobweb offers its own calc
method to mirror this. When dealing with raw numerical values, you must wrap them in num
so we can escape the raw type system which was causing runtime confusion above:
At this point, you can write code like this:
It's a little hard to remember to wrap raw values in num
, but you will get compile errors if you do it wrong.
Working with variables representing length values don't require calc blocks because Compose HTML supports mathematical operations on such numeric unit types:
However, a calc block could still be useful if you were starting with a raw number that you wanted to convert to a size:
Font Awesome
Kobweb provides the silk-icons-fa
artifact which you can use in your project if you want access to all the free Font Awesome (v6) icons.
Using it is easy! Search the Font Awesome gallery, choose an icon, and then call it using the associated Font Awesome icon composable.
For example, if I wanted to add the Kobweb-themed spider icon, I could call this in my Kobweb code:
That's it!
Some icons have a choice between solid and outline versions, such as "Square" (outline and filled). In that case, the default choice will be an outline mode, but you can pass in a style enum to control this:
All Font Awesome composables accept a modifier parameter, so you can tweak it further:
When you create a project using our app
template, Font Awesome icons are included.
Material Design Icons
Kobweb provides the silk-icons-mdi
artifact which you can use in your project if you want access to all the free Material Design icons.
Using it is easy! Search the Material Icons gallery, choose an icon, and then call it using the associated Material Design Icon composable.
For example, let's say after a search I found and wanted to use their bug report icon, I could call this in my Kobweb code by converting the name to camel case:
That's it!
Most material design icons support multiple styles: outlined, filled, rounded, sharp, and two-tone. Check the gallery search link above to verify what styles are supported by your icon. You can identify the one you want to use by passing it into the method's style
parameter:
All Material Design Icon composables accept a modifier parameter, so you can tweak it further:
The Silk stylesheet
The default styles provided by browsers for many HTML elements rarely fit most site designs, and it's likely you'll want to tweak at least some of them. A very common example of this is the default web font, which if left as is will make your site look a bit archaic.
Most traditional sites overwrite styles by creating a CSS stylesheet and then linking to it in their HTML. However, if you are using Silk in your Kobweb application, you can use an approach very similar to CssStyle
but for general HTML elements.
To do this, create an @InitSilk
method. The context parameter includes a stylesheet
property that represents the CSS stylesheet for your site, providing a Silk-idiomatic API for adding CSS rules to it.
Below is a simple example that sets the whole site to more aesthetically pleasing fonts than the browser defaults, one for regular text and one for code:
The registerStyleBase
method is commonly used for registering styles with minimal code, but you can also use registerStyle
, especially if you want to add some support for one or more pseudo-classes ( e.g. hover
, focus
, active
):
Globally changing Silk widget styles
As mentioned earlier, Silk widgets all use component styling ( Component styles) to power their look and feel.
Normally, if you want to tweak a style in select locations within your site, you just create a variant from that style:
But what if you want to globally change the look and feel of a widget across your entire site?
You could of course create your own composable which wraps some underlying composable with its own new style, e.g. MyButton
which defines its own MyButtonStyle
that internally delegates to Button
. However, you'd have to be careful to make sure all new developers who add code to your site know to use MyButton
instead of Button
directly.
Silk provides another way, allowing you to modify any of its declared styles and/or variants in place.
You can do this via an @InitSilk
method. The context parameter provides the theme
property, which exposes the following family of methods allowing you to rewrite all styles and variants:
Technically, you can use these methods with your own site's declared styles and variants as well, but there should be no reason to do so since you can just go to the source and change those values directly. However, this can still be useful if you're using a third-party Kobweb library that provides its own styles and/or variants.
Use the replace
versions if you want to define a whole new set of CSS rules from scratch, or use the modify
versions to layer additional changes on top of what's already there.
Using replace
on some of the more complex Silk styles can be tricky, and you may want to familiarize yourself with the details of how those widgets are implemented before attempting to do so. Additionally, once you replace a style in your site, you will be opting-out of any future improvements to that style that may be made in future versions of Silk.
Here's a real example taken from a site that always wants its horizontal dividers to fill max width. It uses the modify
method (and not the replace
method), which is generally recommended as it is less likely to break in the future: