Create your bingo card in tiddlywiki

Happy New Year! To celebrate the start of 2026, I built a Bingo Card generator (demo) inspired from this website: https://bingocardsfree.com

It will automatically calculates the grid size based on your list of items. If the math works out (odd grid size + spare room), it inserts a “Free Space” in the center. Click a cell to cross it off:

Click the edit button image to toggle a text area and paste your list:

To add it to your wiki, copy-paste the wikitext bellow in a new tiddler, or drag-and-drop the “Bingo 2026” tiddler from the demo link above into your wiki.

Enjoy :slight_smile:

\function get.items(tiddler, field)
[<tiddler>get<field>splitregexp[\n]!is[blank]trim[]]
\end

\function get.all.lines(tiddler, field)
[<tiddler>get<field>splitregexp[\n]]
\end

\function count.items(tiddler, field)
[function[get.items],<tiddler>,<field>count[]]
\end

\function grid.dim(item_count)
[<item_count>power[0.5]ceil[]]
\end

\function total.cells(item_count)
[function[grid.dim],<item_count>power[2]]
\end

\function center.index(cell_count)
[<cell_count>remainder[2]match[1]then<cell_count>add[1]divide[2]]
\end

\function show.star(cell_count, item_count, center_index)
[<cell_count>subtract<item_count>!match[0]]
:filter[<center_index>!is[blank]]
:map[then[yes]else[no]]
\end

\function get.adjusted.index(cell_index, center_index, show_star)
[<show_star>!match[yes]then<cell_index>]
:else[<cell_index>match<center_index>then[free]]
:else[<cell_index>compare:number:gt<center_index>then<cell_index>subtract[1]else<cell_index>]
\end

\function get.display.text(tiddler, field, adjusted_index)
[<adjusted_index>match[free]then[*]]
:else[function[get.items],<tiddler>,<field>nth<adjusted_index>]
\end

<$let
    targetTiddler=<<storyTiddler>>
    targetField="bingo-list"
>

<$checkbox tiddler=`$:/temp/$(thisTiddler)$` field="editing-bingo-list" checked="true" unchecked="false" class="tc-cbx-invisible tc-tiddler-controls"><span title="Edit bingo list"><button class="tc-btn-invisible" style="pointer-events:none">
{{$:/core/images/edit-button}}</button></span>
</$checkbox>


<%if [[$:/temp/$(thisTiddler)$]substitute[]get[editing-bingo-list]] :else[[false]] +[match[true]]%>
<$let
    itemCount={{{ [function[count.items],<targetTiddler>,<targetField>] }}}
    cellCount={{{ [function[total.cells],<itemCount>] }}}
    sideCount={{{ [<cellCount>power[.5]] }}}
    centerIndex={{{ [function[center.index],<cellCount>] }}}
    showStar={{{ [function[show.star],<cellCount>,<itemCount>,<centerIndex>] }}}
>

<<itemCount>> items, <<cellCount>>-cell grid<%if [<showStar>match[yes]] %>, free space = cell <<centerIndex>><%endif%>

</$let>

<div class="bingo-editor-wrapper">
    <ol class="bingo-line-numbers">
        <$list filter="[function[get.all.lines],<targetTiddler>,<targetField>]">
            <li></li>
        </$list>
    </ol>
    
    <$edit field="bingo-list" tag="textarea" class="tc-edit-texteditor bingo-textarea" wrap="off"/>
</div>

<%endif%>

<$let
    itemCount={{{ [function[count.items],<targetTiddler>,<targetField>] }}}
    cellCount={{{ [function[total.cells],<itemCount>] }}}
    sideCount={{{ [<cellCount>power[.5]] }}}
    centerIndex={{{ [function[center.index],<cellCount>] }}}
    showStar={{{ [function[show.star],<cellCount>,<itemCount>,<centerIndex>] }}}
>

<%if [[$:/temp/$(thisTiddler)$]substitute[]get[editing-bingo-list]] :else[[false]] +[!match[true]]%>

<ul class="bingo" style=`--sideCount:$(sideCount)$`>
<$list filter="[range<cellCount>]" variable="currentCell">
<$let 
    adjustedIndex={{{ [function[get.adjusted.index],<currentCell>,<centerIndex>,<showStar>] }}}
    displayText={{{ [function[get.display.text],<targetTiddler>,<targetField>,<adjustedIndex>] }}}
>
    <li>
        <%if [<displayText>!is[blank]] %>
            <%if [<adjustedIndex>!match[free]] %>
                <$checkbox class="tc-cbx-invisible" listField="items-done" checked=<<displayText>>>
                    <$text text=<<displayText>> />
                </$checkbox>
            <%else%>
                <span aria-label="Free Space">
                   <$text text=<<displayText>> />
                </span>
            <%endif%>
        <%endif%>
    </li>
</$let>
</$list>
</ul>

<%endif%>

$$$text/vnd.tiddlywiki
\rules only filteredtranscludeinline transcludeinline macrodef macrocallinline html

<style>
/* --- EDITOR STYLES --- */
.bingo-editor-wrapper {
    position: relative;
    --line-height: 22px; 
    --font-size: 14px;
    --vertical-padding: 10px; 
    margin-top: 1em;
    border: 1px solid #ddd;
    background: #f4f4f4; /* Subtle background for numbers area */
}

.bingo-textarea {
    width: 100%;
    min-height: 200px;
    resize: vertical;
    border: none !important; 
    box-shadow: none !important; 
    
    /* Force Alignment */
    font-family: monospace !important; 
    font-size: var(--font-size) !important;
    line-height: var(--line-height) !important;
    white-space: pre !important; /* Prevents wrapping breaking alignment */
    overflow-x: auto !important;
    
    /* Padding */
    padding-top: var(--vertical-padding) !important;
    padding-bottom: var(--vertical-padding) !important;
    padding-left: 3.5em !important; 
    margin: 0 !important;
    background: white; /* Keep text area white */
}

.bingo-line-numbers {
    position: absolute;
    top: var(--vertical-padding);
    left: 0;
    width: 3em;
    margin: 0;
    padding: 0;
    text-align: right;
    pointer-events: none;
    color: #aaa;
    border-right: 1px solid #eee;
    
    /* Match textarea metrics */
    font-family: monospace !important;
    font-size: var(--font-size) !important;
    line-height: var(--line-height) !important;
    
    counter-reset: line;
    list-style: none;
    z-index: 2;
}

.bingo-line-numbers li {
    height: var(--line-height);
    line-height: var(--line-height);
    box-sizing: border-box;
    padding-right: 8px;
}

.bingo-line-numbers li::before {
    counter-increment: line;
    content: counter(line);
}

/* --- GRID STYLES --- */
ul.bingo {
    list-style: none;
    padding: 0;
    margin-inline: auto;
    
    display: grid;
    aspect-ratio: 1;
    grid-template-columns: repeat(var(--sideCount), 1fr);
    grid-template-rows: repeat(var(--sideCount), 1fr);
    gap: 0; 

    li {
        outline:solid 1px <<color table-border>>;
        outline-offset: -.5px;
        padding: 0;
        display: grid;
        align-items: center;
        text-align: center;
        
        &:has(:focus-visible) {
            outline: solid highlight 1px;
            z-index: 1;
        }

        label {
            display: grid;
            width: 100%;
            height: 100%;
            justify-content: center;
            align-content: center;

            &.tc-checkbox-checked {
                user-select: none;
                position: relative;

                &:after {
                    content: "";
                    position: absolute;
                    inset: -.5px;
                    mask-image: <$text text="""url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'><path d='M0 0 L100 100' stroke='black' stroke-width='1' vector-effect='non-scaling-stroke'/><path d='M100 0 L0 100' stroke='black' stroke-width='1' vector-effect='non-scaling-stroke'/></svg>")"""/>;
                    background-color: <<color table-border>>;
                    background-repeat: no-repeat;
                    background-position: center center;
                    background-size: 100% 100%, auto;
                }
            }
        }
    }
}

.tc-cbx-invisible {
    cursor: pointer;
    input {
        clip: rect(0 0 0 0); 
        clip-path: inset(50%);
        height: 1px;
        overflow: hidden;
        position: absolute;
        white-space: nowrap; 
        width: 1px;
    }
}
</style>
$$$
</$let>

Edit: Added numbered rows and info on the bingo grid while in edit mode

4 Likes

Thanks. It makes me want to back to working on my Corporate-speak bingo card system.

1 Like