Skip to content

Conversation

@elnelson575
Copy link
Contributor

@elnelson575 elnelson575 commented Nov 6, 2025

This PR makes card headers flex by default, with equivalent settings to .hstack.

This makes it much easier to organize complex inputs in headers with vanilla formatting or using other spacing utils to control how the items are spaced within the header.

We are not adding this to footers because it seems like footers are more likely to be longer blocks of text, like figure captions, footnotes, etc.

Minor breakage is possible for people currently expecting vertically stacked inputs in headers, however based on a quick survey of publicly available code on GitHub, this seems to be a very, very small population.

An example with a few inputs in a header:

library(shiny)
library(bslib)

ui <- page_fillable(
    tags$head(
        tags$link(rel = "stylesheet", type = "text/css", href = "styles.css")
    ),
    card(
        card_header(
            icon("star"),
            "Card 1 header",
            actionButton("go", "Go", class = "btn-sm"),
            actionButton("edit", "Edit", class = "btn-sm"),
            checkboxInput("checkbox", "Checkbox", value = FALSE),
        ),
        p("Card 1 body"),
        sliderInput("slider", "Slider", 0, 10, 5),
        max_height = "500px",
        card_footer(
            p(
                "lots of text here, texting text, real text that makes you think, text that texts, but doesn't text
            because it does not possess fingers, text that sits on the page and doesn't really do more than that,
            text that thwarts autocomplete because it is unexpected. Text."
            )
        ),
    )
)

server <- function(input, output) {}


shinyApp(ui = ui, server = server)
Screenshot 2025-11-06 at 1 30 15 PM

An example with ms-auto pushing the second part of the header to the far right:

library(shiny)
library(bslib)

ui <- page_fillable(
    tags$head(
        tags$link(rel = "stylesheet", type = "text/css", href = "styles.css")
    ),
    card(
        card_header(
            icon("star"),
            "Card 1 header",
            actionButton("go", "Go", class = "ms-auto btn-sm border-0"),
        ),
        p("Card 1 body"),
        sliderInput("slider", "Slider", 0, 10, 5),
        max_height = "500px",
        card_footer(
            p(
                "lots of text here, texting text, real text that makes you think, text that texts, but doesn't text
            because it does not possess fingers, text that sits on the page and doesn't really do more than that,
            text that thwarts autocomplete because it is unexpected. Text."
            )
        ),
    )
)

server <- function(input, output) {}
shinyApp(ui = ui, server = server)

Screenshot 2025-11-06 at 1 26 17 PM

Interactions with Nav implementations & nav_spacer

Updates to the card_header scss were made in order to implement the following behaviors:

If a card uses a nav implementation like navset_card_tab, everything will behave normally, including spacers. Screenshot 2025-11-10 at 1 56 14 PM
library(shiny)
library(bslib)

ui <- page_fillable(
    tags$head(
        tags$link(rel = "stylesheet", type = "text/css", href = "styles.css")
    ),
    navset_card_tab(
        nav_panel(
            title = "One",
            p("Card 1 body"),
            sliderInput("slider", "Slider", 0, 10, 5),
            max_height = "500px",
            card_footer(
                checkboxInput("chkbox", "Checkbox"),
            )
        ),
        nav_panel(title = "Two", p("Second tab content.")),
        nav_panel(title = "Three", p("Third tab content")),
        nav_spacer(),
        nav_menu(
            title = "Links",
            nav_item("link_shiny"),
            nav_item("link_posit")
        )
    )
)

server <- function(input, output) {}

shinyApp(ui = ui, server = server)
`nav_spacer` will also work correctly in a `card_header`, even if no other nav items are present: Screenshot 2025-11-10 at 1 57 59 PM
library(shiny)
library(bslib)

ui <- page_fillable(
    tags$head(
        tags$link(rel = "stylesheet", type = "text/css", href = "styles.css")
    ),
    card(
        card_header(
            icon("star"),
            "Card 1 header",
            actionButton("go", "Go", class = "btn-sm"),
            actionButton("edit", "Edit", class = "btn-sm"),
            nav_spacer(),
            actionButton("Test", "test", class = "btn-sm"),
        ),
        p("Card 1 body"),
        sliderInput("slider", "Slider", 0, 10, 5),
        max_height = "500px",
        card_footer(
            p(
                "lots of text here, texting text, real text that makes you think, text that texts, but doesn't text
            because it does not possess fingers, text that sits on the page and doesn't really do more than that,
            text that thwarts autocomplete because it is unexpected. Text."
            )
        ),
    )
)

server <- function(input, output) {}

shinyApp(ui = ui, server = server)

If there is both a nav container with a `nav_spacer` and other flex items in the card header, the nav will use the spacer but leave room for the remaining items: Screenshot 2025-11-10 at 2 01 58 PM
library(shiny)
library(bslib)

ui <- page_fillable(
    tags$head(
        tags$link(rel = "stylesheet", type = "text/css", href = "styles.css")
    ),
    card(
        card_header(
            tags$nav(
                class = "nav",
                icon("star"),
                "Card 1 header",
                nav_spacer(),
                actionButton("go", "Go", class = "btn-sm"),
                actionButton("edit", "Edit", class = "btn-sm"),
            ),
            actionButton("Test", "test", class = "btn-sm"),
        ),
        p("Card 1 body"),
        sliderInput("slider", "Slider", 0, 10, 5),
        max_height = "500px",
        card_footer(
            p(
                "lots of text here, texting text, real text that makes you think, text that texts, but doesn't text
            because it does not possess fingers, text that sits on the page and doesn't really do more than that,
            text that thwarts autocomplete because it is unexpected. Text."
            )
        ),
    )
)

server <- function(input, output) {}


shinyApp(ui = ui, server = server)

@elnelson575 elnelson575 self-assigned this Nov 6, 2025
@elnelson575 elnelson575 linked an issue Nov 6, 2025 that may be closed by this pull request
@elnelson575 elnelson575 requested a review from gadenbuie November 6, 2025 18:40
@elnelson575 elnelson575 marked this pull request as ready for review November 6, 2025 18:40
@gadenbuie
Copy link
Member

gadenbuie commented Nov 6, 2025

.bslib-gap-spacing primarily serves to remove margin-bottom as the primary way to space out elements, i.e. we're in a context where gap controls spacing.

.bslib-gap-spacing {
gap: var(--bslib-mb-spacer);
&,
& > .shiny-html-output,
& > .shiny-panel-conditional {
> .bslib-mb-spacing, > .form-group, > p, > pre {
margin-bottom: 0;
}
}
}

@gadenbuie gadenbuie changed the base branch from feat/toolbar-epic to main November 6, 2025 20:13
Copy link
Member

@gadenbuie gadenbuie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good to merge once the last couple details are handled

@gadenbuie gadenbuie mentioned this pull request Nov 6, 2025
elnelson575 and others added 2 commits November 7, 2025 11:01
Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
gadenbuie
gadenbuie previously approved these changes Nov 12, 2025
Copy link
Member

@gadenbuie gadenbuie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, one more thing to track down to see if we can simplify before merging!

Copy link
Member

@gadenbuie gadenbuie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@elnelson575 elnelson575 merged commit 86ca69f into main Nov 13, 2025
24 of 25 checks passed
@elnelson575 elnelson575 deleted the feat/card-header-flex branch November 13, 2025 17:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make card_header() display flex (equivalent to .hstack) by default

3 participants