FrameSeq

Styling#

For presentation-wide colors, fonts, spacing, and corner radii, start with Themes. The modifiers below override the theme for an individual element.

Content and low-level components return builders. Every modifier returns the same builder, so calls can be chained.

text("Important")
  .size(pt(30))
  .bold()
  .color("#38bdf8")
  .margin(12, 0);

Presentation-wide typography#

Use the font option in presentation() when slides should keep a theme's colors and layout but use different font defaults:

presentation({
  title: "My Slides",
  theme: "minimal-academic",
  font: {
    family: '"Noto Sans SC", sans-serif',
    size: 24,
    weight: 400,
    lineHeight: 1.5,
    heading: {
      family: '"Noto Serif SC", serif',
      size: 40,
      weight: 700,
    },
    code: {
      family: '"JetBrains Mono", monospace',
      size: 18,
    },
  },
});

Numbers used as size values are pixels. Length helpers and CSS strings are also accepted, such as pt(20), "1.5rem", or "24px". lineHeight can be a unitless number or a CSS string.

The precedence is: an individual object's modifier or .style() value, then presentation.font, then the selected theme. A top-level family applies to both body text and headings; code keeps the theme's monospace family unless code.family is set.

Lengths and units#

A number is interpreted as pixels:

text("Pixels").size(24);

Use helpers when the unit should be explicit:

px(20)
pt(20)
rem(2)
percent(50)
vw(40)
vh(30)

These helpers return CSS length strings and can be used anywhere a Length is accepted.

Size and spacing#

.width(640)
.height(percent(100))
.minWidth(200)
.minHeight(120)
.maxWidth(720)
.maxHeight(400)
.padding(24)
.padding(16, 24)              // vertical, horizontal
.padding({ top: 8, left: 16 }) // individual sides
.margin(12)
.margin(8, 16)                // vertical, horizontal
.margin({ bottom: 24 })       // individual sides
.gap(20)
.gap(12, 40)                  // rows, then columns

maxWidth() is how a paragraph is held to a readable measure. In the object form of padding() and margin(), a side that is left out is zero, exactly as in the CSS shorthand — padding({ top: 8 }) sets the other three to zero rather than leaving them alone.

Appearance#

.background("#0f172a")
.color("#f8fafc")
.border("1px solid #334155")
.radius(16)
.opacity(0.8)

Typography#

.size(pt(28))
.fontSize(pt(28))
.weight(600)
.fontWeight(600)
.bold()
.lineHeight(1.4)
.textAlign("center")

size() aliases fontSize(), and weight() aliases fontWeight().

Text role modifiers provide semantic defaults:

.body()
.title()
.hero()
.subtitle()
.author()
.eyebrow()
.lead()
.caption()
.quote()

Text roles are available on objects returned by the lowercase text() authoring command.

Flex layout#

Every modifier below is available on every object, but each one is answered by a different part of the layout. Three of them arrange the object's own children, so they need it to be a row, column, or grid:

.row()
.column()
.stack()
.grid(3)
.center()
.align("center")
.justify("space-between")
.wrap()
.alignContent("center")

The other two describe how this object sits inside the container that holds it, so they need that container to be a row, column, or grid:

.selfAlign("center")
.centerSelf()
.grow()

align(), selfAlign(), and alignContent() accept "start", "center", "end", and "stretch". justify() accepts "start", "center", "end", "space-between", "space-around", and "space-evenly"; alignContent() accepts those distributions too, and only means something once wrap() is on.

align() and selfAlign() work across the region — horizontally in a column, vertically in a row — while justify() works along it. Which axis a modifier moves is the full table; Center one object covers centring one thing rather than the whole region.

Objects stretch across the axis by default, so selfAlign() moves an object only once it has a size of its own — width() in a column, height() in a row.

A modifier that lands somewhere it cannot mean anything is ignored by the browser without a word. frameseq check reports those as inert-modifier rather than letting the page quietly disagree with the source.

Position and transform#

.position({ x: 80, y: 120 })
.rotate(-4)

position() uses absolute positioning and is intended for a canvas or another deliberately positioned parent.

For a positioned region inside an ordinary flow layout, name a container and make it a local canvas:

at("panel").canvas().width(600).height(260).clip();

text("Local coordinates").position({ x: 32, y: 24 });

Objects written after at("panel") belong to that container. Its .canvas() makes their coordinates local, while .clip() keeps them inside the panel. Pass false to .clip(false) to restore visible overflow.

Reveals#

text("Appears first").showAt(1);
text("Appears second").showAt(2);

Reveal indices start at 1. In PDF and print mode, all reveal steps are visible.

Tailwind CSS#

Tailwind utilities are built in and require no configuration. Pass a utility string to style():

text("A strong statement")
  .style("text-4xl font-bold tracking-tight text-blue-600");

text("Precisely placed")
  .style("absolute left-[80px] top-[120px] w-[640px]");

Arbitrary values such as text-[30px], bg-[#0f172a], and grid-cols-[2fr_3fr] are supported. Keep dynamically selected utilities as complete strings so Tailwind can detect them:

const emphasis = important ? "text-red-600" : "text-slate-500";
text("Status").style(emphasis);

Avoid constructing fragments such as `text-${color}-600` because those complete class names do not appear in the source file.

Inline CSS and class names#

The object overload remains available for inline CSS:

text("Custom")
  .className("my-callout")
  .style({
    letterSpacing: "0.08em",
    textTransform: "uppercase",
  });

Properties passed to style() use JavaScript-style CSS names such as letterSpacing.

Inline properties take precedence over Tailwind utilities regardless of call order. className() remains available when a class name should be attached without using the style() shorthand.