Styling

In any kind of web development, you probably want your site to look good, and that will involve working with a language called CSS, short for Cascading Style Sheets. It's well beyond the scope of these docs to explain CSS, so we'll leave that to this fantastic introduction if you're new to it.

Right now, Perseus and Sycamore have limited inbuilt styling capabilities, and you're best off using either traditional styling (i.e. set a class header-button and style that in header.css, etc.), or a styling library like Tailwind, which provides utility classes like rounded and dark:shadow-lg.

There is currently work ongoing on a styling framework for Sycamore/Perseus called Jacaranda, which will support fully typed styling!

Full-page layouts

A lot of websites these days are based on full-page layouts, which are when the entire page is taken up, usually by a header, some main content, and a footer. Getting this to work well, however, if unreasonably complicated in many cases. So, here's an example of exactly what CSS you need to get it working:

/* This removes the margins inserted by default around pages */
* {
    margin: 0;
}

/* This makes all the elements that wrap our code take up the whole page, so that we can put things at the bottom.
 * Without this, the footer would be just beneath the content if the content doesn't fill the whole page (try disabling this).
*/
html, body, #root {
    height: 100%;
}
/* This makes the `<div>` that wraps our whole app use CSS Grid to display three sections: the header, content, and footer. */
#root {
    display: grid;
    grid-template-columns: 1fr;
    /* The header will be automatically sized, the footer will be as small as possible, and the content will take up the rest of the space in the middle */
    grid-template-rows: auto 1fr min-content;
    grid-template-areas:
        'header'
        'main'
        'footer';
}
header {
    /* Put this in the right place in the grid */
    grid-area: header;
    /* Make this float over the content so it persists as we scroll */
    position: fixed;
    top: 0;
    z-index: 99;
    /* Make this span the whole page */
    width: 100%;
}
main {
    /* Put this in the right place in the grid */
    grid-area: main;
    /* The header is positioned 'floating' over the content, so we have to make sure this doesn't go behind the header, or it would be invisible.
     * You may need to adjust this based on screen size, depending on how the header expands.
    */
    margin-top: 5rem;
}
footer {
    /* Put this in the right place in the grid */
    grid-area: footer;
}

The comments in this file should make it fairly self-explanatory, but what it does is creates a sticky header that maintains its spot when the user scrolls, while the footer will always be at the bottom of the page (but is not sticky when the content overflows the page). You can combine this with a layout component like this to get an easy way of creating full-page layouts for your sites:

use sycamore::prelude::*;

// NOTE: None of the code in this file is Perseus-specific! This could easily be
// applied to any Sycamore app.

#[component]
pub fn Layout<'a, G: Html>(
    cx: Scope<'a>,
    LayoutProps { title, children }: LayoutProps<'a, G>,
) -> View<G> {
    let children = children.call(cx);

    view! { cx,
        // These elements are styled with bright colors for demonstration purposes
        header(style = "background-color: red; color: white; padding: 1rem") {
            p { (title.to_string()) }
        }
        main(style = "padding: 1rem") {
            (children)
        }
        footer(style = "background-color: black; color: white; padding: 1rem") {
            p { "Hey there, I'm a footer!" }
        }
    }
}

#[derive(Prop)]
pub struct LayoutProps<'a, G: Html> {
    /// The title of the page, which will be displayed in the header.
    pub title: &'a str,
    /// The content to put inside the layout.
    pub children: Children<'a, G>,
}

You can then use this like so:

use crate::components::layout::Layout;
use perseus::prelude::*;
use sycamore::prelude::*;

fn index_page<G: Html>(cx: Scope) -> View<G> {
    view! { cx,
        Layout(title = "Index") {
            // Anything we put in here will be rendered inside the `<main>` block of the layout
            p { "Hello World!" }
            br {}
            a(href = "long") { "Long page" }
        }
    }
}

#[engine_only_fn]
fn head(cx: Scope) -> View<SsrNode> {
    view! { cx,
        title { "Index Page" }
    }
}

pub fn get_template<G: Html>() -> Template<G> {
    Template::build("index").view(index_page).head(head).build()
}

For more about full page layouts, see this example.