lustre/stylish

Lustre Stylish

A declarative layout library for Lustre, inspired by elm-ui.

Build responsive, accessible layouts with a type-safe API that automatically generates CSS. No more wrestling with stylesheets!

Quick Start

import lustre/stylish as el
import lustre/stylish/font
import lustre/stylish/background
import lustre/stylish/border

pub fn view() {
  el.el([
    background.color(el.rgb(0.95, 0.95, 0.95)),
    el.padding(20),
    border.rounded(8),
  ], 
    el.column([el.spacing(15)], [
      el.el([font.size(24), font.bold()], el.text("Hello World!")),
      el.paragraph([], [el.text("A beautiful layout with zero CSS.")]),
    ])
  )
}

Core Concepts

Elements are the building blocks. Use el, row, column, or paragraph to create layouts.

Attributes style elements. Import from submodules like font, border, and background for specific styling needs.

Automatic CSS - All styles are generated automatically. You never write CSS!

Available Modules

Total: 130+ functions for building complete UIs

Basic Elements

Rows and Columns

When you want more than one child in an element, you need to be specific about how they will be laid out.

Text Layout

Size

Layout Attributes

Advanced Features

Note: Some helpers are constants (like fill, center_x) and don’t need (), while others are functions (like scrollbars(), clip()) and do need (). Check the documentation for each function to see which is which.

Types

An attribute that can be attached to an Element.

pub type Attribute(msg) =
  @internal Attribute(@internal Aligned, msg)

A color value.

pub type Color =
  @internal Color
pub type Decoration(msg) {
  Decoration(
    attribute: @internal Attribute(@internal Aligned, msg),
  )
}

Constructors

  • Decoration(
      attribute: @internal Attribute(@internal Aligned, msg),
    )
pub type Device {
  Device(class: DeviceClass, orientation: Orientation)
}

Constructors

pub type DeviceClass {
  Phone
  Tablet
  Desktop
  BigDesktop
}

Constructors

  • Phone
  • Tablet
  • Desktop
  • BigDesktop

The basic building block of your layout.

Example

let howdy: Element(msg) = el([], text("Howdy!"))
pub type Element(msg) =
  @internal Element(msg)
pub type ExplainReason {
  Layout
}

Constructors

  • Layout

    Show the layout structure

Configuration for an image element.

pub type ImageConfig {
  ImageConfig(src: String, description: String)
}

Constructors

  • ImageConfig(src: String, description: String)
pub type LayoutOption {
  NoHover
  ForceHover
  FocusStyle(style: @internal FocusStyle)
  NoStaticStyleSheet
}

Constructors

  • NoHover

    Disable all hover styles

  • ForceHover

    Always enable hover styles (useful for mobile)

  • FocusStyle(style: @internal FocusStyle)

    Provide custom focus styling

  • NoStaticStyleSheet

    Don’t embed the static stylesheet (when using multiple layouts)

pub type LayoutOptions {
  LayoutOptions(options: List(LayoutOption))
}

Constructors

A length value for width and height.

pub type Length =
  @internal Length

Configuration for a link element.

pub type LinkConfig(msg) {
  LinkConfig(url: String, label: Element(msg))
}

Constructors

  • LinkConfig(url: String, label: Element(msg))
pub type Orientation {
  Portrait
  Landscape
}

Constructors

  • Portrait
  • Landscape

Values

pub fn above(
  element: Element(msg),
) -> @internal Attribute(@internal Aligned, msg)

Place an element above the current element.

This element will not affect the layout - it’s positioned absolutely.

el([above(text("Tooltip!"))], text("Hover me"))
pub const align_bottom: @internal Attribute(l, m)

Align to the bottom edge.

pub const align_left: @internal Attribute(f, g)

Align to the left edge.

pub const align_right: @internal Attribute(h, i)

Align to the right edge.

pub const align_top: @internal Attribute(j, k)

Align to the top edge.

pub fn alpha(
  opacity: Float,
) -> @internal Attribute(@internal Aligned, msg)

Set the opacity of an element.

Takes a value between 0.0 (fully transparent) and 1.0 (fully opaque). This is semantically equivalent to CSS opacity.

Example

el([alpha(0.5)], text("Half transparent"))
pub fn behind_content(
  element: Element(msg),
) -> @internal Attribute(@internal Aligned, msg)

Place an element behind the current element’s content (lower z-index).

el([behind_content(background_decoration)], foreground_content)
pub fn below(
  element: Element(msg),
) -> @internal Attribute(@internal Aligned, msg)

Place an element below the current element.

el([below(dropdown_menu)], button_content)
pub const center_x: @internal Attribute(b, c)

Center an element horizontally within its container.

Example

el([center_x], text("Centered"))
pub const center_y: @internal Attribute(d, e)

Center an element vertically within its container.

Example

el([center_y], text("Centered"))
pub fn classify_device(window: #(Int, Int)) -> Device

Classify a device based on window dimensions.

Useful for responsive layouts:

  • Phone: width <= 600px
  • Tablet: width <= 1200px
  • Desktop: width <= 1800px
  • BigDesktop: width > 1800px
case classify_device(Device(width: 800, height: 600)) {
  Device(Tablet, Landscape) -> tablet_layout()
  Device(Phone, _) -> mobile_layout()
  _ -> desktop_layout()
}
pub const clip: @internal Attribute(@internal Aligned, msg)

Clip content that overflows on both axes.

Example

el([clip, width(px(100)), height(px(100))], large_image)
pub const clip_x: @internal Attribute(@internal Aligned, msg)

Clip content that overflows horizontally.

Example

el([clip_x, width(px(100))], wide_content)
pub const clip_y: @internal Attribute(@internal Aligned, msg)

Clip content that overflows vertically.

Example

el([clip_y, height(px(100))], tall_content)
pub fn column(
  attributes: List(@internal Attribute(@internal Aligned, msg)),
  children: List(Element(msg)),
) -> Element(msg)

Layout children vertically.

Example

column([spacing(10)], [
  text("First"),
  text("Second"),
  text("Third"),
])
pub fn download(
  attrs: List(@internal Attribute(@internal Aligned, msg)),
  config: LinkConfig(msg),
) -> Element(msg)

A link for downloading a file.

Example

download([], LinkConfig(url: "/files/document.pdf", label: text("Download PDF")))
pub fn el(
  attributes: List(@internal Attribute(@internal Aligned, msg)),
  child: Element(msg),
) -> Element(msg)

Create an element that wraps a single child.

Example

el([padding(20), center_x], text("Centered with padding"))
pub fn explain(
  reason: ExplainReason,
) -> @internal Attribute(@internal Aligned, msg)

Highlight the boundaries of this element for debugging.

Adds a border and background to show the element’s size and position.

el([explain(Layout)], mysterious_element)
pub const fill: Length

Fill the available space.

The available space will be split evenly between elements that have fill.

Example

el([width(fill)], text("Fills available space"))
pub fn fill_portion(portion: Int) -> Length

Fill the available space proportionally.

Example

row([], [
  el([width(fill_portion(2))], text("Takes 2/3")),
  el([width(fill_portion(1))], text("Takes 1/3")),
])
pub fn focused(
  styles: List(Decoration(msg)),
) -> @internal Attribute(@internal Aligned, msg)

Style an element when it has focus.

Note: This is a simplified implementation. For full pseudo-class support, you may need to use CSS directly via html_attribute.

el([focused([Decoration(alpha(1.0))])], input_element)
pub fn height(
  length: Length,
) -> @internal Attribute(@internal Aligned, msg)

Set the height of an element.

Example

el([height(px(100))], text("100px tall"))
el([height(fill)], text("Fills available height"))
pub fn html(lustre_element: element.Element(msg)) -> Element(msg)

Embed raw Lustre HTML elements.

Use this to embed standard HTML elements that don’t need elm-ui styling.

import lustre/element/html

el([], html(html.video([attribute.src("video.mp4")], [])))
pub fn html_attribute(
  attr: attribute.Attribute(msg),
) -> @internal Attribute(@internal Aligned, msg)

Convert a Lustre attribute to an Element attribute.

import lustre/attribute as attr

el([html_attribute(attr.id("my-id"))], content)
pub fn image(
  attrs: List(@internal Attribute(@internal Aligned, msg)),
  config: ImageConfig,
) -> Element(msg)

Display an image.

The description field is used for the alt attribute, providing accessibility. Describe what someone would need to know if they couldn’t see the image.

Example

image([width(px(200))], ImageConfig(
  src: "/images/logo.png",
  description: "Company logo"
))
pub fn in_front(
  element: Element(msg),
) -> @internal Attribute(@internal Aligned, msg)

Place an element in front of the current element (higher z-index).

el([in_front(overlay)], main_content)
pub fn layout(
  attrs: List(@internal Attribute(@internal Aligned, msg)),
  child: Element(msg),
) -> element.Element(msg)

Convert an Element into HTML that can be displayed.

This is your top-level node where you turn Element into Html.

import lustre
import lustre/stylish as el

pub fn main() {
  let app = lustre.element(el.layout([], view()))
  let assert Ok(_) = lustre.start(app, "#app", Nil)
}
pub fn layout_with(
  config: LayoutOptions,
  attrs: List(@internal Attribute(@internal Aligned, msg)),
  child: Element(msg),
) -> element.Element(msg)

Same as layout but you can provide rendering options.

import lustre/stylish as el

el.layout_with(
  el.LayoutOptions(options: [el.NoHover]),
  [],
  view()
)
pub fn link(
  attrs: List(@internal Attribute(@internal Aligned, msg)),
  config: LinkConfig(msg),
) -> Element(msg)

A link element.

Example

link([center_x], LinkConfig(url: "https://example.com", label: text("Visit us")))
pub fn map(f: fn(a) -> msg, element: Element(a)) -> Element(msg)

Transform the messages produced by an Element.

map(ButtonClicked, button_element)
pub fn map_attribute(
  f: fn(a) -> msg,
  attr: @internal Attribute(@internal Aligned, a),
) -> @internal Attribute(@internal Aligned, msg)

Transform the messages produced by an Attribute.

map_attribute(ButtonClicked, on_click_attr)
pub fn maximum(max: Int, length: Length) -> Length

Set a maximum size.

Example

el([width(maximum(500, fill))], text("Max 500px wide"))
pub fn minimum(min: Int, length: Length) -> Length

Set a minimum size.

Example

el([width(minimum(100, shrink))], text("At least 100px"))
pub fn modular(base: Float, ratio: Float, step: Int) -> Float

Create a modular scale.

A modular scale is a sequence of numbers where each value is the previous value multiplied by a ratio. Useful for creating harmonious font sizes.

// Golden ratio scale
let scale = modular(16.0, 1.618, _)

font.size(float.round(scale(0)))  // 16
font.size(float.round(scale(1)))  // 26
font.size(float.round(scale(2)))  // 42
font.size(float.round(scale(-1))) // 10
pub fn mouse_down(
  styles: List(Decoration(msg)),
) -> @internal Attribute(@internal Aligned, msg)

Style an element when it’s being clicked.

Note: This is a simplified implementation. For full pseudo-class support, you may need to use CSS directly via html_attribute.

el([mouse_down([Decoration(scale(0.95))])], text("Click me"))
pub fn mouse_over(
  styles: List(Decoration(msg)),
) -> @internal Attribute(@internal Aligned, msg)

Style an element when the mouse is over it.

Note: This is a simplified implementation. For full pseudo-class support, you may need to use CSS directly via html_attribute.

import lustre/attribute

el([html_attribute(attribute.class("hoverable"))], text("Hover me"))
pub fn move_down(
  distance: Float,
) -> @internal Attribute(@internal Aligned, msg)

Move an element down.

Example

el([move_down(10.0)], text("Shifted down 10px"))
pub fn move_left(
  distance: Float,
) -> @internal Attribute(@internal Aligned, msg)

Move an element left.

Example

el([move_left(10.0)], text("Shifted left 10px"))
pub fn move_right(
  distance: Float,
) -> @internal Attribute(@internal Aligned, msg)

Move an element right.

Example

el([move_right(10.0)], text("Shifted right 10px"))
pub fn move_up(
  distance: Float,
) -> @internal Attribute(@internal Aligned, msg)

Move an element up.

Example

el([move_up(10.0)], text("Shifted up 10px"))
pub fn new_tab_link(
  attrs: List(@internal Attribute(@internal Aligned, msg)),
  config: LinkConfig(msg),
) -> Element(msg)

A link that opens in a new tab.

Example

new_tab_link([], LinkConfig(url: "https://example.com", label: text("Open in new tab")))
pub const none: Element(a)

An empty element.

Example

el([], none)
pub fn on_left(
  element: Element(msg),
) -> @internal Attribute(@internal Aligned, msg)

Place an element to the left of the current element.

el([on_left(icon)], text_content)
pub fn on_right(
  element: Element(msg),
) -> @internal Attribute(@internal Aligned, msg)

Place an element to the right of the current element.

el([on_right(label)], input_field)
pub fn padding(
  value: Int,
) -> @internal Attribute(@internal Aligned, msg)

Set padding on all sides.

Example

el([padding(20)], text("20px padding on all sides"))
pub fn padding_each(
  top: Int,
  right: Int,
  bottom: Int,
  left: Int,
) -> @internal Attribute(@internal Aligned, msg)

Set padding on each side independently.

Order is: top, right, bottom, left (clockwise from top)

Example

el([padding_each(10, 20, 10, 20)], text("Custom padding"))
pub fn padding_xy(
  x: Int,
  y: Int,
) -> @internal Attribute(@internal Aligned, msg)

Set horizontal and vertical padding separately.

Example

el([padding_xy(20, 10)], text("20px horizontal, 10px vertical"))
pub fn paragraph(
  attributes: List(@internal Attribute(@internal Aligned, msg)),
  children: List(Element(msg)),
) -> Element(msg)

Text that wraps with spacing between inline elements.

This is useful for text with inline elements like links or emphasis.

Example

paragraph([], [
  text("This is a "),
  el([font_bold], text("bold")),
  text(" word."),
])
pub const pointer: @internal Attribute(@internal Aligned, msg)

Set the cursor to a pointer when hovering over this element.

el([pointer], clickable_content)
pub fn px(value: Int) -> Length

A length in pixels.

Example

el([width(px(200)), height(px(100))], text("Fixed size"))
pub fn rgb(r: Float, g: Float, b: Float) -> Color

Create a color from red, green, and blue channels.

Each channel takes a value between 0.0 and 1.0.

Example

rgb(0.5, 0.2, 0.8)  // Purple
pub fn rgb255(red: Int, green: Int, blue: Int) -> Color

Create a color from red, green, and blue channels.

Each channel takes a value between 0 and 255.

Example

rgb255(128, 50, 200)  // Purple
pub fn rgba(r: Float, g: Float, b: Float, a: Float) -> Color

Create a color from red, green, blue, and alpha channels.

Each channel takes a value between 0.0 and 1.0.

Example

rgba(1.0, 0.0, 0.0, 0.5)  // Semi-transparent red
pub fn rgba255(
  red: Int,
  green: Int,
  blue: Int,
  alpha: Float,
) -> Color

Create a color from red, green, blue, and alpha channels.

RGB channels take values between 0 and 255. Alpha takes a value between 0.0 and 1.0.

Example

rgba255(255, 0, 0, 0.5)  // Semi-transparent red
pub fn rotate(
  angle: Float,
) -> @internal Attribute(@internal Aligned, msg)

Rotate an element.

The angle is in radians. Use functions like degrees to convert from degrees.

Example

el([rotate(3.14159)], text("Upside down"))
pub fn row(
  attributes: List(@internal Attribute(@internal Aligned, msg)),
  children: List(Element(msg)),
) -> Element(msg)

Layout children horizontally.

Example

row([spacing(10)], [
  text("First"),
  text("Second"),
  text("Third"),
])
pub fn scale(
  factor: Float,
) -> @internal Attribute(@internal Aligned, msg)

Scale an element.

Example

el([scale(1.5)], text("150% size"))
pub const scrollbar_x: @internal Attribute(@internal Aligned, msg)

Enable horizontal scrollbar when content overflows.

Example

el([scrollbar_x, width(px(200))], wide_content)
pub const scrollbar_y: @internal Attribute(@internal Aligned, msg)

Enable vertical scrollbar when content overflows.

Example

el([scrollbar_y, height(px(200))], long_content)
pub const scrollbars: @internal Attribute(@internal Aligned, msg)

Enable scrollbars on both axes when content overflows.

Example

el([scrollbars, width(px(200)), height(px(200))], 
   text("Very long content that will scroll..."))
pub const shrink: Length

Shrink an element to fit its contents.

Example

el([width(shrink)], text("Fits content"))
pub const space_evenly: @internal Attribute(
  @internal Aligned,
  msg,
)

Evenly distribute space between child elements.

row([space_evenly], [child1, child2, child3])
pub fn spacing(
  value: Int,
) -> @internal Attribute(@internal Aligned, msg)

Set spacing between children.

Example

row([spacing(10)], [
  text("First"),
  text("Second"),  // 10px space here
  text("Third"),
])
pub fn spacing_xy(
  x: Int,
  y: Int,
) -> @internal Attribute(@internal Aligned, msg)

Set horizontal and vertical spacing separately.

Example

row([spacing_xy(20, 10)], children)
pub fn text(content: String) -> Element(msg)

Display a string of text.

Example

text("Hello, world!")
pub fn text_column(
  attributes: List(@internal Attribute(@internal Aligned, msg)),
  children: List(Element(msg)),
) -> Element(msg)

Multiple paragraphs with spacing between them.

Example

text_column([spacing(20)], [
  paragraph([], [text("First paragraph.")]),
  paragraph([], [text("Second paragraph.")]),
])
pub fn transparent(
  is_transparent: Bool,
) -> @internal Attribute(@internal Aligned, msg)

Make an element transparent and ignore mouse/touch events.

The element will still take up space in the layout.

Example

el([transparent(True)], text("You can't see or click me!"))
pub fn width(
  length: Length,
) -> @internal Attribute(@internal Aligned, msg)

Set the width of an element.

Example

el([width(px(200))], text("200px wide"))
el([width(fill)], text("Fills available width"))
pub fn wrapped_row(
  attributes: List(@internal Attribute(@internal Aligned, msg)),
  children: List(Element(msg)),
) -> Element(msg)

Layout children horizontally, wrapping to new lines when needed.

Example

wrapped_row([spacing(5)], [
  text("This"),
  text("will"),
  text("wrap"),
])
Search Document