Lesson 26 of 45 · Web pages

Page structure and lists

A page that only uses boxes looks fine in a browser and is a maze for everyone who cannot see it. This lesson shows the elements that name the parts of a page, the elements for lists, and the ones for things that open and close, and it ends with a complete blog page built from them.

Boxes and landmarks

box and span are the generic containers. A box is a block, it starts on a new line and takes the full width. A span sits inside a line of text. Neither one says anything about its content, so they are the right tools when the only reason for the container is styling.

When a part of the page has a job, there is an element that names it. header holds the introduction, nav a group of navigation links, main the content the page is about, aside something related but separate, and footer the closing part. section is a themed group of content that usually starts with a heading, article is a piece that makes sense alone, such as a post or a comment, and search wraps the controls for searching. Browsers draw all of them like a plain box. The difference is for the people who cannot see the page: a screen reader lists these parts as landmarks and lets its user jump to the main content or to the navigation in one key press. Search engines read the outline in the same way.

web-structure-landmarks.salam
page:
    title = "Landmarks"
    global: selector = "body" font family = "system-ui, sans-serif" margin = 0 line height = 1.5 end
    header: background = "#0f766e" color = "white" padding = "10px 20px"
        h1: content = "Garden Notes" margin = 0 font size = "1.3rem" end
    end
    nav: background = "#ccfbf1" padding = "8px 20px" display = flex gap = 16
        link: url = "#soil" content = "Soil" end
        link: url = "#water" content = "Water" end
    end
    box: display = flex gap = 20 padding = 20
        main: flex = 1
            section: id = "soil"
                heading: size = 2 content = "Soil" end
                paragraph: content = "Dig in compost before you plant." end
            end
            section: id = "water"
                heading: size = 2 content = "Water" end
                paragraph: content = "Water early, when the air is cool." end
            end
        end
        aside: width = 160 background = "#f1f5f9" padding = 12 border radius = 8
            strong: content = "Tip" end
            paragraph: content = "A thick layer of mulch keeps the soil damp." end
        end
    end
    footer: padding = "10px 20px" border top = "1px solid #cbd5e1" color = "#475569"
        small: content = "Garden Notes, written by hand." end
    end
end

The example puts a header and a nav above, then a main with two sections and an aside next to it, then a footer. The id on each section is what the links #soil and #water point to: a link that starts with # scrolls to the element with that id. The display = flex and gap lines are CSS and are explained in the CSS lessons.

A page can have only one main, and it cannot sit inside an article. The compiler reports an error for a second one (E120). It also refuses a header or a footer inside another header or footer, and an address inside an address. A header or footer inside an article or a section is fine: it then belongs to that part and not to the whole page. When a page has two nav elements, tell them apart with aria label, for example aria label = "Main", so that a screen reader can announce which one it is on.

Lists

list is a bullet list. ordered list is a numbered one, for steps and rankings where the order matters. Only item elements can sit directly inside a list, and an item holds the text of the entry in its content. To nest a list, put it inside an item, after the content, as the example does under Olives. Browsers use a different marker for each level.

web-structure-lists.salam
page:
    title = "Lists"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" line height = 1.6 end
    heading: size = 2 content = "Shopping" end
    list:
        item: content = "Bread" end
        item:
            content = "Olives"
            list:
                item: content = "Green" end
                item: content = "Black" end
            end
        end
        item: content = "Tea" end
    end
    heading: size = 2 content = "Countdown" end
    ordered list: reversed = true
        item: content = "Mix the dough" end
        item: content = "Let it rise" end
        item: content = "Bake" end
    end
    heading: size = 2 content = "Chapters" end
    ordered list: start = 4 type = "I"
        item: content = "The road" end
        item: content = "The inn" end
        item: value = 9 content = "The return" end
    end
end

An ordered list has three attributes. reversed counts down, so a list of three items is numbered 3, 2, 1. start is the number of the first item, and it can be zero or negative. type chooses the kind of marker: 1, a, A, i or I. On a single item, value sets that item's number and the items after it carry on from there, which is why the last chapter above is IX. A bullet list ignores all of these. The marker can also be changed with the CSS property list style type, for example persian for Persian digits.

menu is a list of commands, for example the buttons of a toolbar. It holds item elements like list does. Do not use it for the links of a site: that is a nav with a list inside.

Description lists

A description list is for name and value pairs: a glossary, the details of a product, the facts in a profile. A term is the name, and the description after it is the value. One term can have several descriptions, as the Footer entry in the first list shows, and several terms can share one description. Wrap each group in a box when you want to style a group as a unit, or to keep the groups visibly apart, as in the second list.

web-structure-definitions.salam
page:
    title = "Definitions"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" line height = 1.6 end
    description list:
        term: content = "Header" font weight = "bold" end
        description: content = "The top of a page or of a section." end
        term: content = "Footer" font weight = "bold" end
        description: content = "Closing notes, links and the author." end
        description: content = "It can also close a section." end
    end
    description list:
        box: margin top = 16
            term: content = "Name" font weight = "bold" end
            description: content = "Salam" end
        end
        box: margin top = 16
            term: content = "Kind" font weight = "bold" end
            description: content = "Programming language" end
        end
    end
end

You always write term and description. The compiler tells the two situations apart by the parent: directly inside the list they are the plain elements, and inside a box that sits directly inside a list they are the variants called term in box and description in box. They produce the same tags. Outside a description list neither is allowed, and the compiler says so.

Details and summary

details is a block the reader can open and close, with no script at all. Its first child must be a summary, and the compiler refuses it anywhere else. The summary is the line that is always visible and acts as the button. Everything after the summary is hidden until the block is opened. With open = true a block starts open. Give several blocks the same name and they become an exclusive group: opening one closes the others, which is exactly an accordion. A block without a name stays independent.

web-structure-details.salam
page:
    title = "Questions"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" max width = "30rem" end
    global: selector = "details" border = "1px solid #cbd5e1" border radius = 8 padding = "8px 12px" margin bottom = 8 end
    global: selector = "summary" cursor = pointer font weight = "bold" end
    details: name = "faq" open = true
        summary: content = "What is a page block?" end
        paragraph: content = "It holds everything on the page." end
    end
    details: name = "faq"
        summary: content = "Can two be open at once?" end
        paragraph: content = "Not here. They share the name faq, so opening one closes the other." end
    end
    details:
        summary: content = "And this one?" end
        paragraph: content = "It has no name, so it opens and closes on its own." end
    end
end

In the example the first two blocks share the name faq, and the third has no name. Click them to see the difference. The global lines at the top only style the boxes and the pointer. They use plain tag names as selectors.

Dialog

dialog is a box that floats above the page, for a message or a question. It has two ways of appearing. With open = true it is shown at once, with no dimming of the page behind it. To show it as a modal, a dialog that dims the page, traps the keyboard focus and closes on the Esc key, you have to ask the browser from a script. In Salam that is one call from a click handler: dom.Call0(dom.ById("saved"), "showModal"), and "close" in the same way to close it. The attribute closedby says what else may close it: closerequest (the Esc key and the device's close gesture), any (Esc or a click outside) or none (only your own code).

web-structure-dialog.salam
import dom

page:
    title = "Dialog"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" end
    button: content = "Save the draft" click = (): dom.Call0(dom.ById("saved"), "showModal") end end
    paragraph: id = "state" content = "Nothing saved yet." end
    dialog: id = "saved" closedby = "any" padding = 20 border radius = 10
        heading: size = 2 content = "Saved" margin top = 0 end
        paragraph: content = "Your draft is stored. Press Esc, click outside or use the button." end
        button:
            content = "Close"
            click = (): dom.SetText(dom.ById("state"), "Draft saved.") dom.Call0(dom.ById("saved"), "close") end
        end
    end
end

The page starts with import dom, which is needed for the calls. dom.ById finds the element by its id, and the compiler checks that an element with that id exists in the same page. dom.SetText changes the text of the paragraph. Events and the dom package get their own lessons later.

Figures and addresses

figure wraps something that belongs with the text but can stand on its own: a picture, a chart, a listing. The figure caption inside it names the figure, and a screen reader connects the two. The image gets its own text description in the alt attribute, and the next lesson explains why that is a different thing from the caption. address is for contact details of the author of the nearest article or of the whole page, not for any postal address you happen to mention. Browsers set it in italic, and the example turns that off with font style = normal.

web-structure-figure.salam
page:
    title = "Figure"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" max width = "30rem" end
    figure: margin = 0
        image: url = "data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20viewBox=%270%200%20260%20120%27%3E%3Crect%20width=%27260%27%20height=%27120%27%20fill=%27%23bae6fd%27/%3E%3Ccircle%20cx=%27200%27%20cy=%2740%27%20r=%2724%27%20fill=%27%23facc15%27/%3E%3Crect%20y=%2790%27%20width=%27260%27%20height=%2730%27%20fill=%27%2316a34a%27/%3E%3C/svg%3E" alt = "A yellow sun over a green field" width = 260 height = 120 end
        figure caption: content = "Figure 1. The field at noon." color = "#475569" font size = "0.9rem" end
    end
    address: margin top = 20 font style = normal
        content = "Garden Notes"
        break: end
        link: url = "mailto:hello@example.com" content = "hello@example.com" end
    end
end

A whole page

Here is a blog post that uses most of the elements above. The page header holds the name of the site and a search with its form. A nav follows. The main holds one article, which has a header of its own with the title and the date in a time element, and two section parts that each start with a level 2 heading. An aside next to it lists related reading, and the page footer holds the address. Read the code from the outside in, and notice that every element has a reason to be there.

web-structure-blog.salam
page:
    title = "A blog post"
    global: selector = "body" font family = "system-ui, sans-serif" margin = 0 line height = 1.6 color = "#1e293b" end
    header: display = flex justify content = "space-between" align items = center padding = "10px 20px" background = "#0f766e" color = "white"
        strong: content = "Garden Notes" end
        search:
            form: input: type = "search" placeholder = "Search" aria label = "Search the blog" end end
        end
    end
    nav: display = flex gap = 16 padding = "8px 20px" background = "#ccfbf1" aria label = "Main"
        link: url = "#" content = "Home" end
        link: url = "#" content = "Archive" end
    end
    box: display = flex gap = 24 padding = 20
        main: flex = 1
            article:
                header:
                    heading: size = 1 content = "Why mulch matters" margin bottom = 4 end
                    paragraph: margin top = 0 color = "#64748b"
                        content = "By Sara, "
                        time: datetime = "2026-10-26" content = "26 October 2026" end
                    end
                end
                section:
                    heading: size = 2 content = "Less watering" end
                    paragraph: content = "A layer of straw keeps the sun off the soil." end
                end
                section:
                    heading: size = 2 content = "Fewer weeds" end
                    paragraph: content = "Seeds in the dark do not sprout." end
                end
            end
        end
        aside: width = 170 aria label = "Related"
            heading: size = 2 content = "Related" font size = "1.1rem" end
            list: padding inline start = 20
                item: link: url = "#" content = "Compost basics" end end
                item: link: url = "#" content = "Rain barrels" end end
            end
        end
    end
    footer: padding = "10px 20px" border top = "1px solid #cbd5e1"
        address: font style = normal content = "Garden Notes, 12 Orchard Lane" end
    end
end

Reference: structure and list elements

Every element of this lesson is below, with its attributes and a short example. The examples are checked by the compiler, so you can paste any of them inside a page: block. The attributes that every element has, such as id and title, are in the lesson on the head and global attributes.

Containers and landmarks

box (Persian: جعبه; HTML: <div>)

A generic block container (HTML div). It starts on a new line and takes the full width. It has no meaning of its own, so use it only to group things for layout or style when no other element fits.

box: padding = 12 background = "#f1f5f9" border radius = 8
    paragraph: content = "Two paragraphs share one box." end
    paragraph: content = "The box gives them a background." end
end

section (Persian: بخش; HTML: <section>)

A themed group of content, almost always with a heading at its start. A page can have many sections; give each a heading so the outline makes sense.

section:
    heading: size = 2 content = "Watering" end
    paragraph: content = "Water early in the morning." end
end

article (Persian: مقاله; HTML: <article>)

A self-contained piece that would make sense if it were copied somewhere else: a blog post, a news story, a product card, a comment. Articles can be nested, for example comments inside a post.

article:
    heading: size = 2 content = "Mulch matters" end
    paragraph: content = "A layer of straw keeps the soil damp." end
end

aside (Persian: کناری; HTML: <aside>)

Content that is related to the main content but could be removed without breaking it: a sidebar, a note, a pull quote. Screen readers announce it as a complementary landmark.

aside: padding = 12 background = "#fef9c3" border radius = 8
    strong: content = "Tip" end
    paragraph: content = "Water the roots, not the leaves." end
end

header (Persian: سرصفحه; HTML: <header>)

The introduction of the page or of a section: a title, a logo, a menu. Directly in the page it is the banner landmark. Inside an article or section it introduces only that part. It cannot be placed inside another header or footer.

header: background = "#0f766e" color = "white" padding = 12
    h1: content = "Garden Notes" margin = 0 end
end

The closing part of the page or of a section: author, copyright, related links. Directly in the page it is the content information landmark. It cannot be placed inside a header or another footer.

footer: padding = 12 border top = "1px solid #cbd5e1" color = "#475569"
    small: content = "Written by hand in Salam." end
end

nav (Persian: ناوبری; HTML: <nav>)

A group of links for moving around the site or the page. Not every group of links is navigation, only the main ones. When a page has several, tell them apart with aria label.

nav: aria label = "Main" display = flex gap = 16
    link: url = "#" content = "Home" end
    link: url = "#" content = "Archive" end
end

main (Persian: اصلی; HTML: <main>)

The content the page exists for. A page can have only one, and it cannot be inside an article, aside, header, footer or nav. A screen reader can jump straight to it.

main: max width = 600
    heading: size = 1 content = "Why mulch matters" end
    paragraph: content = "The page content goes here." end
end

Wraps the controls used to search the page or the site. It is a landmark of its own, so a screen reader can jump to the search box. Put a form inside it.

search:
    form: input: type = "search" placeholder = "Search" aria label = "Search the blog" end end
end

address (Persian: آدرس; HTML: <address>)

Contact information for the author of the nearest article or of the whole page. It is not for every postal address in a text. Browsers show it in italic; font style = normal turns that off. It cannot contain another address.

address: font style = normal
    content = "Garden Notes"
    break: end
    link: url = "mailto:hello@example.com" content = "hello@example.com" end
end

Lists

list (Persian: فهرست; HTML: <ul>)

A bullet list (HTML ul). Use it when the order of the entries does not matter. Only item elements can be its direct children.

list:
    item: content = "Bread" end
    item: content = "Olives" end
    item: content = "Tea" end
end

ordered list (Persian: فهرست شماره‌دار; HTML: <ol>)

A numbered list (HTML ol) for steps and rankings, where the order matters. Its attributes change the counting and the look of the numbers.

ordered list:
    item: content = "Mix the dough" end
    item: content = "Let it rise" end
    item: content = "Bake" end
end
AttributeWhat it doesExample
reversed
معکوس
A boolean. With true the list counts down, so three items are numbered 3, 2, 1. The items stay in the order you wrote them. [bool]ordered list: reversed = true item: content = "Third place" end item: content = "Second place" end item: content = "Winner" end end
start
شروع
A whole number, the number of the first item. It may be zero or negative. The default is 1 (or the last number when the list is reversed). [int]ordered list: start = 5 item: content = "Fifth" end item: content = "Sixth" end end
type
نوع
The kind of marker: 1 for numbers (the default), a or A for letters, i or I for Roman numerals in lower or upper case. [enum: ol_type]ordered list: type = "A" item: content = "Apples" end item: content = "Pears" end end

item (Persian: مورد; HTML: <li>)

One entry of a list, an ordered list or a menu (HTML li). Its text goes in content; a nested list goes after it.

list:
    item:
        content = "Olives"
        list:
            item: content = "Green" end
            item: content = "Black" end
        end
    end
end

Only inside: list, ordered list, menu

AttributeWhat it doesExample
value
مقدار
A whole number. In an ordered list it sets the number of this item, and the items after it continue from there. In a bullet list it has no effect. [int]ordered list: item: content = "One" end item: value = 10 content = "Ten" end item: content = "Eleven" end end

menu (Persian: منو; HTML: <menu>)

A list of commands, such as the buttons of a toolbar (HTML menu). It holds item elements like a list. Site navigation is a nav with a list, not a menu.

menu:
    item: button: content = "Cut" end end
    item: button: content = "Copy" end end
    item: button: content = "Paste" end end
end

Description lists

description list (Persian: فهرست تعریف; HTML: <dl>)

A list of name and value groups (HTML dl): a glossary, the facts about a product. Its children are term and description elements, or box elements that each wrap one group.

description list:
    term: content = "Salam" end
    description: content = "A programming language." end
    term: content = "Page" end
    description: content = "What the browser shows." end
end

term (Persian: واژه; HTML: <dt>)

The name part of a group (HTML dt). It may be followed by one or more description elements. It is allowed directly inside a description list, and inside a box there.

description list:
    term: content = "Colour" end
    description: content = "Teal" end
end

Only inside: description list

description (Persian: شرح; HTML: <dd>)

The value part of a group (HTML dd). A term can have several descriptions. It is allowed directly inside a description list, and inside a box there.

description list:
    term: content = "Sizes" end
    description: content = "Small" end
    description: content = "Large" end
end

Only inside: description list

term in box (Persian: واژه; HTML: <dt>)

The same tag as term, used when the name sits inside a box that wraps one group of a description list. You write term; the compiler picks this variant from the parent. It is refused anywhere else.

description list:
    box:
        term: content = "Kind" end
        description: content = "Language" end
    end
end

description in box (Persian: شرح; HTML: <dd>)

The same tag as description, used inside a box that wraps one group of a description list. You write description; the compiler picks this variant from the parent.

description list:
    box:
        term: content = "Born" end
        description: content = "2018" end
        description: content = "Tehran" end
    end
end

Details, dialog and figures

details (Persian: جزئیات; HTML: <details>)

A block the reader can open and close without any script. Its first child must be a summary, and the rest is hidden until the block is open.

details:
    summary: content = "More about mulch" end
    paragraph: content = "Straw, bark or leaves all work." end
end
AttributeWhat it doesExample
name
نام
A string that makes details blocks with the same name an exclusive group: opening one closes the others. Blocks without a name do not affect each other. [string]details: name = "tips" summary: content = "Spring" end paragraph: content = "Sow seeds." end end details: name = "tips" summary: content = "Autumn" end paragraph: content = "Plant bulbs." end end
open
باز
A boolean. With true the block is open when the page loads. The reader can still close it. [bool]details: open = true summary: content = "Read this first" end paragraph: content = "It starts open." end end

summary (Persian: خلاصه; HTML: <summary>)

The always visible line of a details block; it works as the button that opens and closes it. It must be the first child of details.

details:
    summary: content = "Shipping" font weight = "bold" end
    paragraph: content = "Orders leave on Monday." end
end

Only inside: details

dialog (Persian: پنجره گفتگو; HTML: <dialog>)

A box that floats above the page, for a message or a question. With open = true it is shown at once. To show it as a modal call showModal on it from a click handler, and close to hide it.

dialog: open = true padding = 16 border radius = 8
    paragraph: content = "Shown without a script." end
end
AttributeWhat it doesExample
open
باز
A boolean. With true the dialog is visible when the page loads, as a normal, non-modal box with no dimmed backdrop. [bool]dialog: open = true paragraph: content = "I am open." end end
closedby
بستن با
What may close a modal dialog besides your own code: closerequest (the Esc key and the device's close gesture), any (a close request or a click outside the dialog) or none (only code). [enum: closed_by]dialog: closedby = "any" paragraph: content = "Click outside to close." end end

figure (Persian: شکل; HTML: <figure>)

Content that goes with the text but can stand on its own: an image, a chart, a code listing. It may hold one figure caption.

figure:
    preformatted: content = "1 + 1 = 2" end
    figure caption: content = "Listing 1. A sum." end
end

figure caption (Persian: زیرنویس شکل; HTML: <figcaption>)

The caption of a figure (HTML figcaption). A figure can have only one.

figure:
    paragraph: content = "A chart would be here." end
    figure caption: content = "Figure 2. Rain per month." font size = "0.9rem" end
end

Only inside: figure

Try it yourself

  • Write the landmarks of your own home page: a header with a title, a nav with three links, a main with two sections and a footer. Then add a second main and read the error.
  • Make a recipe: an ordered list of steps and, in a description list, the ingredients with their amounts. Make the steps start from 0.
  • Build a page with a question and answer for each of three topics using details. Give them one name, then remove the name from one of them and see what changes.
  • Add a button to the blog page that opens a dialog asking the reader to subscribe, and let a click outside close it.