How do I write a function that displays a text in a box with a heading?

I would like to have a function that displays any content in a box and provides it with a heading. The image shows an example (the text should not be painted over). The double border is not important to me. A single border is sufficient.

#let my_box(title, content) = {
    // ...
}

And that’s how I wanted to use the function:

#my_box([This is a title]) [
    - And here
    - is the content
]

Is this good enough?:

 #let my_box(title, body) = {
    box(stroke: 1pt, outset: 2pt, title)
    linebreak()
    v(-9pt)
    box(stroke: 1pt, outset: 2pt, body)
}

#my_box([This is a title])[
    - And here
    - is the content
]

grafik

I wrote down the function call again, because it is important that there is no space between the closing round bracket and the opening “[” for Typst to understand that it should be the content of the function.
I think you could also add a slight offset to the title box and let the content begin a little bit lower if you want to mimic your example picture a bit more. If you need help with that, feel free to ask again :)

showybox may fit your needs.

2 Likes

Here’s my attempt:

#let my-box(title, content) = {
  block(stroke: .5pt, inset: (x: 8pt, top: 16pt, bottom: 8pt))[
    #place(top+left, dx: -12pt, dy: -20pt)[
      #block(stroke: .5pt, inset: 4pt, fill: white, title)
    ]
    #content
  ]
}

#my-box([Some title])[
  - Here is
  - the content.
]
Output

Edit: if the title is long and the content block is not wide enough, it will break and overlap the content block. One can combine context and measure to dynamically create the needed spacing:

#let my-box(title, content) = {
  let title-box = box(width: 60pt, stroke: .5pt, inset: 4pt, fill: white, title)
  
  context block(stroke: .5pt, inset: 8pt, breakable: false)[
    #v(measure(title-box).height)
    #place(top+left, dx: -12pt, dy: -12pt, title-box)
    #content
  ]
}

The disadvantage of this is that the title box needs to have a fixed width, otherwise measure will assume infinite space and return 0.

Indeed, showybox is a great package for this, but unfortunately it slightly lacks configuration flexibility. You can use shadow for this, but for shadow you can only set fill, but not stroke. Here I modified it (don’t know how long will the link last, maybe you should pitch the feature to the author so it can be included in the official package version):
showybox:2.0.1.zip

With this I can do:

#import "@preview/showybox:2.0.1": showybox
#showybox(
  width: 11cm,
  frame: (title-color: white, radius: 0pt),
  shadow: (color: black, offset: 0.2em),
  title-style: (
    color: black,
    boxed-style: (
      anchor: (x: start, y: top),
      offset: (x: -1.4em, y: -0.4em),
      radius: 0pt,
    ),
  ),
  title: lorem(2),
  lorem(25),
)

image

And here is my one-file solution:

#let shadow-box(title, body, offset: 2pt, title-offset: 2pt) = {
  let frame = block.with(
    width: 11cm, // This probably should be removed.
    inset: (top: 3em, rest: 0.7em),
    fill: white,
    stroke: black,
  )
  let title-frame = frame.with(width: auto, inset: 0.7em)
  context {
    let body-height = measure(frame(body)).height
    place(dx: offset, dy: offset, frame(hide(body))) // shadow
    frame(body)
    place(dx: -offset, dy: -body-height - offset, title-frame(hide(title))) // shadow
    let offset = title-offset + offset
    place(dx: -offset, dy: -body-height - offset, title-frame(title))
  }
}

#shadow-box(lorem(2), lorem(25))
Output

image