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
lustre_stylish- Core layout functions (63 functions)lustre_stylish/font- Typography styling (28 functions)lustre_stylish/border- Borders and shadows (15 functions)lustre_stylish/background- Backgrounds and gradients (8 functions)lustre_stylish/events- Event handlers (5 functions)lustre_stylish/region- Accessibility landmarks (8 functions)lustre_stylish/keyed- Efficient list rendering (3 functions)lustre_stylish/input- Form input types (stub - see module docs)
Total: 130+ functions for building complete UIs
Basic Elements
none- An empty elementtext- Display a stringel- An element that wraps a single child
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.
row- Layout children horizontallywrapped_row- Horizontal layout that wraps to new linescolumn- Layout children vertically
Text Layout
paragraph- Text that wraps with spacing between inline elementstext_column- Multiple paragraphs with spacing
Size
width,height- Set the size of an elementpx- Size in pixelsfill- Fill available space (constant, no())fill_portion- Fill proportionallyshrink- Fit to content (constant, no())
Layout Attributes
padding- Space between border and contentspacing- Space between childrencenter_x,center_y- Center alignment (constants, no())align_left,align_right,align_top,align_bottom- Edge alignment (constants, no())
Advanced Features
- Transformations:
scale,rotate,move_up,move_down, etc. - Transparency:
transparent,alpha - Scrolling:
scrollbars(),scrollbar_x(),scrollbar_y()(functions, need()) - Clipping:
clip(),clip_x(),clip_y()(functions, need()) - Links:
link,new_tab_link,download - Images:
image
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)
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
-
Device(class: DeviceClass, orientation: Orientation)
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
-
LayoutShow 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
-
NoHoverDisable all hover styles
-
ForceHoverAlways enable hover styles (useful for mobile)
-
FocusStyle(style: @internal FocusStyle)Provide custom focus styling
-
NoStaticStyleSheetDon’t embed the static stylesheet (when using multiple layouts)
pub type LayoutOptions {
LayoutOptions(options: List(LayoutOption))
}
Constructors
-
LayoutOptions(options: List(LayoutOption))
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 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 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"))