0

How can I have a box that's automatically sized to its content (including form fields) and centered without using role="presentation" tables for layout? (I'm trying to modernize, and to reduce markup clutter.)

As you can see, I've got most of it (I think), but the form fields stick out of their container, which I suspect is down to fundamental errors in my CSS.

html {
    box-sizing: border-box;
}
*, *:before, *:after {
    box-sizing: inherit;
}

html, body {
    height: 100%;
    width: 100%;
    padding: 0;
    margin: 0;
    font-family: sans-serif;
}

.app {
    height: 100%;
    padding-top: 20px;
}

.welcome {
    text-align: center;
}

/* Center the box horizontally */
.center-wrapper {
    display: flex;
    justify-content: center;
}

/* The box I'm trying to center and size to content */
.login {
    display: inline-block;
    border: 1px solid black;
    padding: 8px;
    line-height: 1.8rem;
}

.login .fields {
    margin-top: 8px;
    display: grid;
    grid-template-columns: 40% auto;
}
<div class="app">
    <div class="center-wrapper">
        <div class="login">
            <div class="welcome"><strong>Welcome!</strong></div>
            <div class="welcome">Please sign in</div>
            <div class="fields">
                <label for="user-email">Email address:</label>
                <input id="user-email" type="email" size="25" autofocus>
                <label for="user-pass">Password:</label>
                <input id="user-email" type="password" size="25">
            </div>
        </div>
    </div>
</div>

The form fields escape their container:

login box with fields sticking out the right side

I don't want to assign fixed sizes to the elements and just make sure they fit, I want the box sized to its content and want it to handle the user zooming, different device sizes, etc.

The below gives the effect I'm looking for (very basically), but it uses a presentation table:

html {
    box-sizing: border-box;
}
*, *:before, *:after {
    box-sizing: inherit;
}

html, body {
    height: 100%;
    width: 100%;
    padding: 0;
    margin: 0;
    font-family: sans-serif;
}

.app {
    height: 100%;
    padding-top: 20px;
}

.welcome {
    text-align: center;
}

/* Center the box horizontally */
.center-wrapper {
    display: flex;
    justify-content: center;
}

/* The box I'm trying to center and size to content */
.login {
    display: inline-block;
    border: 1px solid black;
    padding: 8px;
    line-height: 1.8rem;
}

.login th {
    font-weight: normal;
}
<div class="app">
    <div class="center-wrapper">
        <table class="login" role="presentation">
            <tbody>
                <tr>
                    <th colspan="2"><strong>Welcome!</strong></th>
                </tr>
                <tr>
                    <th colspan="2">Please sign in</th>
                </tr>
                <tr>
                    <td>
                        <label for="user-email">Email address:</label>
                    </td>
                    <td>
                        <input id="user-email" type="email" size="25" autofocus>
                    </td>
                </tr>
                <tr>
                    <td>
                        <label for="user-pass">Password:</label>
                    </td>
                    <td>
                        <input id="user-email" type="password" size="25">
                    </td>
            </tbody>
        </table>
    </div>
</div>

login box with form fields contained within it

Aside from not being great semantically (it's not really a table), I don't like the markup burden.

Temani Afif
  • 245,468
  • 26
  • 309
  • 415
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875

1 Answers1

1

Either use 1fr auto so that the input will take the needed space and then the label will span the free space (you can consider some gap too)

html {
    box-sizing: border-box;
}
*, *:before, *:after {
    box-sizing: inherit;
}

html, body {
    height: 100%;
    width: 100%;
    padding: 0;
    margin: 0;
    font-family: sans-serif;
}

.app {
    height: 100%;
    padding-top: 20px;
}

.welcome {
    text-align: center;
}

/* Center the box horizontally */
.center-wrapper {
    display: flex;
    justify-content: center;
}

/* The box I'm trying to center and size to content */
.login {
    /*display: inline-block; you don't need this */
    border: 1px solid black;
    padding: 8px;
    line-height: 1.8rem;
}

.login .fields {
    margin-top: 8px;
    display: grid;
    grid-template-columns: 1fr auto;
    column-gap:10px;
}
<div class="app">
    <div class="center-wrapper">
        <div class="login">
            <div class="welcome"><strong>Welcome!</strong></div>
            <div class="welcome">Please sign in</div>
            <div class="fields">
                <label for="user-email">Email address:</label>
                <input id="user-email" type="email" size="25" autofocus>
                <label for="user-pass">Password:</label>
                <input id="user-email" type="password" size="25">
            </div>
        </div>
    </div>
</div>

Or consider 40% minmax(0,1fr) which will force the input to shrink to the remaining space

html {
    box-sizing: border-box;
}
*, *:before, *:after {
    box-sizing: inherit;
}

html, body {
    height: 100%;
    width: 100%;
    padding: 0;
    margin: 0;
    font-family: sans-serif;
}

.app {
    height: 100%;
    padding-top: 20px;
}

.welcome {
    text-align: center;
}

/* Center the box horizontally */
.center-wrapper {
    display: flex;
    justify-content: center;
}

/* The box I'm trying to center and size to content */
.login {
    /*display: inline-block; you don't need this */
    border: 1px solid black;
    padding: 8px;
    line-height: 1.8rem;
}

.login .fields {
    margin-top: 8px;
    display: grid;
    grid-template-columns: 40% minmax(0,1fr);
}
<div class="app">
    <div class="center-wrapper">
        <div class="login">
            <div class="welcome"><strong>Welcome!</strong></div>
            <div class="welcome">Please sign in</div>
            <div class="fields">
                <label for="user-email">Email address:</label>
                <input id="user-email" type="email" size="25" autofocus>
                <label for="user-pass">Password:</label>
                <input id="user-email" type="password" size="25">
            </div>
        </div>
    </div>
</div>

Related: Why does minmax(0, 1fr) work for long elements while 1fr doesn't?


The first one will not shrink the inputs while the second one will do due to the use of percetange that is based on the initial width of the input and label.

Here is a before/after to see the percentage effect

html {
    box-sizing: border-box;
}
*, *:before, *:after {
    box-sizing: inherit;
}

html, body {
    height: 100%;
    width: 100%;
    padding: 0;
    margin: 0;
    font-family: sans-serif;
}

.app {
    height: 100%;
    padding-top: 20px;
}

.welcome {
    text-align: center;
}

/* Center the box horizontally */
.center-wrapper {
    display: flex;
    justify-content: center;
}

/* The box I'm trying to center and size to content */
.login {
    /*display: inline-block; you don't need this */
    border: 1px solid black;
    padding: 8px;
    line-height: 1.8rem;
}

.login .fields {
    margin-top: 8px;
    display: grid;
}
<div class="app">
    <div class="center-wrapper">
        <div class="login">
            <div class="welcome"><strong>Welcome!</strong></div>
            <div class="welcome">Please sign in</div>
            <div class="fields" style="grid-template-columns:auto auto">
                <label for="user-email">Email address:</label>
                <input id="user-email" type="email" size="25" autofocus>
                <label for="user-pass">Password:</label>
                <input id="user-email" type="password" size="25">
            </div>
            <div class="fields" style="grid-template-columns:40% auto">
                <label for="user-email">Email address:</label>
                <input id="user-email" type="email" size="25" autofocus>
                <label for="user-pass">Password:</label>
                <input id="user-email" type="password" size="25">
            </div>
            <div class="fields" style="grid-template-columns:40% minmax(0,1fr)">
                <label for="user-email">Email address:</label>
                <input id="user-email" type="email" size="25" autofocus>
                <label for="user-pass">Password:</label>
                <input id="user-email" type="password" size="25">
            </div>
        </div>
    </div>
</div>

The 40% was based on the width of the text and the input considering that both are set to auto which will logically create an overflow since (in your case) the 40% is bigger that the initial width of the label.

A longer text will produce a different result:

html {
    box-sizing: border-box;
}
*, *:before, *:after {
    box-sizing: inherit;
}

html, body {
    height: 100%;
    width: 100%;
    padding: 0;
    margin: 0;
    font-family: sans-serif;
}

.app {
    height: 100%;
    padding-top: 20px;
}

.welcome {
    text-align: center;
}

/* Center the box horizontally */
.center-wrapper {
    display: flex;
    justify-content: center;
}

/* The box I'm trying to center and size to content */
.login {
    /*display: inline-block; you don't need this */
    border: 1px solid black;
    padding: 8px;
    line-height: 1.8rem;
}

.login .fields {
    margin-top: 8px;
    display: grid;
}
<div class="app">
    <div class="center-wrapper">
        <div class="login">
            <div class="welcome"><strong>Welcome!</strong></div>
            <div class="welcome">Please sign in</div>
            <div class="fields" style="grid-template-columns:auto auto">
                <label for="user-email">Email address:</label>
                <input id="user-email" type="email" size="25" autofocus>
                <label for="user-pass">very very long label here</label>
                <input id="user-email" type="password" size="25">
            </div>
            <div class="fields" style="grid-template-columns:40% auto">
                <label for="user-email">Email address:</label>
                <input id="user-email" type="email" size="25" autofocus>
                <label for="user-pass">very very long label here</label>
                <input id="user-email" type="password" size="25">
            </div>
            <div class="fields" style="grid-template-columns:40% minmax(0,1fr)">
                <label for="user-email">Email address:</label>
                <input id="user-email" type="email" size="25" autofocus>
                <label for="user-pass">very very long label here</label>
                <input id="user-email" type="password" size="25">
            </div>
        </div>
    </div>
</div>

In the above, the label will shrink because 40% is smaller than the initial width of the label and the input will get bigger.


Concerning the first case, using 1fr auto is also the same as using auto auto because in your case the grid is a shrink to fit element (a flex item) so the content will define its width. The remaining space after setting the input to auto will simply be the initial width of the label.

Temani Afif
  • 245,468
  • 26
  • 309
  • 415
  • Thank you for the answer and explanations! So if I understand correctly, when I did `40% auto`, it calculated the natural (if you will) width of the `label` plus the `input` and made the box big enough to handle that -- and then made the label `40%` of that total. Since that was more than it was originally, the input box stuck out. That really makes sense. I love the `auto auto` solution: let things be as they are. (Plus, as you say, `grid-column-gap`.) – T.J. Crowder May 18 '21 at 08:45
  • 1
    @T.J.Crowder yes exactly, it's a cyclic dependency that you can read about here: https://www.w3.org/TR/css-sizing-3/#cyclic-percentage-contribution .. it's when the percentage of an element depend on its parent and its parent also depend on the child content (one quirck of CSS that gives a lot of you headaches ...) and in most of the case, we first resole to size to auto and then we calculate the percentage – Temani Afif May 18 '21 at 08:52
  • Thanks again! A quick follow-up if I could. If I wanted to have the same visual result but have the `input` *inside* the label (so I wouldn't need `for` and `id`), is that feasible in a reasonable way? (The text of the `label` could be in a `span` if it helps.) I thought maybe the label would be the flex container, but then things don't line up. – T.J. Crowder May 18 '21 at 08:53
  • 1
    @T.J.Crowder only a table layout can do this and it will be supported in all the browsers: https://jsfiddle.net/xjz4v2hb/1/ until we have more support for sub-grid and it will be a trivial task using CSS grid (https://caniuse.com/css-subgrid) – Temani Afif May 18 '21 at 08:58
  • 1
    @T.J.Crowder by the way, display:flex can do it in this case due to some more quirck around percentage size and shrink effect: https://jsfiddle.net/xjz4v2hb/4/ ... the size will be defined with the input+text, both flex container will have the same size since they are block level, we force the label to have 100% width but an input cannot shrink past its content width so the label will shrink to the remain width ... yes a bit crazy – Temani Afif May 18 '21 at 09:04