Easily usable column layouts like in Notion

Hey everybody!

Why does Obsidian until this day not have an intuitive solution for creating multi-column layouts that does not rely on hacky and not easily usable ways of getting them to work? I would love to just be able to change the size of my columns with my mouse and easily put stuff inside of them. It really makes the experience I have with Obsidian so much more worse, it makes me rethink to switch back to Notion but I really would not want to.

As an example: This would be the layout I’d like to do in Obsidian:

But this is really the only thing that’s viable, as I would not want to take 5 minutes to use any CSS Snippets that are hard to write down and then manually adjust image sizes:

Anybody who shares the same pain? What am I supposed to do?

Cheers!

What you’re doing now: CSS.
You only need to write it once.

Then over time, when you get better at Obsidian, you can have your plugins written. E.g. you select all the text you want side by side, press a keyboard shortcut and the transformation needed is carried out.

For example, for this Callout Grid CSS, I had a pseudo-plugin written by AI, which I saved as a .ts (TypeScript) file:

import { App, Plugin, Notice, Editor, MarkdownView } from 'obsidian';

/**
 * Formats content into a two-column grid layout
 */
const formatToTwoColumnGrid = async (app: App, editor?: Editor): Promise<void> => {
    try {
        let content = '';
        // Check if we have an active editor with selection
        if (editor && editor.somethingSelected()) {
            // Use the selected text from editor
            content = editor.getSelection();
        } else {
            new Notice('No text selected');
            return;
        }
        
        // Extract logical sections from content
        const sections = extractSections(content);
        
        // Calculate distribution for two columns
        const midpoint = Math.ceil(sections.length / 2);
        const firstColumnSections = sections.slice(0, midpoint);
        const secondColumnSections = sections.slice(midpoint);
        
        // Format each column
        const firstColumnContent = formatSections(firstColumnSections);
        const secondColumnContent = formatSections(secondColumnSections);
        
        // Combine into grid format with correct syntax
        const formattedContent = `> [!grid-card-2 same-width]
> > [!grid-item]
${firstColumnContent}
> 
> > [!grid-item]
${secondColumnContent}`;
        
        // Replace selection
        editor.replaceSelection(formattedContent);
        new Notice('Content formatted to two-column grid');
    } catch (error) {
        console.error('Error formatting content:', error);
        new Notice('Error formatting content');
    }
};

/**
 * Extracts logical sections from content, ensuring content is divided
 * even when no clear section markers are present
 */
function extractSections(content: string): string[] {
    const sections: string[] = [];
    const lines = content.split('\n');
    
    let currentSection = '';
    let inCodeBlock = false;
    let sectionStarted = false;
    
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        
        // Track code block state
        if (line.trim().startsWith('```')) {
            inCodeBlock = !inCodeBlock;
        }
        
        // Start a new section if we encounter a heading and we're not in a code block
        const isHeading = line.match(/^#{1,6}\s/) !== null;
        if ((isHeading || (line.trim() === '' && currentSection.trim() !== '')) && !inCodeBlock && sectionStarted) {
            sections.push(currentSection.trim());
            currentSection = '';
            sectionStarted = isHeading;
        }
        
        // Add line to current section
        currentSection += line + '\n';
        if (line.trim() !== '') {
            sectionStarted = true;
        }
        
        // If we're at the end of the content, add the final section
        if (i === lines.length - 1 && currentSection.trim()) {
            sections.push(currentSection.trim());
        }
    }
    
    // If we still have only one section, force-split it into paragraphs
    if (sections.length <= 1) {
        return forceSplitContent(content);
    }
    
    return sections;
}

/**
 * Forces splitting content into roughly equal parts when no natural divisions exist
 */
function forceSplitContent(content: string): string[] {
    const paragraphs = content.split(/\n\s*\n/).filter(p => p.trim() !== '');
    
    // If we have multiple paragraphs, use them as sections
    if (paragraphs.length > 1) {
        return paragraphs;
    }
    
    // If we have only one paragraph, split by sentences or just characters
    const sentences = content.split(/(?<=[.!?])\s+/).filter(s => s.trim() !== '');
    if (sentences.length > 1) {
        return sentences;
    }
    
    // Last resort: just split the content in half
    const midpoint = Math.ceil(content.length / 2);
    const firstHalf = content.substring(0, midpoint).trim();
    const secondHalf = content.substring(midpoint).trim();
    
    return [firstHalf, secondHalf].filter(part => part !== '');
}

/**
 * Formats sections for grid display
 */
function formatSections(sections: string[]): string {
    let result = '';
    
    for (const section of sections) {
        let formattedSection = '';
        
        // Process each line with proper indentation
        const lines = section.split('\n');
        for (const line of lines) {
            // Add proper indentation
            formattedSection += '> > ' + line + '\n';
        }
        
        // Add a blank line between sections if not the last section
        result += formattedSection + '> > \n';
    }
    
    // Remove trailing newlines and extra spaces
    return result.trim();
}

export class EnhancedGridFormatterPlugin extends Plugin {
    async onload() {
        // Command that works with editor selection
        this.addCommand({
            id: 'format-selection-to-grid',
            name: 'Format selection to two-column grid',
            editorCallback: (editor: Editor, view: MarkdownView) => {
                formatToTwoColumnGrid(this.app, editor);
            }
        });
    }
}

export async function invoke(app: App): Promise<void> {
    const activeView = app.workspace.getActiveViewOfType(MarkdownView);
    if (activeView && activeView.editor) {
        return formatToTwoColumnGrid(app, activeView.editor);
    } else {
        new Notice('No active markdown editor');
        return;
    }
}

It works roughly as I want it.
The plugin that I use for running my pseudo-plugins is what I mentioned here:

This is just an idea here, not to be taken as a fix for your use case. I’m just throwing up what is possible (and you can use this script as a sample to get AI such as Claude going).
Once you have 1-2 working scripts, it can get addictive: for one’s workflow(s), one can get 20-30 scripts written, but as I mentioned in this post, some coding mindset and patience to tinker with stuff is required.