Language Tutorial
This page contains a tutorial covering the essential parts of the hop language.
All of the code examples on this page are interactive. You can click the Open in playground buttons on the bottom of the code blocks to run the code in your browser.
Let's get started!
Views
Views are the entry points for a hop program.
Every view in a program compiles to a function that is callable from Rust. The simplest possible program is one that declares a single view.
Any describable type in hop can be used as the type of a view parameter, allowing you to pass data from the host program in a fully type-safe manner.
In Rust, we would render the Greeting view like this:
mod hop;
fn main() {
let view = hop::Greeting {
name: "World".to_string(),
};
println!("{}", view.render());
}When compiling the view, hop will add the necessary boilerplate to turn your view into a complete HTML document.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1" name="viewport">
</head>
<body>
<div>Hello World!</div>
</body>
</html>Let's move on to the next topic, components!
Components
Components are the building blocks of a hop program.
Components make it possible to construct composable user interfaces. This concept might be familiar to you if you've ever tried React, Vue, Blazor, Flutter or similar frameworks.
The syntax for declaring a component looks a lot like the syntax for declaring a view. However, unlike views, components can be invoked in views and in other components.
// The AlbumCard component renders an album with its cover art.
component AlbumCard(
cover: String,
title: String,
author: String,
year: Int,
) {
<div>
<img src={cover}>
<h2>
{title}
</h2>
<div>
Released {year.to_string()} by {author}
</div>
</div>
}
// In the Main view we render three instances
// of the AlbumCard component.
view Main {
<div>
<AlbumCard
cover="/tutorial/2012_2017.jpg"
title="2012-2017"
author="Against All Logic"
year={2018}
/>
<AlbumCard
cover="/tutorial/inner_song.jpg"
title="Inner Song"
author="Kelly Lee Owens"
year={2020}
/>
<AlbumCard
cover="/tutorial/persona.jpg"
title="Persona"
author="Rival Consoles"
year={2018}
/>
</div>
}Open in playgroundStyling components
To style our components we'll use the CSS framework Tailwind.
hop has built-in support for Tailwind so we can get started by just adding Tailwind classes directly to the class attribute of HTML elements in the code.
If you've never used Tailwind before we recommend skimming their introduction before continuing.
Now, let's add some styling.
component AlbumCard(
cover: String,
title: String,
author: String,
year: Int,
) {
<div class={
join!(
"flex",
"border",
"rounded-lg",
"overflow-hidden",
"sm:flex-col",
)
}>
<img src={cover} class="h-24 sm:h-auto">
<div class={
join!(
"flex",
"flex-col",
"gap-1",
"p-5",
)
}>
<h2 class="text-lg sm:text-xl">
{title}
</h2>
<span class="text-zinc-600">
Released {year.to_string()} by {author}
</span>
</div>
</div>
}
view Main {
<div class={
join!(
"max-w-5xl",
"mx-auto",
"p-8",
"flex",
"flex-col",
"gap-4",
"sm:flex-row",
)
}>
<AlbumCard
cover="/tutorial/2012_2017.jpg"
title="2012-2017"
author="Against All Logic"
year={2018}
/>
<AlbumCard
cover="/tutorial/inner_song.jpg"
title="Inner Song"
author="Kelly Lee Owens"
year={2020}
/>
<AlbumCard
cover="/tutorial/persona.jpg"
title="Persona"
author="Rival Consoles"
year={2018}
/>
</div>
}Open in playgroundIf you've seen Tailwind before the additions above will probably look mostly familiar to you.
One thing that might stick out is the use of the join! macro inside the Main view containing the CSS classes broken up into separate strings.
The join! macro concatenates strings, adding a space between each string while filtering out empty strings.
In hop, it is idiomatic to use this macro to break up class strings. This aids readability and allows for better diffs in version control systems.
The join! macro is evaluated at compile time and adds no runtime overhead so it is recommended to use it generously. The hop formatter will also break up and format the strings for you automatically if you write join!("foo bar baz").
Building flexible components
Designing flexible components that are easy to reuse across different contexts of a user interface is hard. Just like in all software design, spending some time thinking about how components will be used before establishing their API usually pays off in the long run.
Modules and visibility
One of the most fundamental tools in software design is encapsulation, the ability to decide whether code and data should be public or private in a given context.
In hop, any declaration (such as a component declaration or a type declaration) can either be private to the file they are declared in, or public to the whole program.
By default, all declarations are private and can only be used in the file they are declared in. To make a declaration public, add the keyword pub to the start of a declaration. When a declaration is made public, it can be imported from another file using the import keyword.
import icons::DownloadIcon
import icons::GhostIcon
import icons::HeartIcon
view Main {
<div class={
join!(
"flex",
"gap-4",
"justify-center",
"items-center",
"py-16",
)
}>
<DownloadIcon/>
<GhostIcon/>
<HeartIcon/>
</div>
}Open in playgroundpub component DownloadIcon {
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 15V3">
</path>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4">
</path>
<path d="m7 10 5 5 5-5">
</path>
</svg>
}
pub component GhostIcon {
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-ghost-icon lucide-ghost"
>
<path d="M9 10h.01">
</path>
<path d="M15 10h.01">
</path>
<path d="M12 2a8 8 0 0 0-8 8v12l3-3 2.5 2.5L12 19l2.5 2.5L17 19l3 3V10a8 8 0 0 0-8-8z">
</path>
</svg>
}
pub component HeartIcon {
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-heart-icon lucide-heart"
>
<path d="M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5">
</path>
</svg>
}Open in playgroundThe ability to make components private makes it possible to break up complex components into sub-components without changing the public interface that a file exports.
Now, let's look in to component composition.
Slots and composition
To make composition of components possible, hop allows for components to declare a slot which allows the invoker to render markup inside the slot. The parameter name of a slot must be declared as slot (lowercase) and its type must be Slot (capitalized).
import icons::DownloadIcon
component Button(slot: Slot) {
<button class={
join!(
"h-9",
"px-4",
"border",
"inline-flex",
"items-center",
"justify-center",
"gap-2",
"rounded-full",
"text-sm",
"font-medium",
"shrink-0",
)
}>
{slot}
</button>
}
view Main {
<div class={
join!(
"flex",
"justify-center",
"py-16",
)
}>
<!--
The content inside <Button> is evaluated
in this variable scope, but rendered in the
slot of the Button
-->
<Button>
Download
<DownloadIcon/>
</Button>
</div>
}Open in playgroundpub component DownloadIcon {
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path d="M12 15V3">
</path>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4">
</path>
<path d="m7 10 5 5 5-5">
</path>
</svg>
}Open in playgroundThe fact that a component can have at most one slot might seem like a serious limitation, but we can leverage the slot as a building block to create complex structures. We will look into that now.
Compound component pattern
The compound component pattern is, in some sense, a way to create a component structure that can have multiple slots. In this pattern we create a hierarchy of components that should be used together.
It is recommended to let all components that are part of the same hierarchy share a prefix in their name. If it makes sense, let the name of the outermost component be just the prefix.
import alert::Alert
import alert::AlertTitle
import alert::AlertDescription
import icons::CircleCheckIcon
view Main {
<div class="p-6">
<Alert>
<CircleCheckIcon/>
<AlertTitle>
Upload successful
</AlertTitle>
<AlertDescription>
Your package has successfully been uploaded to the repository.
</AlertDescription>
</Alert>
</div>
}Open in playgroundpub component Alert(
slot: Slot,
class: String = "",
) {
<div
data-slot="alert"
role="alert"
class={
join!(
"grid",
"w-full",
"gap-0.5",
"rounded-lg",
"border",
"border-neutral-300",
"px-2.5",
"py-2",
"text-left",
"text-sm",
"has-[>svg]:grid-cols-[auto_1fr]",
"has-[>svg]:gap-x-2",
"*:[svg]:row-span-2",
"*:[svg]:translate-y-0.5",
"*:[svg]:text-current",
"*:[svg:not([class*='size-'])]:size-4",
"bg-white",
class,
)
}
>
{slot}
</div>
}
pub component AlertTitle(
slot: Slot,
class: String = "",
) {
<div
data-slot="alert-title"
class={
join!(
"font-medium",
class,
)
}
>
{slot}
</div>
}
pub component AlertDescription(
slot: Slot,
class: String = "",
) {
<div
data-slot="alert-description"
class={
join!(
"text-sm",
"text-balance",
"text-neutral-600",
class,
)
}
>
{slot}
</div>
}Open in playgroundpub component CircleCheckIcon(class: String = "") {
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class={class}
>
<circle cx="12" cy="12" r="10">
</circle>
<path d="m9 12 2 2 4-4">
</path>
</svg>
}Open in playgroundComponent variant pattern
It is often useful to be able to define different variants of a component that share a common base style.
In hop, the idiomatic way to achieve this is to declare the variants as an enum, and then use match expressions to handle each case. The match expression in hop is similar to the one in Rust. It is always exhaustive, meaning that the compiler will tell you if you've forgot to handle a case.
import icons::DownloadIcon
enum ButtonSize {
Sm,
Default,
Lg,
}
component Button(
slot: Slot,
size: ButtonSize = ButtonSize::Default,
) {
<button class={
join!(
"border",
"inline-flex",
"items-center",
"justify-center",
"gap-2",
"rounded-full",
"text-sm",
"font-medium",
"shrink-0",
match size {
ButtonSize::Sm => join!(
"h-8",
"px-3",
"gap-1.5",
),
ButtonSize::Default => join!(
"h-9",
"px-4",
),
ButtonSize::Lg => join!(
"h-10",
"px-6",
),
},
)
}>
{slot}
</button>
}
view Main {
<div class={
join!(
"flex",
"gap-4",
"justify-center",
"items-center",
"py-16",
)
}>
<!-- Render small button -->
<Button size={ButtonSize::Sm}>
Download
<DownloadIcon/>
</Button>
<!-- Render default button -->
<Button>
Download
<DownloadIcon/>
</Button>
<!-- Render large button -->
<Button size={ButtonSize::Lg}>
Download
<DownloadIcon/>
</Button>
</div>
}Open in playgroundpub component DownloadIcon {
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path d="M12 15V3">
</path>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4">
</path>
<path d="m7 10 5 5 5-5">
</path>
</svg>
}Open in playgroundAppending and overriding CSS classes
It can be useful to be able to override or append to the default styles of a component. To achieve this we can allow for extra classes to be sent in via a parameter and added to the arguments of join!.
It is idiomatic to call the extra parameter class so that the attribute for a component mirrors the attribute for an HTML element.
import icons::DownloadIcon
component Button(
slot: Slot,
class: String = "",
) {
<button class={
join!(
"h-9",
"px-4",
"border",
"inline-flex",
"items-center",
"justify-center",
"gap-2",
"rounded-full",
"text-sm",
"font-medium",
"shrink-0",
class,
)
}>
{slot}
</button>
}
view Main {
<div class={
join!(
"flex",
"justify-center",
"py-16",
)
}>
<Button class={
join!(
"hover:scale-110",
"transition-transform",
)
}>
Download
<DownloadIcon/>
</Button>
</div>
}Open in playgroundpub component DownloadIcon {
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path d="M12 15V3">
</path>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4">
</path>
<path d="m7 10 5 5 5-5">
</path>
</svg>
}Open in playgroundNote that we add a default value to the class parameter of the Button component. This is necessary, since otherwise we would get a compile error if we didn't provide a value for the class attribute when rendering Button. Default values can be specified for component parameters, but not for view parameters.
Advanced: Conflict resolution of Tailwind classes
If you are aware of the the intricacies of HTML/CSS you might be worried about conflict resolution of Tailwind classes.
The fact that HTML puts no semantic meaning into the order in which classes appear inside the class attribute has spawned various overhead inducing workarounds for component frameworks such as twMerge in React.
In hop, conflict resolution is handled automatically at compile time. Since hop is aware of the semantics of Tailwind it evaluates the value of class attributes and removes the classes that has overrides. The last CSS class of a conflict wins. Therefore it is important that the overrides appear last inside the join macro or class string, i.e. join!(..., overrides).
Advanced: Tagged element pattern
Sometimes a component needs to be able to be rendered as different HTML elements (sometimes called polymorphic components in React). The typical example of this is a Button component that needs to be rendered as an <a> element in some contexts and as a <button> element in other contexts. In hop, the idiomatic way to achieve this is via the tagged element pattern.
The tagged element pattern works by declaring an enum for each of the elements that the component can render as. The enum variants declare the attributes of each element as fields. A base component then uses the <match> tag to destructure the enum and passes the attributes to the respective HTML element in a type-safe manner.
A component that wraps the base component and constructs the enum variant is then defined for each of the possible HTML elements, making the API more ergonomic for the caller.
import button::ButtonLink
import button::Button
view Main {
<div class={
join!(
"flex",
"gap-4",
"justify-center",
"items-center",
"py-16",
)
}>
<!-- Render as <button> -->
<Button>
Download
</Button>
<!-- Render as <a> -->
<ButtonLink href="#">
Download
</ButtonLink>
</div>
}Open in playgroundenum ButtonElement {
Link {
href: String,
},
Button {
type: String,
},
}
component ButtonBase(
slot: Slot,
el: ButtonElement,
) {
<let {
classes: String = join!(
"h-9",
"px-4",
"border",
"inline-flex",
"items-center",
"justify-center",
"gap-2",
"rounded-full",
"text-sm",
"font-medium",
"shrink-0",
),
}>
<match {el}>
<case {ButtonElement::Link {href}}>
<a href={href} class={classes}>
{slot}
</a>
</case>
<case {ButtonElement::Button {type}}>
<button type={type} class={classes}>
{slot}
</button>
</case>
</match>
</let>
}
pub component ButtonLink(
slot: Slot,
href: String,
) {
<ButtonBase el={ButtonElement::Link {href: href}}>
{slot}
</ButtonBase>
}
pub component Button(
slot: Slot,
type: String = "button",
) {
<ButtonBase el={ButtonElement::Button {type: type}}>
{slot}
</ButtonBase>
}Open in playgroundAsset handling
During compilation hop has the ability to copy assets used in the HTML to an output directory.
To register an asset into the asset pipeline of hop, use the asset! macro.
By default, hop will add a hash to the resulting filename on disk as well as in the url inside the macro.