the_permalink() echoes the output, and so does the_title() by default. So you should use get_permalink() and get_the_title():
$html .= '<h3><a href="' . get_permalink() . '">' . get_the_title() . '</a></h3>';
Or use the_title() with the third parameter set to false to return and not echo the output:
$html .= '<h3><a href="' . get_permalink() . '">' . the_title( '', '', false ) . '</a></h3>';
Update: The return $html; (in your original non-edited question) was actually outside the function and that would result the shortcode to give you no output! So make sure that it’s inside the function in your actual code. 🙂 And for secondary/custom WP_Query instances like your $loop variable, you just need to call wp_reset_postdata() and not wp_reset_query() since secondary queries don’t touch the (global) $wp_query variable. Unless of course, in your code, you modified that variable. But why would you do that.