Scoped Styles

In this chapter, we will look at styling the classroom application we created previously. Normally in html and css you would start adding classes to the html elements, and then use those classes in index.css file to select and style each element. However, Live Elements has a faster way to do these things, using scoped styles.

Components and Scoped Styles

Scoped styles basically mean styles that are limited to a scope. In case of Live Elements, that scope is a component. So in Live Elements, a component can have styles that are applied only to that component, and they won't interfere with styles from other components accross the application. Creating a component with scoped styles assigned will also apply the styles for that component. There are already a few of these components implemented in the live-web-view package.

PrimaryButton is a styled button found in live-web-view package, in live-web-view.button module. We can try it out in the class room example:

import live-web.dom
import live-web-view.button // Import the module
import live-elements-web-server.view
import live-elements-web-server.style

component ClassroomData{
    string label: ''
    number attendeeCount: 0
}

component Classroom < Li{
    id: classRoom
    ClassroomData data: ClassroomData{}

    T{ text: 'Classroom ' + classRoom.data.label }
    PrimaryButton{ // Switch from Button to PrimaryButton
        on click: () => { 
            if ( classRoom.data.attendeeCount > 0 )
                classRoom.data.attendeeCount--
        }
        T`-`
    }
    Span{ T{ text: classRoom.data.attendeeCount } }
    PrimaryButton{ // Switch from Button to PrimaryButton
        on click: () => { classRoom.data.attendeeCount++ }
        T`+`
    }
}

component Index < PageView{
    static any[] use = [ ScopedStyle{ src: './index.css'} ]
    
    Ul{
        Classroom{ data: ClassroomData{ label: 'A' } }
        Classroom{ data: ClassroomData{ label: 'B' } }
        Classroom{ data: ClassroomData{ label: 'C' } }
    }
}

The example works like previously, but the button looks the same. Even though we are using PrimaryButtonthe server running our example isn't aware of that, and so it cannot populate the stylesheet accordingly. This is where the use property comes along. Besides letting the application know we are using the ScopedStyle{ src: './index.css' }, we will also need to let it know we are using the PrimaryButton:

static any[] use = [
    PrimaryButton,
    ScopedStyle{ src: './index.css'}
]

index.css should also be added at the end in order to be able to override any styles from the PrimaryButton.

So the entire implementation will be:

import live-web.dom
import live-web-view.button // Import the module
import live-elements-web-server.view
import live-elements-web-server.style

component ClassroomData{
    string label: ''
    number attendeeCount: 0
}

component Classroom < Li{
    id: classRoom
    ClassroomData data: ClassroomData{}

    T{ text: 'Classroom ' + classRoom.data.label }
    PrimaryButton{ // Switch from Button to PrimaryButton
        on click: () => { 
            if ( classRoom.data.attendeeCount > 0 )
                classRoom.data.attendeeCount--
        }
        T`-`
    }
    Span{ T{ text: classRoom.data.attendeeCount } }
    PrimaryButton{ // Switch from Button to PrimaryButton
        on click: () => { classRoom.data.attendeeCount++ }
        T`+`
    }
}

component Index < PageView{
    static any[] use = [
        PrimaryButton, // Use PrimaryButton
        ScopedStyle{ src: './index.css'}
    ]
    
    Ul{
        Classroom{ data: ClassroomData{ label: 'A' } }
        Classroom{ data: ClassroomData{ label: 'B' } }
        Classroom{ data: ClassroomData{ label: 'C' } }
    }
}

Now we can see the actual change take effect. Let's add some more components.

CenterLayout and UlV

CenterLayout is a component that will center all it's children. It's available in live-web-view.layout:

import live-web.dom
import live-web-view.button
import live-web-view.layout // Import live-web-view.layout
import live-elements-web-server.view
import live-elements-web-server.style

component ClassroomData{
    string label: ''
    number attendeeCount: 0
}

component Classroom < Li{
    id: classRoom
    ClassroomData data: ClassroomData{}

    T{ text: 'Classroom ' + classRoom.data.label }
    PrimaryButton{
        on click: () => { 
            if ( classRoom.data.attendeeCount > 0 )
                classRoom.data.attendeeCount--
        }
        T`-`
    }
    Span{ T{ text: classRoom.data.attendeeCount } }
    PrimaryButton{
        on click: () => { classRoom.data.attendeeCount++ }
        T`+`
    }
}

component Index < PageView{
    static any[] use = [
        PrimaryButton,
        CenterLayout, // Use CenterLayout
        ScopedStyle{ src: './index.css'}
    ]

    CenterLayout{ // Move Ul in CenterLayout
        Ul{
            Classroom{ data: ClassroomData{ label: 'A' } }
            Classroom{ data: ClassroomData{ label: 'B' } }
            Classroom{ data: ClassroomData{ label: 'C' } }
        }
    }
}

UlV is a styled Ul. The letter 'V' comes from vertical, so it's a vertical list, there's also an UlH for horizontal lists:

import live-web.dom
import live-web-view.button
import live-web-view.layout
import live-web-view.content
import live-elements-web-server.view
import live-elements-web-server.style

component ClassroomData{
    string label: ''
    number attendeeCount: 0
}

component Classroom < Li{
    id: classRoom
    ClassroomData data: ClassroomData{}

    T{ text: 'Classroom ' + classRoom.data.label }
    PrimaryButton{
        on click: () => { 
            if ( classRoom.data.attendeeCount > 0 )
                classRoom.data.attendeeCount--
        }
        T`-`
    }
    Span{ T{ text: classRoom.data.attendeeCount } }
    PrimaryButton{
        on click: () => { classRoom.data.attendeeCount++ }
        T`+`
    }
}

component Index < PageView{
    static any[] use = [
        PrimaryButton,
        CenterLayout,
        UlV,
        ScopedStyle{ src: './index.css'}
    ]

    CenterLayout{
        UlV{
            Classroom{ data: ClassroomData{ label: 'A' } }
            Classroom{ data: ClassroomData{ label: 'B' } }
            Classroom{ data: ClassroomData{ label: 'C' } }
        }
    }
    
}

Scope Style Selectors

The components we are using are now styled, but we should still do some updates so the design blends in better throughout the page. We should modify the margin for the PrimaryButton, so the text has more room around it. One way to do that is to add a class to the primary button, something like primary-button, and then select it in css with .primary-button { ... }. Another way is to use a specific selector live elements provides in css for scoped style components:

&PrimaryButton{ margin: 0 5px; }

&PrimaryButton will select all PrimaryButton components in our webpage. This type of selector, '&ComponentName', can be used for any component that's being used by the component using the current stylesheet. Specifically, the Index component in Index.lv uses PrimaryButton, CenterLayout, UlV, and it also uses index.css:

static any[] use = [
    PrimaryButton,
    CenterLayout,
    UlV,
    ScopedStyle{ src: './index.css'}
]

This means index.css has access to all these 3 components, plus any nested component inside those components. For example, if UlV uses IconButton, then &IconButton selector would be available in index.css as a selector.

Let's wrap up the styling in index.css:

&PrimaryButton{ margin: 0 5px; }
&UlV{ font-size: 1.3rem; }
&UlV li span{ font-weight: bold; }

There are 2 more things to note on scoped styles, one is that most components with scoped styles reserve the classes property internally, and expose another property extraClasses instead. For example, to add an html class to PrimaryButton we should use the extraClasses property:

PrimaryButton{
    extraClasses: ['green-button']
}

The second is when using additional selectors beside the component name, we need to append the & symbol after the component name:

&PrimaryButton&.green-button { color: green; }

Creating a scoped component

Since the playground currently doesn't support creating new files, and since a scoped style component needs a separate style file, we won't be able to create a component with scoped styles here, but the scoped styles tutorial will guide you through creating a component with scoped styles in a development environment.

We will overview how to create such a component instead. We can look at an already implemented component. Let's check out the source for CenterLayout:

CenterLayout.lv:

import live-web.dom
import live-web
import live-elements-web-server.style

component CenterLayout < Div{
    static any[] use = [ 
        ScopedStyle{ src: './style/centerlayout.css' process: '../style/CSSProcessor.lv' }, 
        ScopedStyle{ src: '../style/global.css' process: '../style/CSSProcessor.lv' } 
    ]
    
    Array extraClasses: []
    classes: this.extraClasses.concat([ ScopedStyle.className(CenterLayout) ])
}

and centerlayout.css:

&CenterLayout {
    @apply flex flex-col justify-center items-center w-full h-full;
}

In CenterLayout.lv we can see CenterLayout component is extending the Div(component CenterLayout < Div), so it will generate a <div> tag in html. Next, it's using the centerlayout.css file and a global.css file. There's also a process property for both styles assigned to CSSProcessor.lv. The process property can specify a component that will process the file before it is delivered to the client. This particular CSSProcessor is using tailwind css to do the processing, it's why the css file is using the @apply directive from tailwind css:

@apply flex flex-col justify-center items-center w-full h-full;

global.css is simply to initialize the global tailwind styles, like @tailwind base; and @tailwind components; since we are using tailwind css processor. Otherwise, the process property allows you to specify other css processors, or even create your own custom css processor implementation if you want.

We haven't really mentioned how are styles scoped to a component. Basically, in every style file used by CenterLayout, any selector that doesn't start with &CenterLayout will have the &CenterLayout selector prepended automatically:

&CenterLayout {
    @apply flex flex-col justify-center items-center w-full h-full;
}

/*  
  This will automatically become '&CenterLayout span' since it's part of CenterLayout
*/
span{ 
    font-weight: bold;
}

Conclusion & Next Step

In this chapter, we've looked at:

  • Components with scoped styles: PrimaryButton, CenterLayout, UlV.
  • The use property and how it links to components with scoped styles and stylesheets.
  • The & selector in css
  • The process property and how custom processors can be used for stylesheets.

The next chapter will go through a list of examples with scoped style components available in the live-web-view package.